Bug 1031527 - Remove dup fd from ParamTraits<MagicGrallocBufferHandle>::Read(). r...
[gecko.git] / browser / components / tabview / tabitems.js
blob983fab082a1cd4bdc87753122383f5ae198fe860
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/. */
5 // **********
6 // Title: tabitems.js
8 // ##########
9 // Class: TabItem
10 // An <Item> that represents a tab. Also implements the <Subscribable> interface.
12 // Parameters:
13 //   tab - a xul:tab
14 function TabItem(tab, options) {
15   Utils.assert(tab, "tab");
17   this.tab = tab;
18   // register this as the tab's tabItem
19   this.tab._tabViewTabItem = this;
21   if (!options)
22     options = {};
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);
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);
42   this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
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;
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'));
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);
64   this._lastTabUpdateTime = Date.now();
66   // ___ superclass setup
67   this._init(div);
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   };
76   this.draggable();
78   let self = this;
80   // ___ more div setup
81   $div.mousedown(function(e) {
82     if (!Utils.isRightClick(e))
83       self.lastMouseDownTarget = e.target;
84   });
86   $div.mouseup(function(e) {
87     var same = (e.target == self.lastMouseDownTarget);
88     self.lastMouseDownTarget = null;
89     if (!same)
90       return;
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   });
102   this.droppable(true);
104   this.$close.attr("title", tabbrowserString("tabs.closeTab"));
106   TabItems.register(this);
108   // ___ reconnect to data from Storage
109   if (!TabItems.reconnectingPaused())
110     this._reconnect(options);
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   },
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   },
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   },
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   },
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);
158     this.$cachedThumb.attr("src", thumbnailURL).show();
159     this.$canvas.css({opacity: 0});
161     let tooltip = (title && title != url ? title + "\n" + url : url);
162     this.$tabTitle.text(title).attr("title", tooltip);
163     this._showsCachedData = true;
164   },
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   },
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;
185     return data;
186   },
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;
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   },
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);
210     if (tabState) {
211       let index = (tabState.index || tabState.entries.length) - 1;
212       if (index in tabState.entries)
213         return tabState.entries[index];
214     }
216     return null;
217   },
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 = "";
228     if (entry) {
229       if (entry.title)
230         title = entry.title;
232       url = entry.url;
233     } else {
234       url = this.tab.linkedBrowser.currentURI.spec;
235     }
237     return {title: title, url: url};
238   },
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");
255     let tabData = Storage.getTabData(this.tab);
256     let groupItem;
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();
263       if (this.parent)
264         this.parent.remove(this, {immediately: true});
266       if (tabData.groupID)
267         groupItem = GroupItems.groupItem(tabData.groupID);
268       else
269         groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds});
271       if (groupItem) {
272         groupItem.add(this, {immediately: true});
274         // restore the active tab for each group between browser sessions
275         if (tabData.active)
276           groupItem.setActiveTab(this);
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);
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     }
296     this._reconnected = true;
297     this.save();
298     this._sendToSubscribers("reconnected");
299   },
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   },
312   // ----------
313   // Function: getHidden
314   // Return hide state of item
315   getHidden: function TabItem_getHidden() {
316     return this._hidden;
317   },
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!');
333     if (!options)
334       options = {};
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);
343     var css = {};
345     if (rect.left != this.bounds.left || options.force)
346       css.left = rect.left;
348     if (rect.top != this.bounds.top || options.force)
349       css.top = rect.top;
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     }
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     }
363     if (Utils.isEmptyObject(css))
364       return;
366     this.bounds.copy(rect);
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     }
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     }
391     if (css.width) {
392       TabItems.update(this.tab);
394       let widthRange, proportion;
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       }
414       if (proportion <= .1)
415         this.$close.hide();
416       else
417         this.$close.show().css({opacity:proportion});
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     }
430     this._hasBeenDrawn = true;
432     UI.clearShouldResizeItems();
434     rect = this.getBounds(); // ensure that it's a <Rect>
436     Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!');
438     if (!this.parent && Utils.isValidXULTab(this.tab))
439       this.setTrenches(rect);
441     this.save();
442   },
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   },
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     }
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;
474     if (tabClosed)
475       this._sendToSubscribers("tabRemoved");
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   },
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   },
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   },
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");
502     if (this.parent)
503       this.parent.setActiveTab(this);
504   },
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   },
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;
524     let self = this;
525     let $tabEl = this.$container;
526     let $canvas = this.$canvas;
528     Search.hide();
530     UI.setActive(this);
531     TabItems._update(this.tab, {force: true});
533     // Zoom in!
534     let tab = this.tab;
536     function onZoomDone() {
537       $canvas.css({ 'transform': null });
538       $tabEl.removeClass("front");
540       UI.goToTab(tab);
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();
553       self._sendToSubscribers("zoomedIn");
554     }
556     let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
557     if (animateZoom) {
558       let transform = this.getZoomTransform();
559       TabItems.pausePainting();
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();
572             setTimeout(function() {
573               TabItems.resumePainting();
574             }, 0);
575           }
576         });
577     } else {
578       setTimeout(onZoomDone, 0);
579     }
580   },
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);
597       if (typeof complete == "function")
598         complete();
599     };
601     UI.setActive(this);
602     TabItems._update(this.tab, {force: true});
604     $tab.addClass("front");
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();
613       $canvas.css({
614         'transform': transform.transform,
615         'transform-origin': transform.transformOrigin
616       });
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   },
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();
640     let { innerWidth: windowWidth, innerHeight: windowHeight } = window;
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.
651     if (!scaleCheat)
652       scaleCheat = 1.7;
654     let zoomWidth = width + (window.innerWidth - width) / scaleCheat;
655     let zoomScaleFactor = zoomWidth / width;
657     let zoomHeight = height * zoomScaleFactor;
658     let zoomTop = top * (1 - 1/scaleCheat);
659     let zoomLeft = left * (1 - 1/scaleCheat);
661     let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100;
662     let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100;
664     return {
665       transformOrigin: xOrigin + "% " + yOrigin + "%",
666       transform: "scale(" + zoomScaleFactor + ")"
667     };
668   },
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     }
685     TabItems._lastUpdateTime = Date.now();
686     this._lastTabUpdateTime = TabItems._lastUpdateTime;
688     if (this.tabCanvas)
689       this.tabCanvas.paint();
691     // ___ cache
692     if (this.isShowingCachedData())
693       this.hideCachedData();
694   }
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,
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   },
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;
742     let $canvas = iQ("<canvas>")
743       .attr('moz-opaque', '');
744     $canvas.appendTo(iQ("body"));
745     $canvas.hide();
747     let mm = gWindow.messageManager;
748     this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this);
749     mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
751     // When a tab is opened, create the TabItem
752     this._eventListeners.open = function (event) {
753       let tab = event.target;
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;
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;
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     }
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;
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   },
797   // ----------
798   // Function: uninit
799   uninit: function TabItems_uninit() {
800     let mm = gWindow.messageManager;
801     mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
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;
809       for (let x in tabItem) {
810         if (typeof tabItem[x] == "object")
811           tabItem[x] = null;
812       }
813     });
815     this.items = null;
816     this._eventListeners = null;
817     this._lastUpdateTime = null;
818     this._tabsWaitingForUpdate.clear();
819   },
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;
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'>&nbsp;</span>" +
836             "<div class='close'></div>";
837     this._fragment = document.createDocumentFragment();
838     this._fragment.appendChild(div);
840     return this._fragment;
841   },
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");
849     // A pending tab can't be complete, yet.
850     if (tab.hasAttribute("pending")) {
851       setTimeout(() => callback(false));
852       return;
853     }
855     let mm = tab.linkedBrowser.messageManager;
856     let message = "Panorama:isDocumentLoaded";
858     mm.addMessageListener(message, function onMessage(cx) {
859       mm.removeMessageListener(cx.name, onMessage);
860       callback(cx.json.isLoaded);
861     });
862     mm.sendAsyncMessage(message);
863   },
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;
873     let tab = gBrowser.tabs[index];
874     if (!tab.pinned)
875       this.update(tab);
876   },
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");
887       let shouldDefer = (
888         this.isPaintingPaused() ||
889         this._tabsWaitingForUpdate.hasItems() ||
890         Date.now() - this._lastUpdateTime < this._heartbeatTiming
891       );
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   },
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;
918       Utils.assertThrow(tab, "tab");
920       // ___ get the TabItem
921       Utils.assertThrow(tab._tabViewTabItem, "must already be linked");
922       let tabItem = tab._tabViewTabItem;
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       });
941       // ___ label
942       let label = tab.label;
943       let $name = tabItem.$tabTitle;
944       if ($name.text() != label)
945         $name.text(label);
947       // ___ remove from waiting list now that we have no other
948       // early returns
949       this._tabsWaitingForUpdate.remove(tab);
951       // ___ URL
952       let tabUrl = tab.linkedBrowser.currentURI.spec;
953       let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl);
954       tabItem.$container.attr("title", tooltip);
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;
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   },
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   },
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
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);
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);
1013       this._tabsWaitingForUpdate.remove(tab);
1014     } catch(e) {
1015       Utils.log(e);
1016     }
1017   },
1019   // ----------
1020   // when a tab becomes pinned, destroy its TabItem
1021   handleTabPin: function TabItems_handleTabPin(xulTab) {
1022     this.unlink(xulTab);
1023   },
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   },
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   },
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;
1056     if (this.isPaintingPaused())
1057       return;
1059     // restart the heartbeat to update all waiting tabs once the UI becomes idle
1060     if (!UI.isIdle()) {
1061       this.startHeartbeat();
1062       return;
1063     }
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();
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     }
1081     if (this._tabsWaitingForUpdate.hasItems())
1082       this.startHeartbeat();
1083   },
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   },
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   },
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   },
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");
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");
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   },
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   },
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   },
1173   // ----------
1174   // Function: saveAll
1175   // Saves all open <TabItem>s.
1176   saveAll: function TabItems_saveAll() {
1177     let tabItems = this.getItems();
1179     tabItems.forEach(function TabItems_saveAll_forEach(tabItem) {
1180       tabItem.save();
1181     });
1182   },
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   },
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   },
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   },
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   },
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');
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);
1230     if (size.x > -1)
1231       retSize.y = this._getHeightForWidth(width);
1232     if (size.y > -1)
1233       retSize.x = this._getWidthForHeight(height);
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     }
1242     if (showTitle)
1243       retSize.y += titleSize;
1245     return retSize;
1246   }
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() {
1259 TabPriorityQueue.prototype = {
1260   _low: [], // low priority queue
1261   _high: [], // high priority queue
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   },
1270   // ----------
1271   // Function: clear
1272   // Empty the update queue
1273   clear: function TabPriorityQueue_clear() {
1274     this._low = [];
1275     this._high = [];
1276   },
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   },
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   },
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   },
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   },
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   },
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   }
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;
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   },
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;
1386     if (!this.tab.linkedBrowser.contentWindow) {
1387       Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
1388       return;
1389     }
1391     let win = this.tab.linkedBrowser.contentWindow;
1392     gPageThumbnails.captureToCanvas(win, this.canvas);
1394     this._sendToSubscribers("painted");
1395   },
1397   // ----------
1398   // Function: toImageData
1399   toImageData: function TabCanvas_toImageData() {
1400     return this.canvas.toDataURL("image/png");
1401   }