browser/components/tabview/tabitems.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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.
    11 //
    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));
    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   }
    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);
   111 };
   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;
   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   }
   695 });
   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;
   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);
  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);
  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;
  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;
  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;
  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   },
  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   },
  1141   // ----------
  1142   // Function: reconnectingPaused
  1143   // Returns true if reconnecting is paused.
  1144   reconnectingPaused: function TabItems_reconnectingPaused() {
  1145     return this._reconnectingPaused;
  1146   },
  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);
  1242     if (showTitle)
  1243       retSize.y += titleSize;
  1245     return retSize;
  1247 };
  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 };
  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);
  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);
  1359 };
  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 };
  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;
  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");
  1402 });

mercurial