|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 // ********** |
|
6 // Title: tabitems.js |
|
7 |
|
8 // ########## |
|
9 // Class: TabItem |
|
10 // An <Item> that represents a tab. Also implements the <Subscribable> interface. |
|
11 // |
|
12 // Parameters: |
|
13 // tab - a xul:tab |
|
14 function TabItem(tab, options) { |
|
15 Utils.assert(tab, "tab"); |
|
16 |
|
17 this.tab = tab; |
|
18 // register this as the tab's tabItem |
|
19 this.tab._tabViewTabItem = this; |
|
20 |
|
21 if (!options) |
|
22 options = {}; |
|
23 |
|
24 // ___ set up div |
|
25 document.body.appendChild(TabItems.fragment().cloneNode(true)); |
|
26 |
|
27 // The document fragment contains just one Node |
|
28 // As per DOM3 appendChild: it will then be the last child |
|
29 let div = document.body.lastChild; |
|
30 let $div = iQ(div); |
|
31 |
|
32 this._showsCachedData = false; |
|
33 this.canvasSizeForced = false; |
|
34 this.$thumb = iQ('.thumb', $div); |
|
35 this.$fav = iQ('.favicon', $div); |
|
36 this.$tabTitle = iQ('.tab-title', $div); |
|
37 this.$canvas = iQ('.thumb canvas', $div); |
|
38 this.$cachedThumb = iQ('img.cached-thumb', $div); |
|
39 this.$favImage = iQ('.favicon>img', $div); |
|
40 this.$close = iQ('.close', $div); |
|
41 |
|
42 this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]); |
|
43 |
|
44 this._hidden = false; |
|
45 this.isATabItem = true; |
|
46 this.keepProportional = true; |
|
47 this._hasBeenDrawn = false; |
|
48 this._reconnected = false; |
|
49 this.isDragging = false; |
|
50 this.isStacked = false; |
|
51 |
|
52 // Read off the total vertical and horizontal padding on the tab container |
|
53 // and cache this value, as it must be the same for every TabItem. |
|
54 if (Utils.isEmptyObject(TabItems.tabItemPadding)) { |
|
55 TabItems.tabItemPadding.x = parseInt($div.css('padding-left')) |
|
56 + parseInt($div.css('padding-right')); |
|
57 |
|
58 TabItems.tabItemPadding.y = parseInt($div.css('padding-top')) |
|
59 + parseInt($div.css('padding-bottom')); |
|
60 } |
|
61 |
|
62 this.bounds = new Rect(0,0,1,1); |
|
63 |
|
64 this._lastTabUpdateTime = Date.now(); |
|
65 |
|
66 // ___ superclass setup |
|
67 this._init(div); |
|
68 |
|
69 // ___ drag/drop |
|
70 // override dropOptions with custom tabitem methods |
|
71 this.dropOptions.drop = function(e) { |
|
72 let groupItem = drag.info.item.parent; |
|
73 groupItem.add(drag.info.$el); |
|
74 }; |
|
75 |
|
76 this.draggable(); |
|
77 |
|
78 let self = this; |
|
79 |
|
80 // ___ more div setup |
|
81 $div.mousedown(function(e) { |
|
82 if (!Utils.isRightClick(e)) |
|
83 self.lastMouseDownTarget = e.target; |
|
84 }); |
|
85 |
|
86 $div.mouseup(function(e) { |
|
87 var same = (e.target == self.lastMouseDownTarget); |
|
88 self.lastMouseDownTarget = null; |
|
89 if (!same) |
|
90 return; |
|
91 |
|
92 // press close button or middle mouse click |
|
93 if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) { |
|
94 self.closedManually = true; |
|
95 self.close(); |
|
96 } else { |
|
97 if (!Items.item(this).isDragging) |
|
98 self.zoomIn(); |
|
99 } |
|
100 }); |
|
101 |
|
102 this.droppable(true); |
|
103 |
|
104 this.$close.attr("title", tabbrowserString("tabs.closeTab")); |
|
105 |
|
106 TabItems.register(this); |
|
107 |
|
108 // ___ reconnect to data from Storage |
|
109 if (!TabItems.reconnectingPaused()) |
|
110 this._reconnect(options); |
|
111 }; |
|
112 |
|
113 TabItem.prototype = Utils.extend(new Item(), new Subscribable(), { |
|
114 // ---------- |
|
115 // Function: toString |
|
116 // Prints [TabItem (tab)] for debug use |
|
117 toString: function TabItem_toString() { |
|
118 return "[TabItem (" + this.tab + ")]"; |
|
119 }, |
|
120 |
|
121 // ---------- |
|
122 // Function: forceCanvasSize |
|
123 // Repaints the thumbnail with the given resolution, and forces it |
|
124 // to stay that resolution until unforceCanvasSize is called. |
|
125 forceCanvasSize: function TabItem_forceCanvasSize(w, h) { |
|
126 this.canvasSizeForced = true; |
|
127 this.$canvas[0].width = w; |
|
128 this.$canvas[0].height = h; |
|
129 this.tabCanvas.paint(); |
|
130 }, |
|
131 |
|
132 // ---------- |
|
133 // Function: unforceCanvasSize |
|
134 // Stops holding the thumbnail resolution; allows it to shift to the |
|
135 // size of thumbnail on screen. Note that this call does not nest, unlike |
|
136 // <TabItems.resumePainting>; if you call forceCanvasSize multiple |
|
137 // times, you just need a single unforce to clear them all. |
|
138 unforceCanvasSize: function TabItem_unforceCanvasSize() { |
|
139 this.canvasSizeForced = false; |
|
140 }, |
|
141 |
|
142 // ---------- |
|
143 // Function: isShowingCachedData |
|
144 // Returns a boolean indicates whether the cached data is being displayed or |
|
145 // not. |
|
146 isShowingCachedData: function TabItem_isShowingCachedData() { |
|
147 return this._showsCachedData; |
|
148 }, |
|
149 |
|
150 // ---------- |
|
151 // Function: showCachedData |
|
152 // Shows the cached data i.e. image and title. Note: this method should only |
|
153 // be called at browser startup with the cached data avaliable. |
|
154 showCachedData: function TabItem_showCachedData() { |
|
155 let {title, url} = this.getTabState(); |
|
156 let thumbnailURL = gPageThumbnails.getThumbnailURL(url); |
|
157 |
|
158 this.$cachedThumb.attr("src", thumbnailURL).show(); |
|
159 this.$canvas.css({opacity: 0}); |
|
160 |
|
161 let tooltip = (title && title != url ? title + "\n" + url : url); |
|
162 this.$tabTitle.text(title).attr("title", tooltip); |
|
163 this._showsCachedData = true; |
|
164 }, |
|
165 |
|
166 // ---------- |
|
167 // Function: hideCachedData |
|
168 // Hides the cached data i.e. image and title and show the canvas. |
|
169 hideCachedData: function TabItem_hideCachedData() { |
|
170 this.$cachedThumb.attr("src", "").hide(); |
|
171 this.$canvas.css({opacity: 1.0}); |
|
172 this._showsCachedData = false; |
|
173 }, |
|
174 |
|
175 // ---------- |
|
176 // Function: getStorageData |
|
177 // Get data to be used for persistent storage of this object. |
|
178 getStorageData: function TabItem_getStorageData() { |
|
179 let data = { |
|
180 groupID: (this.parent ? this.parent.id : 0) |
|
181 }; |
|
182 if (this.parent && this.parent.getActiveTab() == this) |
|
183 data.active = true; |
|
184 |
|
185 return data; |
|
186 }, |
|
187 |
|
188 // ---------- |
|
189 // Function: save |
|
190 // Store persistent for this object. |
|
191 save: function TabItem_save() { |
|
192 try { |
|
193 if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save |
|
194 return; |
|
195 |
|
196 let data = this.getStorageData(); |
|
197 if (TabItems.storageSanity(data)) |
|
198 Storage.saveTab(this.tab, data); |
|
199 } catch(e) { |
|
200 Utils.log("Error in saving tab value: "+e); |
|
201 } |
|
202 }, |
|
203 |
|
204 // ---------- |
|
205 // Function: _getCurrentTabStateEntry |
|
206 // Returns the current tab state's active history entry. |
|
207 _getCurrentTabStateEntry: function TabItem__getCurrentTabStateEntry() { |
|
208 let tabState = Storage.getTabState(this.tab); |
|
209 |
|
210 if (tabState) { |
|
211 let index = (tabState.index || tabState.entries.length) - 1; |
|
212 if (index in tabState.entries) |
|
213 return tabState.entries[index]; |
|
214 } |
|
215 |
|
216 return null; |
|
217 }, |
|
218 |
|
219 // ---------- |
|
220 // Function: getTabState |
|
221 // Returns the current tab state, i.e. the title and URL of the active |
|
222 // history entry. |
|
223 getTabState: function TabItem_getTabState() { |
|
224 let entry = this._getCurrentTabStateEntry(); |
|
225 let title = ""; |
|
226 let url = ""; |
|
227 |
|
228 if (entry) { |
|
229 if (entry.title) |
|
230 title = entry.title; |
|
231 |
|
232 url = entry.url; |
|
233 } else { |
|
234 url = this.tab.linkedBrowser.currentURI.spec; |
|
235 } |
|
236 |
|
237 return {title: title, url: url}; |
|
238 }, |
|
239 |
|
240 // ---------- |
|
241 // Function: _reconnect |
|
242 // Load the reciever's persistent data from storage. If there is none, |
|
243 // treats it as a new tab. |
|
244 // |
|
245 // Parameters: |
|
246 // options - an object with additional parameters, see below |
|
247 // |
|
248 // Possible options: |
|
249 // groupItemId - if the tab doesn't have any data associated with it and |
|
250 // groupItemId is available, add the tab to that group. |
|
251 _reconnect: function TabItem__reconnect(options) { |
|
252 Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected"); |
|
253 Utils.assertThrow(this.tab, "should have a xul:tab"); |
|
254 |
|
255 let tabData = Storage.getTabData(this.tab); |
|
256 let groupItem; |
|
257 |
|
258 if (tabData && TabItems.storageSanity(tabData)) { |
|
259 // Show the cached data while we're waiting for the tabItem to be updated. |
|
260 // If the tab isn't restored yet this acts as a placeholder until it is. |
|
261 this.showCachedData(); |
|
262 |
|
263 if (this.parent) |
|
264 this.parent.remove(this, {immediately: true}); |
|
265 |
|
266 if (tabData.groupID) |
|
267 groupItem = GroupItems.groupItem(tabData.groupID); |
|
268 else |
|
269 groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds}); |
|
270 |
|
271 if (groupItem) { |
|
272 groupItem.add(this, {immediately: true}); |
|
273 |
|
274 // restore the active tab for each group between browser sessions |
|
275 if (tabData.active) |
|
276 groupItem.setActiveTab(this); |
|
277 |
|
278 // if it matches the selected tab or no active tab and the browser |
|
279 // tab is hidden, the active group item would be set. |
|
280 if (this.tab.selected || |
|
281 (!GroupItems.getActiveGroupItem() && !this.tab.hidden)) |
|
282 UI.setActive(this.parent); |
|
283 } |
|
284 } else { |
|
285 if (options && options.groupItemId) |
|
286 groupItem = GroupItems.groupItem(options.groupItemId); |
|
287 |
|
288 if (groupItem) { |
|
289 groupItem.add(this, {immediately: true}); |
|
290 } else { |
|
291 // create tab group by double click is handled in UI_init(). |
|
292 GroupItems.newTab(this, {immediately: true}); |
|
293 } |
|
294 } |
|
295 |
|
296 this._reconnected = true; |
|
297 this.save(); |
|
298 this._sendToSubscribers("reconnected"); |
|
299 }, |
|
300 |
|
301 // ---------- |
|
302 // Function: setHidden |
|
303 // Hide/unhide this item |
|
304 setHidden: function TabItem_setHidden(val) { |
|
305 if (val) |
|
306 this.addClass("tabHidden"); |
|
307 else |
|
308 this.removeClass("tabHidden"); |
|
309 this._hidden = val; |
|
310 }, |
|
311 |
|
312 // ---------- |
|
313 // Function: getHidden |
|
314 // Return hide state of item |
|
315 getHidden: function TabItem_getHidden() { |
|
316 return this._hidden; |
|
317 }, |
|
318 |
|
319 // ---------- |
|
320 // Function: setBounds |
|
321 // Moves this item to the specified location and size. |
|
322 // |
|
323 // Parameters: |
|
324 // rect - a <Rect> giving the new bounds |
|
325 // immediately - true if it should not animate; default false |
|
326 // options - an object with additional parameters, see below |
|
327 // |
|
328 // Possible options: |
|
329 // force - true to always update the DOM even if the bounds haven't changed; default false |
|
330 setBounds: function TabItem_setBounds(inRect, immediately, options) { |
|
331 Utils.assert(Utils.isRect(inRect), 'TabItem.setBounds: rect is not a real rectangle!'); |
|
332 |
|
333 if (!options) |
|
334 options = {}; |
|
335 |
|
336 // force the input size to be valid |
|
337 let validSize = TabItems.calcValidSize( |
|
338 new Point(inRect.width, inRect.height), |
|
339 {hideTitle: (this.isStacked || options.hideTitle === true)}); |
|
340 let rect = new Rect(inRect.left, inRect.top, |
|
341 validSize.x, validSize.y); |
|
342 |
|
343 var css = {}; |
|
344 |
|
345 if (rect.left != this.bounds.left || options.force) |
|
346 css.left = rect.left; |
|
347 |
|
348 if (rect.top != this.bounds.top || options.force) |
|
349 css.top = rect.top; |
|
350 |
|
351 if (rect.width != this.bounds.width || options.force) { |
|
352 css.width = rect.width - TabItems.tabItemPadding.x; |
|
353 css.fontSize = TabItems.getFontSizeFromWidth(rect.width); |
|
354 css.fontSize += 'px'; |
|
355 } |
|
356 |
|
357 if (rect.height != this.bounds.height || options.force) { |
|
358 css.height = rect.height - TabItems.tabItemPadding.y; |
|
359 if (!this.isStacked) |
|
360 css.height -= TabItems.fontSizeRange.max; |
|
361 } |
|
362 |
|
363 if (Utils.isEmptyObject(css)) |
|
364 return; |
|
365 |
|
366 this.bounds.copy(rect); |
|
367 |
|
368 // If this is a brand new tab don't animate it in from |
|
369 // a random location (i.e., from [0,0]). Instead, just |
|
370 // have it appear where it should be. |
|
371 if (immediately || (!this._hasBeenDrawn)) { |
|
372 this.$container.css(css); |
|
373 } else { |
|
374 TabItems.pausePainting(); |
|
375 this.$container.animate(css, { |
|
376 duration: 200, |
|
377 easing: "tabviewBounce", |
|
378 complete: function() { |
|
379 TabItems.resumePainting(); |
|
380 } |
|
381 }); |
|
382 } |
|
383 |
|
384 if (css.fontSize && !(this.parent && this.parent.isStacked())) { |
|
385 if (css.fontSize < TabItems.fontSizeRange.min) |
|
386 immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut(); |
|
387 else |
|
388 immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn(); |
|
389 } |
|
390 |
|
391 if (css.width) { |
|
392 TabItems.update(this.tab); |
|
393 |
|
394 let widthRange, proportion; |
|
395 |
|
396 if (this.parent && this.parent.isStacked()) { |
|
397 if (UI.rtl) { |
|
398 this.$fav.css({top:0, right:0}); |
|
399 } else { |
|
400 this.$fav.css({top:0, left:0}); |
|
401 } |
|
402 widthRange = new Range(70, 90); |
|
403 proportion = widthRange.proportion(css.width); // between 0 and 1 |
|
404 } else { |
|
405 if (UI.rtl) { |
|
406 this.$fav.css({top:4, right:2}); |
|
407 } else { |
|
408 this.$fav.css({top:4, left:4}); |
|
409 } |
|
410 widthRange = new Range(40, 45); |
|
411 proportion = widthRange.proportion(css.width); // between 0 and 1 |
|
412 } |
|
413 |
|
414 if (proportion <= .1) |
|
415 this.$close.hide(); |
|
416 else |
|
417 this.$close.show().css({opacity:proportion}); |
|
418 |
|
419 var pad = 1 + 5 * proportion; |
|
420 var alphaRange = new Range(0.1,0.2); |
|
421 this.$fav.css({ |
|
422 "-moz-padding-start": pad + "px", |
|
423 "-moz-padding-end": pad + 2 + "px", |
|
424 "padding-top": pad + "px", |
|
425 "padding-bottom": pad + "px", |
|
426 "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")", |
|
427 }); |
|
428 } |
|
429 |
|
430 this._hasBeenDrawn = true; |
|
431 |
|
432 UI.clearShouldResizeItems(); |
|
433 |
|
434 rect = this.getBounds(); // ensure that it's a <Rect> |
|
435 |
|
436 Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!'); |
|
437 |
|
438 if (!this.parent && Utils.isValidXULTab(this.tab)) |
|
439 this.setTrenches(rect); |
|
440 |
|
441 this.save(); |
|
442 }, |
|
443 |
|
444 // ---------- |
|
445 // Function: setZ |
|
446 // Sets the z-index for this item. |
|
447 setZ: function TabItem_setZ(value) { |
|
448 this.zIndex = value; |
|
449 this.$container.css({zIndex: value}); |
|
450 }, |
|
451 |
|
452 // ---------- |
|
453 // Function: close |
|
454 // Closes this item (actually closes the tab associated with it, which automatically |
|
455 // closes the item. |
|
456 // Parameters: |
|
457 // groupClose - true if this method is called by group close action. |
|
458 // Returns true if this tab is removed. |
|
459 close: function TabItem_close(groupClose) { |
|
460 // When the last tab is closed, put a new tab into closing tab's group. If |
|
461 // closing tab doesn't belong to a group and no empty group, create a new |
|
462 // one for the new tab. |
|
463 if (!groupClose && gBrowser.tabs.length == 1) { |
|
464 let group = this.tab._tabViewTabItem.parent; |
|
465 group.newTab(null, { closedLastTab: true }); |
|
466 } |
|
467 |
|
468 // when "TabClose" event is fired, the browser tab is about to close and our |
|
469 // item "close" is fired before the browser tab actually get closed. |
|
470 // Therefore, we need "tabRemoved" event below. |
|
471 gBrowser.removeTab(this.tab); |
|
472 let tabClosed = !this.tab; |
|
473 |
|
474 if (tabClosed) |
|
475 this._sendToSubscribers("tabRemoved"); |
|
476 |
|
477 // No need to explicitly delete the tab data, becasue sessionstore data |
|
478 // associated with the tab will automatically go away |
|
479 return tabClosed; |
|
480 }, |
|
481 |
|
482 // ---------- |
|
483 // Function: addClass |
|
484 // Adds the specified CSS class to this item's container DOM element. |
|
485 addClass: function TabItem_addClass(className) { |
|
486 this.$container.addClass(className); |
|
487 }, |
|
488 |
|
489 // ---------- |
|
490 // Function: removeClass |
|
491 // Removes the specified CSS class from this item's container DOM element. |
|
492 removeClass: function TabItem_removeClass(className) { |
|
493 this.$container.removeClass(className); |
|
494 }, |
|
495 |
|
496 // ---------- |
|
497 // Function: makeActive |
|
498 // Updates this item to visually indicate that it's active. |
|
499 makeActive: function TabItem_makeActive() { |
|
500 this.$container.addClass("focus"); |
|
501 |
|
502 if (this.parent) |
|
503 this.parent.setActiveTab(this); |
|
504 }, |
|
505 |
|
506 // ---------- |
|
507 // Function: makeDeactive |
|
508 // Updates this item to visually indicate that it's not active. |
|
509 makeDeactive: function TabItem_makeDeactive() { |
|
510 this.$container.removeClass("focus"); |
|
511 }, |
|
512 |
|
513 // ---------- |
|
514 // Function: zoomIn |
|
515 // Allows you to select the tab and zoom in on it, thereby bringing you |
|
516 // to the tab in Firefox to interact with. |
|
517 // Parameters: |
|
518 // isNewBlankTab - boolean indicates whether it is a newly opened blank tab. |
|
519 zoomIn: function TabItem_zoomIn(isNewBlankTab) { |
|
520 // don't allow zoom in if its group is hidden |
|
521 if (this.parent && this.parent.hidden) |
|
522 return; |
|
523 |
|
524 let self = this; |
|
525 let $tabEl = this.$container; |
|
526 let $canvas = this.$canvas; |
|
527 |
|
528 Search.hide(); |
|
529 |
|
530 UI.setActive(this); |
|
531 TabItems._update(this.tab, {force: true}); |
|
532 |
|
533 // Zoom in! |
|
534 let tab = this.tab; |
|
535 |
|
536 function onZoomDone() { |
|
537 $canvas.css({ 'transform': null }); |
|
538 $tabEl.removeClass("front"); |
|
539 |
|
540 UI.goToTab(tab); |
|
541 |
|
542 // tab might not be selected because hideTabView() is invoked after |
|
543 // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab |
|
544 if (!tab.selected) { |
|
545 UI.onTabSelect(gBrowser.selectedTab); |
|
546 } else { |
|
547 if (isNewBlankTab) |
|
548 gWindow.gURLBar.focus(); |
|
549 } |
|
550 if (self.parent && self.parent.expanded) |
|
551 self.parent.collapse(); |
|
552 |
|
553 self._sendToSubscribers("zoomedIn"); |
|
554 } |
|
555 |
|
556 let animateZoom = gPrefBranch.getBoolPref("animate_zoom"); |
|
557 if (animateZoom) { |
|
558 let transform = this.getZoomTransform(); |
|
559 TabItems.pausePainting(); |
|
560 |
|
561 if (this.parent && this.parent.expanded) |
|
562 $tabEl.removeClass("stack-trayed"); |
|
563 $tabEl.addClass("front"); |
|
564 $canvas |
|
565 .css({ 'transform-origin': transform.transformOrigin }) |
|
566 .animate({ 'transform': transform.transform }, { |
|
567 duration: 230, |
|
568 easing: 'fast', |
|
569 complete: function() { |
|
570 onZoomDone(); |
|
571 |
|
572 setTimeout(function() { |
|
573 TabItems.resumePainting(); |
|
574 }, 0); |
|
575 } |
|
576 }); |
|
577 } else { |
|
578 setTimeout(onZoomDone, 0); |
|
579 } |
|
580 }, |
|
581 |
|
582 // ---------- |
|
583 // Function: zoomOut |
|
584 // Handles the zoom down animation after returning to TabView. |
|
585 // It is expected that this routine will be called from the chrome thread |
|
586 // |
|
587 // Parameters: |
|
588 // complete - a function to call after the zoom down animation |
|
589 zoomOut: function TabItem_zoomOut(complete) { |
|
590 let $tab = this.$container, $canvas = this.$canvas; |
|
591 var self = this; |
|
592 |
|
593 let onZoomDone = function onZoomDone() { |
|
594 $tab.removeClass("front"); |
|
595 $canvas.css("transform", null); |
|
596 |
|
597 if (typeof complete == "function") |
|
598 complete(); |
|
599 }; |
|
600 |
|
601 UI.setActive(this); |
|
602 TabItems._update(this.tab, {force: true}); |
|
603 |
|
604 $tab.addClass("front"); |
|
605 |
|
606 let animateZoom = gPrefBranch.getBoolPref("animate_zoom"); |
|
607 if (animateZoom) { |
|
608 // The scaleCheat of 2 here is a clever way to speed up the zoom-out |
|
609 // code. See getZoomTransform() below. |
|
610 let transform = this.getZoomTransform(2); |
|
611 TabItems.pausePainting(); |
|
612 |
|
613 $canvas.css({ |
|
614 'transform': transform.transform, |
|
615 'transform-origin': transform.transformOrigin |
|
616 }); |
|
617 |
|
618 $canvas.animate({ "transform": "scale(1.0)" }, { |
|
619 duration: 300, |
|
620 easing: 'cubic-bezier', // note that this is legal easing, even without parameters |
|
621 complete: function() { |
|
622 TabItems.resumePainting(); |
|
623 onZoomDone(); |
|
624 } |
|
625 }); |
|
626 } else { |
|
627 onZoomDone(); |
|
628 } |
|
629 }, |
|
630 |
|
631 // ---------- |
|
632 // Function: getZoomTransform |
|
633 // Returns the transform function which represents the maximum bounds of the |
|
634 // tab thumbnail in the zoom animation. |
|
635 getZoomTransform: function TabItem_getZoomTransform(scaleCheat) { |
|
636 // Taking the bounds of the container (as opposed to the canvas) makes us |
|
637 // immune to any transformations applied to the canvas. |
|
638 let { left, top, width, height, right, bottom } = this.$container.bounds(); |
|
639 |
|
640 let { innerWidth: windowWidth, innerHeight: windowHeight } = window; |
|
641 |
|
642 // The scaleCheat is a clever way to speed up the zoom-in code. |
|
643 // Because image scaling is slowest on big images, we cheat and stop |
|
644 // the image at scaled-down size and placed accordingly. Because the |
|
645 // animation is fast, you can't see the difference but it feels a lot |
|
646 // zippier. The only trick is choosing the right animation function so |
|
647 // that you don't see a change in percieved animation speed from frame #1 |
|
648 // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame |
|
649 // of real animation). Choosing an animation that starts fast is key. |
|
650 |
|
651 if (!scaleCheat) |
|
652 scaleCheat = 1.7; |
|
653 |
|
654 let zoomWidth = width + (window.innerWidth - width) / scaleCheat; |
|
655 let zoomScaleFactor = zoomWidth / width; |
|
656 |
|
657 let zoomHeight = height * zoomScaleFactor; |
|
658 let zoomTop = top * (1 - 1/scaleCheat); |
|
659 let zoomLeft = left * (1 - 1/scaleCheat); |
|
660 |
|
661 let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100; |
|
662 let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100; |
|
663 |
|
664 return { |
|
665 transformOrigin: xOrigin + "% " + yOrigin + "%", |
|
666 transform: "scale(" + zoomScaleFactor + ")" |
|
667 }; |
|
668 }, |
|
669 |
|
670 // ---------- |
|
671 // Function: updateCanvas |
|
672 // Updates the tabitem's canvas. |
|
673 updateCanvas: function TabItem_updateCanvas() { |
|
674 // ___ thumbnail |
|
675 let $canvas = this.$canvas; |
|
676 if (!this.canvasSizeForced) { |
|
677 let w = $canvas.width(); |
|
678 let h = $canvas.height(); |
|
679 if (w != $canvas[0].width || h != $canvas[0].height) { |
|
680 $canvas[0].width = w; |
|
681 $canvas[0].height = h; |
|
682 } |
|
683 } |
|
684 |
|
685 TabItems._lastUpdateTime = Date.now(); |
|
686 this._lastTabUpdateTime = TabItems._lastUpdateTime; |
|
687 |
|
688 if (this.tabCanvas) |
|
689 this.tabCanvas.paint(); |
|
690 |
|
691 // ___ cache |
|
692 if (this.isShowingCachedData()) |
|
693 this.hideCachedData(); |
|
694 } |
|
695 }); |
|
696 |
|
697 // ########## |
|
698 // Class: TabItems |
|
699 // Singleton for managing <TabItem>s |
|
700 let TabItems = { |
|
701 minTabWidth: 40, |
|
702 tabWidth: 160, |
|
703 tabHeight: 120, |
|
704 tabAspect: 0, // set in init |
|
705 invTabAspect: 0, // set in init |
|
706 fontSize: 9, |
|
707 fontSizeRange: new Range(8,15), |
|
708 _fragment: null, |
|
709 items: [], |
|
710 paintingPaused: 0, |
|
711 _tabsWaitingForUpdate: null, |
|
712 _heartbeat: null, // see explanation at startHeartbeat() below |
|
713 _heartbeatTiming: 200, // milliseconds between calls |
|
714 _maxTimeForUpdating: 200, // milliseconds that consecutive updates can take |
|
715 _lastUpdateTime: Date.now(), |
|
716 _eventListeners: [], |
|
717 _pauseUpdateForTest: false, |
|
718 _reconnectingPaused: false, |
|
719 tabItemPadding: {}, |
|
720 _mozAfterPaintHandler: null, |
|
721 |
|
722 // ---------- |
|
723 // Function: toString |
|
724 // Prints [TabItems count=count] for debug use |
|
725 toString: function TabItems_toString() { |
|
726 return "[TabItems count=" + this.items.length + "]"; |
|
727 }, |
|
728 |
|
729 // ---------- |
|
730 // Function: init |
|
731 // Set up the necessary tracking to maintain the <TabItems>s. |
|
732 init: function TabItems_init() { |
|
733 Utils.assert(window.AllTabs, "AllTabs must be initialized first"); |
|
734 let self = this; |
|
735 |
|
736 // Set up tab priority queue |
|
737 this._tabsWaitingForUpdate = new TabPriorityQueue(); |
|
738 this.minTabHeight = this.minTabWidth * this.tabHeight / this.tabWidth; |
|
739 this.tabAspect = this.tabHeight / this.tabWidth; |
|
740 this.invTabAspect = 1 / this.tabAspect; |
|
741 |
|
742 let $canvas = iQ("<canvas>") |
|
743 .attr('moz-opaque', ''); |
|
744 $canvas.appendTo(iQ("body")); |
|
745 $canvas.hide(); |
|
746 |
|
747 let mm = gWindow.messageManager; |
|
748 this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this); |
|
749 mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler); |
|
750 |
|
751 // When a tab is opened, create the TabItem |
|
752 this._eventListeners.open = function (event) { |
|
753 let tab = event.target; |
|
754 |
|
755 if (!tab.pinned) |
|
756 self.link(tab); |
|
757 } |
|
758 // When a tab's content is loaded, show the canvas and hide the cached data |
|
759 // if necessary. |
|
760 this._eventListeners.attrModified = function (event) { |
|
761 let tab = event.target; |
|
762 |
|
763 if (!tab.pinned) |
|
764 self.update(tab); |
|
765 } |
|
766 // When a tab is closed, unlink. |
|
767 this._eventListeners.close = function (event) { |
|
768 let tab = event.target; |
|
769 |
|
770 // XXX bug #635975 - don't unlink the tab if the dom window is closing. |
|
771 if (!tab.pinned && !UI.isDOMWindowClosing) |
|
772 self.unlink(tab); |
|
773 } |
|
774 for (let name in this._eventListeners) { |
|
775 AllTabs.register(name, this._eventListeners[name]); |
|
776 } |
|
777 |
|
778 let activeGroupItem = GroupItems.getActiveGroupItem(); |
|
779 let activeGroupItemId = activeGroupItem ? activeGroupItem.id : null; |
|
780 // For each tab, create the link. |
|
781 AllTabs.tabs.forEach(function (tab) { |
|
782 if (tab.pinned) |
|
783 return; |
|
784 |
|
785 let options = {immediately: true}; |
|
786 // if tab is visible in the tabstrip and doesn't have any data stored in |
|
787 // the session store (see TabItem__reconnect), it implies that it is a |
|
788 // new tab which is created before Panorama is initialized. Therefore, |
|
789 // passing the active group id to the link() method for setting it up. |
|
790 if (!tab.hidden && activeGroupItemId) |
|
791 options.groupItemId = activeGroupItemId; |
|
792 self.link(tab, options); |
|
793 self.update(tab); |
|
794 }); |
|
795 }, |
|
796 |
|
797 // ---------- |
|
798 // Function: uninit |
|
799 uninit: function TabItems_uninit() { |
|
800 let mm = gWindow.messageManager; |
|
801 mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler); |
|
802 |
|
803 for (let name in this._eventListeners) { |
|
804 AllTabs.unregister(name, this._eventListeners[name]); |
|
805 } |
|
806 this.items.forEach(function(tabItem) { |
|
807 delete tabItem.tab._tabViewTabItem; |
|
808 |
|
809 for (let x in tabItem) { |
|
810 if (typeof tabItem[x] == "object") |
|
811 tabItem[x] = null; |
|
812 } |
|
813 }); |
|
814 |
|
815 this.items = null; |
|
816 this._eventListeners = null; |
|
817 this._lastUpdateTime = null; |
|
818 this._tabsWaitingForUpdate.clear(); |
|
819 }, |
|
820 |
|
821 // ---------- |
|
822 // Function: fragment |
|
823 // Return a DocumentFragment which has a single <div> child. This child node |
|
824 // will act as a template for all TabItem containers. |
|
825 // The first call of this function caches the DocumentFragment in _fragment. |
|
826 fragment: function TabItems_fragment() { |
|
827 if (this._fragment) |
|
828 return this._fragment; |
|
829 |
|
830 let div = document.createElement("div"); |
|
831 div.classList.add("tab"); |
|
832 div.innerHTML = "<div class='thumb'>" + |
|
833 "<img class='cached-thumb' style='display:none'/><canvas moz-opaque/></div>" + |
|
834 "<div class='favicon'><img/></div>" + |
|
835 "<span class='tab-title'> </span>" + |
|
836 "<div class='close'></div>"; |
|
837 this._fragment = document.createDocumentFragment(); |
|
838 this._fragment.appendChild(div); |
|
839 |
|
840 return this._fragment; |
|
841 }, |
|
842 |
|
843 // Function: _isComplete |
|
844 // Checks whether the xul:tab has fully loaded and calls a callback with a |
|
845 // boolean indicates whether the tab is loaded or not. |
|
846 _isComplete: function TabItems__isComplete(tab, callback) { |
|
847 Utils.assertThrow(tab, "tab"); |
|
848 |
|
849 // A pending tab can't be complete, yet. |
|
850 if (tab.hasAttribute("pending")) { |
|
851 setTimeout(() => callback(false)); |
|
852 return; |
|
853 } |
|
854 |
|
855 let mm = tab.linkedBrowser.messageManager; |
|
856 let message = "Panorama:isDocumentLoaded"; |
|
857 |
|
858 mm.addMessageListener(message, function onMessage(cx) { |
|
859 mm.removeMessageListener(cx.name, onMessage); |
|
860 callback(cx.json.isLoaded); |
|
861 }); |
|
862 mm.sendAsyncMessage(message); |
|
863 }, |
|
864 |
|
865 // ---------- |
|
866 // Function: onMozAfterPaint |
|
867 // Called when a web page is painted. |
|
868 onMozAfterPaint: function TabItems_onMozAfterPaint(cx) { |
|
869 let index = gBrowser.browsers.indexOf(cx.target); |
|
870 if (index == -1) |
|
871 return; |
|
872 |
|
873 let tab = gBrowser.tabs[index]; |
|
874 if (!tab.pinned) |
|
875 this.update(tab); |
|
876 }, |
|
877 |
|
878 // ---------- |
|
879 // Function: update |
|
880 // Takes in a xul:tab. |
|
881 update: function TabItems_update(tab) { |
|
882 try { |
|
883 Utils.assertThrow(tab, "tab"); |
|
884 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab"); |
|
885 Utils.assertThrow(tab._tabViewTabItem, "should already be linked"); |
|
886 |
|
887 let shouldDefer = ( |
|
888 this.isPaintingPaused() || |
|
889 this._tabsWaitingForUpdate.hasItems() || |
|
890 Date.now() - this._lastUpdateTime < this._heartbeatTiming |
|
891 ); |
|
892 |
|
893 if (shouldDefer) { |
|
894 this._tabsWaitingForUpdate.push(tab); |
|
895 this.startHeartbeat(); |
|
896 } else |
|
897 this._update(tab); |
|
898 } catch(e) { |
|
899 Utils.log(e); |
|
900 } |
|
901 }, |
|
902 |
|
903 // ---------- |
|
904 // Function: _update |
|
905 // Takes in a xul:tab. |
|
906 // |
|
907 // Parameters: |
|
908 // tab - a xul tab to update |
|
909 // options - an object with additional parameters, see below |
|
910 // |
|
911 // Possible options: |
|
912 // force - true to always update the tab item even if it's incomplete |
|
913 _update: function TabItems__update(tab, options) { |
|
914 try { |
|
915 if (this._pauseUpdateForTest) |
|
916 return; |
|
917 |
|
918 Utils.assertThrow(tab, "tab"); |
|
919 |
|
920 // ___ get the TabItem |
|
921 Utils.assertThrow(tab._tabViewTabItem, "must already be linked"); |
|
922 let tabItem = tab._tabViewTabItem; |
|
923 |
|
924 // Even if the page hasn't loaded, display the favicon and title |
|
925 // ___ icon |
|
926 FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) { |
|
927 let favImage = tabItem.$favImage[0]; |
|
928 let fav = tabItem.$fav; |
|
929 if (iconUrl) { |
|
930 if (favImage.src != iconUrl) |
|
931 favImage.src = iconUrl; |
|
932 fav.show(); |
|
933 } else { |
|
934 if (favImage.hasAttribute("src")) |
|
935 favImage.removeAttribute("src"); |
|
936 fav.hide(); |
|
937 } |
|
938 tabItem._sendToSubscribers("iconUpdated"); |
|
939 }); |
|
940 |
|
941 // ___ label |
|
942 let label = tab.label; |
|
943 let $name = tabItem.$tabTitle; |
|
944 if ($name.text() != label) |
|
945 $name.text(label); |
|
946 |
|
947 // ___ remove from waiting list now that we have no other |
|
948 // early returns |
|
949 this._tabsWaitingForUpdate.remove(tab); |
|
950 |
|
951 // ___ URL |
|
952 let tabUrl = tab.linkedBrowser.currentURI.spec; |
|
953 let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl); |
|
954 tabItem.$container.attr("title", tooltip); |
|
955 |
|
956 // ___ Make sure the tab is complete and ready for updating. |
|
957 if (options && options.force) { |
|
958 tabItem.updateCanvas(); |
|
959 tabItem._sendToSubscribers("updated"); |
|
960 } else { |
|
961 this._isComplete(tab, function TabItems__update_isComplete(isComplete) { |
|
962 if (!Utils.isValidXULTab(tab) || tab.pinned) |
|
963 return; |
|
964 |
|
965 if (isComplete) { |
|
966 tabItem.updateCanvas(); |
|
967 tabItem._sendToSubscribers("updated"); |
|
968 } else { |
|
969 this._tabsWaitingForUpdate.push(tab); |
|
970 } |
|
971 }.bind(this)); |
|
972 } |
|
973 } catch(e) { |
|
974 Utils.log(e); |
|
975 } |
|
976 }, |
|
977 |
|
978 // ---------- |
|
979 // Function: link |
|
980 // Takes in a xul:tab, creates a TabItem for it and adds it to the scene. |
|
981 link: function TabItems_link(tab, options) { |
|
982 try { |
|
983 Utils.assertThrow(tab, "tab"); |
|
984 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab"); |
|
985 Utils.assertThrow(!tab._tabViewTabItem, "shouldn't already be linked"); |
|
986 new TabItem(tab, options); // sets tab._tabViewTabItem to itself |
|
987 } catch(e) { |
|
988 Utils.log(e); |
|
989 } |
|
990 }, |
|
991 |
|
992 // ---------- |
|
993 // Function: unlink |
|
994 // Takes in a xul:tab and destroys the TabItem associated with it. |
|
995 unlink: function TabItems_unlink(tab) { |
|
996 try { |
|
997 Utils.assertThrow(tab, "tab"); |
|
998 Utils.assertThrow(tab._tabViewTabItem, "should already be linked"); |
|
999 // note that it's ok to unlink an app tab; see .handleTabUnpin |
|
1000 |
|
1001 this.unregister(tab._tabViewTabItem); |
|
1002 tab._tabViewTabItem._sendToSubscribers("close"); |
|
1003 tab._tabViewTabItem.$container.remove(); |
|
1004 tab._tabViewTabItem.removeTrenches(); |
|
1005 Items.unsquish(null, tab._tabViewTabItem); |
|
1006 |
|
1007 tab._tabViewTabItem.tab = null; |
|
1008 tab._tabViewTabItem.tabCanvas.tab = null; |
|
1009 tab._tabViewTabItem.tabCanvas = null; |
|
1010 tab._tabViewTabItem = null; |
|
1011 Storage.saveTab(tab, null); |
|
1012 |
|
1013 this._tabsWaitingForUpdate.remove(tab); |
|
1014 } catch(e) { |
|
1015 Utils.log(e); |
|
1016 } |
|
1017 }, |
|
1018 |
|
1019 // ---------- |
|
1020 // when a tab becomes pinned, destroy its TabItem |
|
1021 handleTabPin: function TabItems_handleTabPin(xulTab) { |
|
1022 this.unlink(xulTab); |
|
1023 }, |
|
1024 |
|
1025 // ---------- |
|
1026 // when a tab becomes unpinned, create a TabItem for it |
|
1027 handleTabUnpin: function TabItems_handleTabUnpin(xulTab) { |
|
1028 this.link(xulTab); |
|
1029 this.update(xulTab); |
|
1030 }, |
|
1031 |
|
1032 // ---------- |
|
1033 // Function: startHeartbeat |
|
1034 // Start a new heartbeat if there isn't one already started. |
|
1035 // The heartbeat is a chain of setTimeout calls that allows us to spread |
|
1036 // out update calls over a period of time. |
|
1037 // _heartbeat is used to make sure that we don't add multiple |
|
1038 // setTimeout chains. |
|
1039 startHeartbeat: function TabItems_startHeartbeat() { |
|
1040 if (!this._heartbeat) { |
|
1041 let self = this; |
|
1042 this._heartbeat = setTimeout(function() { |
|
1043 self._checkHeartbeat(); |
|
1044 }, this._heartbeatTiming); |
|
1045 } |
|
1046 }, |
|
1047 |
|
1048 // ---------- |
|
1049 // Function: _checkHeartbeat |
|
1050 // This periodically checks for tabs waiting to be updated, and calls |
|
1051 // _update on them. |
|
1052 // Should only be called by startHeartbeat and resumePainting. |
|
1053 _checkHeartbeat: function TabItems__checkHeartbeat() { |
|
1054 this._heartbeat = null; |
|
1055 |
|
1056 if (this.isPaintingPaused()) |
|
1057 return; |
|
1058 |
|
1059 // restart the heartbeat to update all waiting tabs once the UI becomes idle |
|
1060 if (!UI.isIdle()) { |
|
1061 this.startHeartbeat(); |
|
1062 return; |
|
1063 } |
|
1064 |
|
1065 let accumTime = 0; |
|
1066 let items = this._tabsWaitingForUpdate.getItems(); |
|
1067 // Do as many updates as we can fit into a "perceived" amount |
|
1068 // of time, which is tunable. |
|
1069 while (accumTime < this._maxTimeForUpdating && items.length) { |
|
1070 let updateBegin = Date.now(); |
|
1071 this._update(items.pop()); |
|
1072 let updateEnd = Date.now(); |
|
1073 |
|
1074 // Maintain a simple average of time for each tabitem update |
|
1075 // We can use this as a base by which to delay things like |
|
1076 // tab zooming, so there aren't any hitches. |
|
1077 let deltaTime = updateEnd - updateBegin; |
|
1078 accumTime += deltaTime; |
|
1079 } |
|
1080 |
|
1081 if (this._tabsWaitingForUpdate.hasItems()) |
|
1082 this.startHeartbeat(); |
|
1083 }, |
|
1084 |
|
1085 // ---------- |
|
1086 // Function: pausePainting |
|
1087 // Tells TabItems to stop updating thumbnails (so you can do |
|
1088 // animations without thumbnail paints causing stutters). |
|
1089 // pausePainting can be called multiple times, but every call to |
|
1090 // pausePainting needs to be mirrored with a call to <resumePainting>. |
|
1091 pausePainting: function TabItems_pausePainting() { |
|
1092 this.paintingPaused++; |
|
1093 if (this._heartbeat) { |
|
1094 clearTimeout(this._heartbeat); |
|
1095 this._heartbeat = null; |
|
1096 } |
|
1097 }, |
|
1098 |
|
1099 // ---------- |
|
1100 // Function: resumePainting |
|
1101 // Undoes a call to <pausePainting>. For instance, if you called |
|
1102 // pausePainting three times in a row, you'll need to call resumePainting |
|
1103 // three times before TabItems will start updating thumbnails again. |
|
1104 resumePainting: function TabItems_resumePainting() { |
|
1105 this.paintingPaused--; |
|
1106 Utils.assert(this.paintingPaused > -1, "paintingPaused should not go below zero"); |
|
1107 if (!this.isPaintingPaused()) |
|
1108 this.startHeartbeat(); |
|
1109 }, |
|
1110 |
|
1111 // ---------- |
|
1112 // Function: isPaintingPaused |
|
1113 // Returns a boolean indicating whether painting |
|
1114 // is paused or not. |
|
1115 isPaintingPaused: function TabItems_isPaintingPaused() { |
|
1116 return this.paintingPaused > 0; |
|
1117 }, |
|
1118 |
|
1119 // ---------- |
|
1120 // Function: pauseReconnecting |
|
1121 // Don't reconnect any new tabs until resume is called. |
|
1122 pauseReconnecting: function TabItems_pauseReconnecting() { |
|
1123 Utils.assertThrow(!this._reconnectingPaused, "shouldn't already be paused"); |
|
1124 |
|
1125 this._reconnectingPaused = true; |
|
1126 }, |
|
1127 |
|
1128 // ---------- |
|
1129 // Function: resumeReconnecting |
|
1130 // Reconnect all of the tabs that were created since we paused. |
|
1131 resumeReconnecting: function TabItems_resumeReconnecting() { |
|
1132 Utils.assertThrow(this._reconnectingPaused, "should already be paused"); |
|
1133 |
|
1134 this._reconnectingPaused = false; |
|
1135 this.items.forEach(function(item) { |
|
1136 if (!item._reconnected) |
|
1137 item._reconnect(); |
|
1138 }); |
|
1139 }, |
|
1140 |
|
1141 // ---------- |
|
1142 // Function: reconnectingPaused |
|
1143 // Returns true if reconnecting is paused. |
|
1144 reconnectingPaused: function TabItems_reconnectingPaused() { |
|
1145 return this._reconnectingPaused; |
|
1146 }, |
|
1147 |
|
1148 // ---------- |
|
1149 // Function: register |
|
1150 // Adds the given <TabItem> to the master list. |
|
1151 register: function TabItems_register(item) { |
|
1152 Utils.assert(item && item.isAnItem, 'item must be a TabItem'); |
|
1153 Utils.assert(this.items.indexOf(item) == -1, 'only register once per item'); |
|
1154 this.items.push(item); |
|
1155 }, |
|
1156 |
|
1157 // ---------- |
|
1158 // Function: unregister |
|
1159 // Removes the given <TabItem> from the master list. |
|
1160 unregister: function TabItems_unregister(item) { |
|
1161 var index = this.items.indexOf(item); |
|
1162 if (index != -1) |
|
1163 this.items.splice(index, 1); |
|
1164 }, |
|
1165 |
|
1166 // ---------- |
|
1167 // Function: getItems |
|
1168 // Returns a copy of the master array of <TabItem>s. |
|
1169 getItems: function TabItems_getItems() { |
|
1170 return Utils.copy(this.items); |
|
1171 }, |
|
1172 |
|
1173 // ---------- |
|
1174 // Function: saveAll |
|
1175 // Saves all open <TabItem>s. |
|
1176 saveAll: function TabItems_saveAll() { |
|
1177 let tabItems = this.getItems(); |
|
1178 |
|
1179 tabItems.forEach(function TabItems_saveAll_forEach(tabItem) { |
|
1180 tabItem.save(); |
|
1181 }); |
|
1182 }, |
|
1183 |
|
1184 // ---------- |
|
1185 // Function: storageSanity |
|
1186 // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage) |
|
1187 // and returns true if it looks valid. |
|
1188 // TODO: this is a stub, please implement |
|
1189 storageSanity: function TabItems_storageSanity(data) { |
|
1190 return true; |
|
1191 }, |
|
1192 |
|
1193 // ---------- |
|
1194 // Function: getFontSizeFromWidth |
|
1195 // Private method that returns the fontsize to use given the tab's width |
|
1196 getFontSizeFromWidth: function TabItem_getFontSizeFromWidth(width) { |
|
1197 let widthRange = new Range(0, TabItems.tabWidth); |
|
1198 let proportion = widthRange.proportion(width - TabItems.tabItemPadding.x, true); |
|
1199 // proportion is in [0,1] |
|
1200 return TabItems.fontSizeRange.scale(proportion); |
|
1201 }, |
|
1202 |
|
1203 // ---------- |
|
1204 // Function: _getWidthForHeight |
|
1205 // Private method that returns the tabitem width given a height. |
|
1206 _getWidthForHeight: function TabItems__getWidthForHeight(height) { |
|
1207 return height * TabItems.invTabAspect; |
|
1208 }, |
|
1209 |
|
1210 // ---------- |
|
1211 // Function: _getHeightForWidth |
|
1212 // Private method that returns the tabitem height given a width. |
|
1213 _getHeightForWidth: function TabItems__getHeightForWidth(width) { |
|
1214 return width * TabItems.tabAspect; |
|
1215 }, |
|
1216 |
|
1217 // ---------- |
|
1218 // Function: calcValidSize |
|
1219 // Pass in a desired size, and receive a size based on proper title |
|
1220 // size and aspect ratio. |
|
1221 calcValidSize: function TabItems_calcValidSize(size, options) { |
|
1222 Utils.assert(Utils.isPoint(size), 'input is a Point'); |
|
1223 |
|
1224 let width = Math.max(TabItems.minTabWidth, size.x); |
|
1225 let showTitle = !options || !options.hideTitle; |
|
1226 let titleSize = showTitle ? TabItems.fontSizeRange.max : 0; |
|
1227 let height = Math.max(TabItems.minTabHeight, size.y - titleSize); |
|
1228 let retSize = new Point(width, height); |
|
1229 |
|
1230 if (size.x > -1) |
|
1231 retSize.y = this._getHeightForWidth(width); |
|
1232 if (size.y > -1) |
|
1233 retSize.x = this._getWidthForHeight(height); |
|
1234 |
|
1235 if (size.x > -1 && size.y > -1) { |
|
1236 if (retSize.x < size.x) |
|
1237 retSize.y = this._getHeightForWidth(retSize.x); |
|
1238 else |
|
1239 retSize.x = this._getWidthForHeight(retSize.y); |
|
1240 } |
|
1241 |
|
1242 if (showTitle) |
|
1243 retSize.y += titleSize; |
|
1244 |
|
1245 return retSize; |
|
1246 } |
|
1247 }; |
|
1248 |
|
1249 // ########## |
|
1250 // Class: TabPriorityQueue |
|
1251 // Container that returns tab items in a priority order |
|
1252 // Current implementation assigns tab to either a high priority |
|
1253 // or low priority queue, and toggles which queue items are popped |
|
1254 // from. This guarantees that high priority items which are constantly |
|
1255 // being added will not eclipse changes for lower priority items. |
|
1256 function TabPriorityQueue() { |
|
1257 }; |
|
1258 |
|
1259 TabPriorityQueue.prototype = { |
|
1260 _low: [], // low priority queue |
|
1261 _high: [], // high priority queue |
|
1262 |
|
1263 // ---------- |
|
1264 // Function: toString |
|
1265 // Prints [TabPriorityQueue count=count] for debug use |
|
1266 toString: function TabPriorityQueue_toString() { |
|
1267 return "[TabPriorityQueue count=" + (this._low.length + this._high.length) + "]"; |
|
1268 }, |
|
1269 |
|
1270 // ---------- |
|
1271 // Function: clear |
|
1272 // Empty the update queue |
|
1273 clear: function TabPriorityQueue_clear() { |
|
1274 this._low = []; |
|
1275 this._high = []; |
|
1276 }, |
|
1277 |
|
1278 // ---------- |
|
1279 // Function: hasItems |
|
1280 // Return whether pending items exist |
|
1281 hasItems: function TabPriorityQueue_hasItems() { |
|
1282 return (this._low.length > 0) || (this._high.length > 0); |
|
1283 }, |
|
1284 |
|
1285 // ---------- |
|
1286 // Function: getItems |
|
1287 // Returns all queued items, ordered from low to high priority |
|
1288 getItems: function TabPriorityQueue_getItems() { |
|
1289 return this._low.concat(this._high); |
|
1290 }, |
|
1291 |
|
1292 // ---------- |
|
1293 // Function: push |
|
1294 // Add an item to be prioritized |
|
1295 push: function TabPriorityQueue_push(tab) { |
|
1296 // Push onto correct priority queue. |
|
1297 // It's only low priority if it's in a stack, and isn't the top, |
|
1298 // and the stack isn't expanded. |
|
1299 // If it already exists in the destination queue, |
|
1300 // leave it. If it exists in a different queue, remove it first and push |
|
1301 // onto new queue. |
|
1302 let item = tab._tabViewTabItem; |
|
1303 if (item.parent && (item.parent.isStacked() && |
|
1304 !item.parent.isTopOfStack(item) && |
|
1305 !item.parent.expanded)) { |
|
1306 let idx = this._high.indexOf(tab); |
|
1307 if (idx != -1) { |
|
1308 this._high.splice(idx, 1); |
|
1309 this._low.unshift(tab); |
|
1310 } else if (this._low.indexOf(tab) == -1) |
|
1311 this._low.unshift(tab); |
|
1312 } else { |
|
1313 let idx = this._low.indexOf(tab); |
|
1314 if (idx != -1) { |
|
1315 this._low.splice(idx, 1); |
|
1316 this._high.unshift(tab); |
|
1317 } else if (this._high.indexOf(tab) == -1) |
|
1318 this._high.unshift(tab); |
|
1319 } |
|
1320 }, |
|
1321 |
|
1322 // ---------- |
|
1323 // Function: pop |
|
1324 // Remove and return the next item in priority order |
|
1325 pop: function TabPriorityQueue_pop() { |
|
1326 let ret = null; |
|
1327 if (this._high.length) |
|
1328 ret = this._high.pop(); |
|
1329 else if (this._low.length) |
|
1330 ret = this._low.pop(); |
|
1331 return ret; |
|
1332 }, |
|
1333 |
|
1334 // ---------- |
|
1335 // Function: peek |
|
1336 // Return the next item in priority order, without removing it |
|
1337 peek: function TabPriorityQueue_peek() { |
|
1338 let ret = null; |
|
1339 if (this._high.length) |
|
1340 ret = this._high[this._high.length-1]; |
|
1341 else if (this._low.length) |
|
1342 ret = this._low[this._low.length-1]; |
|
1343 return ret; |
|
1344 }, |
|
1345 |
|
1346 // ---------- |
|
1347 // Function: remove |
|
1348 // Remove the passed item |
|
1349 remove: function TabPriorityQueue_remove(tab) { |
|
1350 let index = this._high.indexOf(tab); |
|
1351 if (index != -1) |
|
1352 this._high.splice(index, 1); |
|
1353 else { |
|
1354 index = this._low.indexOf(tab); |
|
1355 if (index != -1) |
|
1356 this._low.splice(index, 1); |
|
1357 } |
|
1358 } |
|
1359 }; |
|
1360 |
|
1361 // ########## |
|
1362 // Class: TabCanvas |
|
1363 // Takes care of the actual canvas for the tab thumbnail |
|
1364 // Does not need to be accessed from outside of tabitems.js |
|
1365 function TabCanvas(tab, canvas) { |
|
1366 this.tab = tab; |
|
1367 this.canvas = canvas; |
|
1368 }; |
|
1369 |
|
1370 TabCanvas.prototype = Utils.extend(new Subscribable(), { |
|
1371 // ---------- |
|
1372 // Function: toString |
|
1373 // Prints [TabCanvas (tab)] for debug use |
|
1374 toString: function TabCanvas_toString() { |
|
1375 return "[TabCanvas (" + this.tab + ")]"; |
|
1376 }, |
|
1377 |
|
1378 // ---------- |
|
1379 // Function: paint |
|
1380 paint: function TabCanvas_paint(evt) { |
|
1381 var w = this.canvas.width; |
|
1382 var h = this.canvas.height; |
|
1383 if (!w || !h) |
|
1384 return; |
|
1385 |
|
1386 if (!this.tab.linkedBrowser.contentWindow) { |
|
1387 Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()'); |
|
1388 return; |
|
1389 } |
|
1390 |
|
1391 let win = this.tab.linkedBrowser.contentWindow; |
|
1392 gPageThumbnails.captureToCanvas(win, this.canvas); |
|
1393 |
|
1394 this._sendToSubscribers("painted"); |
|
1395 }, |
|
1396 |
|
1397 // ---------- |
|
1398 // Function: toImageData |
|
1399 toImageData: function TabCanvas_toImageData() { |
|
1400 return this.canvas.toDataURL("image/png"); |
|
1401 } |
|
1402 }); |