browser/components/tabview/tabitems.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/tabview/tabitems.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1402 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +// **********
     1.9 +// Title: tabitems.js
    1.10 +
    1.11 +// ##########
    1.12 +// Class: TabItem
    1.13 +// An <Item> that represents a tab. Also implements the <Subscribable> interface.
    1.14 +//
    1.15 +// Parameters:
    1.16 +//   tab - a xul:tab
    1.17 +function TabItem(tab, options) {
    1.18 +  Utils.assert(tab, "tab");
    1.19 +
    1.20 +  this.tab = tab;
    1.21 +  // register this as the tab's tabItem
    1.22 +  this.tab._tabViewTabItem = this;
    1.23 +
    1.24 +  if (!options)
    1.25 +    options = {};
    1.26 +
    1.27 +  // ___ set up div
    1.28 +  document.body.appendChild(TabItems.fragment().cloneNode(true));
    1.29 +  
    1.30 +  // The document fragment contains just one Node
    1.31 +  // As per DOM3 appendChild: it will then be the last child
    1.32 +  let div = document.body.lastChild;
    1.33 +  let $div = iQ(div);
    1.34 +
    1.35 +  this._showsCachedData = false;
    1.36 +  this.canvasSizeForced = false;
    1.37 +  this.$thumb = iQ('.thumb', $div);
    1.38 +  this.$fav   = iQ('.favicon', $div);
    1.39 +  this.$tabTitle = iQ('.tab-title', $div);
    1.40 +  this.$canvas = iQ('.thumb canvas', $div);
    1.41 +  this.$cachedThumb = iQ('img.cached-thumb', $div);
    1.42 +  this.$favImage = iQ('.favicon>img', $div);
    1.43 +  this.$close = iQ('.close', $div);
    1.44 +
    1.45 +  this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
    1.46 +
    1.47 +  this._hidden = false;
    1.48 +  this.isATabItem = true;
    1.49 +  this.keepProportional = true;
    1.50 +  this._hasBeenDrawn = false;
    1.51 +  this._reconnected = false;
    1.52 +  this.isDragging = false;
    1.53 +  this.isStacked = false;
    1.54 +
    1.55 +  // Read off the total vertical and horizontal padding on the tab container
    1.56 +  // and cache this value, as it must be the same for every TabItem.
    1.57 +  if (Utils.isEmptyObject(TabItems.tabItemPadding)) {
    1.58 +    TabItems.tabItemPadding.x = parseInt($div.css('padding-left'))
    1.59 +        + parseInt($div.css('padding-right'));
    1.60 +
    1.61 +    TabItems.tabItemPadding.y = parseInt($div.css('padding-top'))
    1.62 +        + parseInt($div.css('padding-bottom'));
    1.63 +  }
    1.64 +  
    1.65 +  this.bounds = new Rect(0,0,1,1);
    1.66 +
    1.67 +  this._lastTabUpdateTime = Date.now();
    1.68 +
    1.69 +  // ___ superclass setup
    1.70 +  this._init(div);
    1.71 +
    1.72 +  // ___ drag/drop
    1.73 +  // override dropOptions with custom tabitem methods
    1.74 +  this.dropOptions.drop = function(e) {
    1.75 +    let groupItem = drag.info.item.parent;
    1.76 +    groupItem.add(drag.info.$el);
    1.77 +  };
    1.78 +
    1.79 +  this.draggable();
    1.80 +
    1.81 +  let self = this;
    1.82 +
    1.83 +  // ___ more div setup
    1.84 +  $div.mousedown(function(e) {
    1.85 +    if (!Utils.isRightClick(e))
    1.86 +      self.lastMouseDownTarget = e.target;
    1.87 +  });
    1.88 +
    1.89 +  $div.mouseup(function(e) {
    1.90 +    var same = (e.target == self.lastMouseDownTarget);
    1.91 +    self.lastMouseDownTarget = null;
    1.92 +    if (!same)
    1.93 +      return;
    1.94 +
    1.95 +    // press close button or middle mouse click
    1.96 +    if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) {
    1.97 +      self.closedManually = true;
    1.98 +      self.close();
    1.99 +    } else {
   1.100 +      if (!Items.item(this).isDragging)
   1.101 +        self.zoomIn();
   1.102 +    }
   1.103 +  });
   1.104 +
   1.105 +  this.droppable(true);
   1.106 +
   1.107 +  this.$close.attr("title", tabbrowserString("tabs.closeTab"));
   1.108 +
   1.109 +  TabItems.register(this);
   1.110 +
   1.111 +  // ___ reconnect to data from Storage
   1.112 +  if (!TabItems.reconnectingPaused())
   1.113 +    this._reconnect(options);
   1.114 +};
   1.115 +
   1.116 +TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
   1.117 +  // ----------
   1.118 +  // Function: toString
   1.119 +  // Prints [TabItem (tab)] for debug use
   1.120 +  toString: function TabItem_toString() {
   1.121 +    return "[TabItem (" + this.tab + ")]";
   1.122 +  },
   1.123 +
   1.124 +  // ----------
   1.125 +  // Function: forceCanvasSize
   1.126 +  // Repaints the thumbnail with the given resolution, and forces it
   1.127 +  // to stay that resolution until unforceCanvasSize is called.
   1.128 +  forceCanvasSize: function TabItem_forceCanvasSize(w, h) {
   1.129 +    this.canvasSizeForced = true;
   1.130 +    this.$canvas[0].width = w;
   1.131 +    this.$canvas[0].height = h;
   1.132 +    this.tabCanvas.paint();
   1.133 +  },
   1.134 +
   1.135 +  // ----------
   1.136 +  // Function: unforceCanvasSize
   1.137 +  // Stops holding the thumbnail resolution; allows it to shift to the
   1.138 +  // size of thumbnail on screen. Note that this call does not nest, unlike
   1.139 +  // <TabItems.resumePainting>; if you call forceCanvasSize multiple
   1.140 +  // times, you just need a single unforce to clear them all.
   1.141 +  unforceCanvasSize: function TabItem_unforceCanvasSize() {
   1.142 +    this.canvasSizeForced = false;
   1.143 +  },
   1.144 +
   1.145 +  // ----------
   1.146 +  // Function: isShowingCachedData
   1.147 +  // Returns a boolean indicates whether the cached data is being displayed or
   1.148 +  // not. 
   1.149 +  isShowingCachedData: function TabItem_isShowingCachedData() {
   1.150 +    return this._showsCachedData;
   1.151 +  },
   1.152 +
   1.153 +  // ----------
   1.154 +  // Function: showCachedData
   1.155 +  // Shows the cached data i.e. image and title.  Note: this method should only
   1.156 +  // be called at browser startup with the cached data avaliable.
   1.157 +  showCachedData: function TabItem_showCachedData() {
   1.158 +    let {title, url} = this.getTabState();
   1.159 +    let thumbnailURL = gPageThumbnails.getThumbnailURL(url);
   1.160 +
   1.161 +    this.$cachedThumb.attr("src", thumbnailURL).show();
   1.162 +    this.$canvas.css({opacity: 0});
   1.163 +
   1.164 +    let tooltip = (title && title != url ? title + "\n" + url : url);
   1.165 +    this.$tabTitle.text(title).attr("title", tooltip);
   1.166 +    this._showsCachedData = true;
   1.167 +  },
   1.168 +
   1.169 +  // ----------
   1.170 +  // Function: hideCachedData
   1.171 +  // Hides the cached data i.e. image and title and show the canvas.
   1.172 +  hideCachedData: function TabItem_hideCachedData() {
   1.173 +    this.$cachedThumb.attr("src", "").hide();
   1.174 +    this.$canvas.css({opacity: 1.0});
   1.175 +    this._showsCachedData = false;
   1.176 +  },
   1.177 +
   1.178 +  // ----------
   1.179 +  // Function: getStorageData
   1.180 +  // Get data to be used for persistent storage of this object.
   1.181 +  getStorageData: function TabItem_getStorageData() {
   1.182 +    let data = {
   1.183 +      groupID: (this.parent ? this.parent.id : 0)
   1.184 +    };
   1.185 +    if (this.parent && this.parent.getActiveTab() == this)
   1.186 +      data.active = true;
   1.187 +
   1.188 +    return data;
   1.189 +  },
   1.190 +
   1.191 +  // ----------
   1.192 +  // Function: save
   1.193 +  // Store persistent for this object.
   1.194 +  save: function TabItem_save() {
   1.195 +    try {
   1.196 +      if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save
   1.197 +        return;
   1.198 +
   1.199 +      let data = this.getStorageData();
   1.200 +      if (TabItems.storageSanity(data))
   1.201 +        Storage.saveTab(this.tab, data);
   1.202 +    } catch(e) {
   1.203 +      Utils.log("Error in saving tab value: "+e);
   1.204 +    }
   1.205 +  },
   1.206 +
   1.207 +  // ----------
   1.208 +  // Function: _getCurrentTabStateEntry
   1.209 +  // Returns the current tab state's active history entry.
   1.210 +  _getCurrentTabStateEntry: function TabItem__getCurrentTabStateEntry() {
   1.211 +    let tabState = Storage.getTabState(this.tab);
   1.212 +
   1.213 +    if (tabState) {
   1.214 +      let index = (tabState.index || tabState.entries.length) - 1;
   1.215 +      if (index in tabState.entries)
   1.216 +        return tabState.entries[index];
   1.217 +    }
   1.218 +
   1.219 +    return null;
   1.220 +  },
   1.221 +
   1.222 +  // ----------
   1.223 +  // Function: getTabState
   1.224 +  // Returns the current tab state, i.e. the title and URL of the active
   1.225 +  // history entry.
   1.226 +  getTabState: function TabItem_getTabState() {
   1.227 +    let entry = this._getCurrentTabStateEntry();
   1.228 +    let title = "";
   1.229 +    let url = "";
   1.230 +
   1.231 +    if (entry) {
   1.232 +      if (entry.title)
   1.233 +        title = entry.title;
   1.234 +
   1.235 +      url = entry.url;
   1.236 +    } else {
   1.237 +      url = this.tab.linkedBrowser.currentURI.spec;
   1.238 +    }
   1.239 +
   1.240 +    return {title: title, url: url};
   1.241 +  },
   1.242 +
   1.243 +  // ----------
   1.244 +  // Function: _reconnect
   1.245 +  // Load the reciever's persistent data from storage. If there is none, 
   1.246 +  // treats it as a new tab. 
   1.247 +  //
   1.248 +  // Parameters:
   1.249 +  //   options - an object with additional parameters, see below
   1.250 +  //
   1.251 +  // Possible options:
   1.252 +  //   groupItemId - if the tab doesn't have any data associated with it and
   1.253 +  //                 groupItemId is available, add the tab to that group.
   1.254 +  _reconnect: function TabItem__reconnect(options) {
   1.255 +    Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
   1.256 +    Utils.assertThrow(this.tab, "should have a xul:tab");
   1.257 +
   1.258 +    let tabData = Storage.getTabData(this.tab);
   1.259 +    let groupItem;
   1.260 +
   1.261 +    if (tabData && TabItems.storageSanity(tabData)) {
   1.262 +      // Show the cached data while we're waiting for the tabItem to be updated.
   1.263 +      // If the tab isn't restored yet this acts as a placeholder until it is.
   1.264 +      this.showCachedData();
   1.265 +
   1.266 +      if (this.parent)
   1.267 +        this.parent.remove(this, {immediately: true});
   1.268 +
   1.269 +      if (tabData.groupID)
   1.270 +        groupItem = GroupItems.groupItem(tabData.groupID);
   1.271 +      else
   1.272 +        groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds});
   1.273 +
   1.274 +      if (groupItem) {
   1.275 +        groupItem.add(this, {immediately: true});
   1.276 +
   1.277 +        // restore the active tab for each group between browser sessions
   1.278 +        if (tabData.active)
   1.279 +          groupItem.setActiveTab(this);
   1.280 +
   1.281 +        // if it matches the selected tab or no active tab and the browser
   1.282 +        // tab is hidden, the active group item would be set.
   1.283 +        if (this.tab.selected ||
   1.284 +            (!GroupItems.getActiveGroupItem() && !this.tab.hidden))
   1.285 +          UI.setActive(this.parent);
   1.286 +      }
   1.287 +    } else {
   1.288 +      if (options && options.groupItemId)
   1.289 +        groupItem = GroupItems.groupItem(options.groupItemId);
   1.290 +
   1.291 +      if (groupItem) {
   1.292 +        groupItem.add(this, {immediately: true});
   1.293 +      } else {
   1.294 +        // create tab group by double click is handled in UI_init().
   1.295 +        GroupItems.newTab(this, {immediately: true});
   1.296 +      }
   1.297 +    }
   1.298 +
   1.299 +    this._reconnected = true;
   1.300 +    this.save();
   1.301 +    this._sendToSubscribers("reconnected");
   1.302 +  },
   1.303 +
   1.304 +  // ----------
   1.305 +  // Function: setHidden
   1.306 +  // Hide/unhide this item
   1.307 +  setHidden: function TabItem_setHidden(val) {
   1.308 +    if (val)
   1.309 +      this.addClass("tabHidden");
   1.310 +    else
   1.311 +      this.removeClass("tabHidden");
   1.312 +    this._hidden = val;
   1.313 +  },
   1.314 +
   1.315 +  // ----------
   1.316 +  // Function: getHidden
   1.317 +  // Return hide state of item
   1.318 +  getHidden: function TabItem_getHidden() {
   1.319 +    return this._hidden;
   1.320 +  },
   1.321 +
   1.322 +  // ----------
   1.323 +  // Function: setBounds
   1.324 +  // Moves this item to the specified location and size.
   1.325 +  //
   1.326 +  // Parameters:
   1.327 +  //   rect - a <Rect> giving the new bounds
   1.328 +  //   immediately - true if it should not animate; default false
   1.329 +  //   options - an object with additional parameters, see below
   1.330 +  //
   1.331 +  // Possible options:
   1.332 +  //   force - true to always update the DOM even if the bounds haven't changed; default false
   1.333 +  setBounds: function TabItem_setBounds(inRect, immediately, options) {
   1.334 +    Utils.assert(Utils.isRect(inRect), 'TabItem.setBounds: rect is not a real rectangle!');
   1.335 +
   1.336 +    if (!options)
   1.337 +      options = {};
   1.338 +
   1.339 +    // force the input size to be valid
   1.340 +    let validSize = TabItems.calcValidSize(
   1.341 +      new Point(inRect.width, inRect.height), 
   1.342 +      {hideTitle: (this.isStacked || options.hideTitle === true)});
   1.343 +    let rect = new Rect(inRect.left, inRect.top, 
   1.344 +      validSize.x, validSize.y);
   1.345 +
   1.346 +    var css = {};
   1.347 +
   1.348 +    if (rect.left != this.bounds.left || options.force)
   1.349 +      css.left = rect.left;
   1.350 +
   1.351 +    if (rect.top != this.bounds.top || options.force)
   1.352 +      css.top = rect.top;
   1.353 +
   1.354 +    if (rect.width != this.bounds.width || options.force) {
   1.355 +      css.width = rect.width - TabItems.tabItemPadding.x;
   1.356 +      css.fontSize = TabItems.getFontSizeFromWidth(rect.width);
   1.357 +      css.fontSize += 'px';
   1.358 +    }
   1.359 +
   1.360 +    if (rect.height != this.bounds.height || options.force) {
   1.361 +      css.height = rect.height - TabItems.tabItemPadding.y;
   1.362 +      if (!this.isStacked)
   1.363 +        css.height -= TabItems.fontSizeRange.max;
   1.364 +    }
   1.365 +
   1.366 +    if (Utils.isEmptyObject(css))
   1.367 +      return;
   1.368 +
   1.369 +    this.bounds.copy(rect);
   1.370 +
   1.371 +    // If this is a brand new tab don't animate it in from
   1.372 +    // a random location (i.e., from [0,0]). Instead, just
   1.373 +    // have it appear where it should be.
   1.374 +    if (immediately || (!this._hasBeenDrawn)) {
   1.375 +      this.$container.css(css);
   1.376 +    } else {
   1.377 +      TabItems.pausePainting();
   1.378 +      this.$container.animate(css, {
   1.379 +          duration: 200,
   1.380 +        easing: "tabviewBounce",
   1.381 +        complete: function() {
   1.382 +          TabItems.resumePainting();
   1.383 +        }
   1.384 +      });
   1.385 +    }
   1.386 +
   1.387 +    if (css.fontSize && !(this.parent && this.parent.isStacked())) {
   1.388 +      if (css.fontSize < TabItems.fontSizeRange.min)
   1.389 +        immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut();
   1.390 +      else
   1.391 +        immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn();
   1.392 +    }
   1.393 +
   1.394 +    if (css.width) {
   1.395 +      TabItems.update(this.tab);
   1.396 +
   1.397 +      let widthRange, proportion;
   1.398 +
   1.399 +      if (this.parent && this.parent.isStacked()) {
   1.400 +        if (UI.rtl) {
   1.401 +          this.$fav.css({top:0, right:0});
   1.402 +        } else {
   1.403 +          this.$fav.css({top:0, left:0});
   1.404 +        }
   1.405 +        widthRange = new Range(70, 90);
   1.406 +        proportion = widthRange.proportion(css.width); // between 0 and 1
   1.407 +      } else {
   1.408 +        if (UI.rtl) {
   1.409 +          this.$fav.css({top:4, right:2});
   1.410 +        } else {
   1.411 +          this.$fav.css({top:4, left:4});
   1.412 +        }
   1.413 +        widthRange = new Range(40, 45);
   1.414 +        proportion = widthRange.proportion(css.width); // between 0 and 1
   1.415 +      }
   1.416 +
   1.417 +      if (proportion <= .1)
   1.418 +        this.$close.hide();
   1.419 +      else
   1.420 +        this.$close.show().css({opacity:proportion});
   1.421 +
   1.422 +      var pad = 1 + 5 * proportion;
   1.423 +      var alphaRange = new Range(0.1,0.2);
   1.424 +      this.$fav.css({
   1.425 +       "-moz-padding-start": pad + "px",
   1.426 +       "-moz-padding-end": pad + 2 + "px",
   1.427 +       "padding-top": pad + "px",
   1.428 +       "padding-bottom": pad + "px",
   1.429 +       "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")",
   1.430 +      });
   1.431 +    }
   1.432 +
   1.433 +    this._hasBeenDrawn = true;
   1.434 +
   1.435 +    UI.clearShouldResizeItems();
   1.436 +
   1.437 +    rect = this.getBounds(); // ensure that it's a <Rect>
   1.438 +
   1.439 +    Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!');
   1.440 +
   1.441 +    if (!this.parent && Utils.isValidXULTab(this.tab))
   1.442 +      this.setTrenches(rect);
   1.443 +
   1.444 +    this.save();
   1.445 +  },
   1.446 +
   1.447 +  // ----------
   1.448 +  // Function: setZ
   1.449 +  // Sets the z-index for this item.
   1.450 +  setZ: function TabItem_setZ(value) {
   1.451 +    this.zIndex = value;
   1.452 +    this.$container.css({zIndex: value});
   1.453 +  },
   1.454 +
   1.455 +  // ----------
   1.456 +  // Function: close
   1.457 +  // Closes this item (actually closes the tab associated with it, which automatically
   1.458 +  // closes the item.
   1.459 +  // Parameters:
   1.460 +  //   groupClose - true if this method is called by group close action.
   1.461 +  // Returns true if this tab is removed.
   1.462 +  close: function TabItem_close(groupClose) {
   1.463 +    // When the last tab is closed, put a new tab into closing tab's group. If
   1.464 +    // closing tab doesn't belong to a group and no empty group, create a new 
   1.465 +    // one for the new tab.
   1.466 +    if (!groupClose && gBrowser.tabs.length == 1) {
   1.467 +      let group = this.tab._tabViewTabItem.parent;
   1.468 +      group.newTab(null, { closedLastTab: true });
   1.469 +    }
   1.470 +
   1.471 +    // when "TabClose" event is fired, the browser tab is about to close and our 
   1.472 +    // item "close" is fired before the browser tab actually get closed. 
   1.473 +    // Therefore, we need "tabRemoved" event below.
   1.474 +    gBrowser.removeTab(this.tab);
   1.475 +    let tabClosed = !this.tab;
   1.476 +
   1.477 +    if (tabClosed)
   1.478 +      this._sendToSubscribers("tabRemoved");
   1.479 +
   1.480 +    // No need to explicitly delete the tab data, becasue sessionstore data
   1.481 +    // associated with the tab will automatically go away
   1.482 +    return tabClosed;
   1.483 +  },
   1.484 +
   1.485 +  // ----------
   1.486 +  // Function: addClass
   1.487 +  // Adds the specified CSS class to this item's container DOM element.
   1.488 +  addClass: function TabItem_addClass(className) {
   1.489 +    this.$container.addClass(className);
   1.490 +  },
   1.491 +
   1.492 +  // ----------
   1.493 +  // Function: removeClass
   1.494 +  // Removes the specified CSS class from this item's container DOM element.
   1.495 +  removeClass: function TabItem_removeClass(className) {
   1.496 +    this.$container.removeClass(className);
   1.497 +  },
   1.498 +
   1.499 +  // ----------
   1.500 +  // Function: makeActive
   1.501 +  // Updates this item to visually indicate that it's active.
   1.502 +  makeActive: function TabItem_makeActive() {
   1.503 +    this.$container.addClass("focus");
   1.504 +
   1.505 +    if (this.parent)
   1.506 +      this.parent.setActiveTab(this);
   1.507 +  },
   1.508 +
   1.509 +  // ----------
   1.510 +  // Function: makeDeactive
   1.511 +  // Updates this item to visually indicate that it's not active.
   1.512 +  makeDeactive: function TabItem_makeDeactive() {
   1.513 +    this.$container.removeClass("focus");
   1.514 +  },
   1.515 +
   1.516 +  // ----------
   1.517 +  // Function: zoomIn
   1.518 +  // Allows you to select the tab and zoom in on it, thereby bringing you
   1.519 +  // to the tab in Firefox to interact with.
   1.520 +  // Parameters:
   1.521 +  //   isNewBlankTab - boolean indicates whether it is a newly opened blank tab.
   1.522 +  zoomIn: function TabItem_zoomIn(isNewBlankTab) {
   1.523 +    // don't allow zoom in if its group is hidden
   1.524 +    if (this.parent && this.parent.hidden)
   1.525 +      return;
   1.526 +
   1.527 +    let self = this;
   1.528 +    let $tabEl = this.$container;
   1.529 +    let $canvas = this.$canvas;
   1.530 +
   1.531 +    Search.hide();
   1.532 +
   1.533 +    UI.setActive(this);
   1.534 +    TabItems._update(this.tab, {force: true});
   1.535 +
   1.536 +    // Zoom in!
   1.537 +    let tab = this.tab;
   1.538 +
   1.539 +    function onZoomDone() {
   1.540 +      $canvas.css({ 'transform': null });
   1.541 +      $tabEl.removeClass("front");
   1.542 +
   1.543 +      UI.goToTab(tab);
   1.544 +
   1.545 +      // tab might not be selected because hideTabView() is invoked after
   1.546 +      // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab
   1.547 +      if (!tab.selected) {
   1.548 +        UI.onTabSelect(gBrowser.selectedTab);
   1.549 +      } else {
   1.550 +        if (isNewBlankTab)
   1.551 +          gWindow.gURLBar.focus();
   1.552 +      }
   1.553 +      if (self.parent && self.parent.expanded)
   1.554 +        self.parent.collapse();
   1.555 +
   1.556 +      self._sendToSubscribers("zoomedIn");
   1.557 +    }
   1.558 +
   1.559 +    let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
   1.560 +    if (animateZoom) {
   1.561 +      let transform = this.getZoomTransform();
   1.562 +      TabItems.pausePainting();
   1.563 +
   1.564 +      if (this.parent && this.parent.expanded)
   1.565 +        $tabEl.removeClass("stack-trayed");
   1.566 +      $tabEl.addClass("front");
   1.567 +      $canvas
   1.568 +        .css({ 'transform-origin': transform.transformOrigin })
   1.569 +        .animate({ 'transform': transform.transform }, {
   1.570 +          duration: 230,
   1.571 +          easing: 'fast',
   1.572 +          complete: function() {
   1.573 +            onZoomDone();
   1.574 +
   1.575 +            setTimeout(function() {
   1.576 +              TabItems.resumePainting();
   1.577 +            }, 0);
   1.578 +          }
   1.579 +        });
   1.580 +    } else {
   1.581 +      setTimeout(onZoomDone, 0);
   1.582 +    }
   1.583 +  },
   1.584 +
   1.585 +  // ----------
   1.586 +  // Function: zoomOut
   1.587 +  // Handles the zoom down animation after returning to TabView.
   1.588 +  // It is expected that this routine will be called from the chrome thread
   1.589 +  //
   1.590 +  // Parameters:
   1.591 +  //   complete - a function to call after the zoom down animation
   1.592 +  zoomOut: function TabItem_zoomOut(complete) {
   1.593 +    let $tab = this.$container, $canvas = this.$canvas;
   1.594 +    var self = this;
   1.595 +    
   1.596 +    let onZoomDone = function onZoomDone() {
   1.597 +      $tab.removeClass("front");
   1.598 +      $canvas.css("transform", null);
   1.599 +
   1.600 +      if (typeof complete == "function")
   1.601 +        complete();
   1.602 +    };
   1.603 +
   1.604 +    UI.setActive(this);
   1.605 +    TabItems._update(this.tab, {force: true});
   1.606 +
   1.607 +    $tab.addClass("front");
   1.608 +
   1.609 +    let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
   1.610 +    if (animateZoom) {
   1.611 +      // The scaleCheat of 2 here is a clever way to speed up the zoom-out
   1.612 +      // code. See getZoomTransform() below.
   1.613 +      let transform = this.getZoomTransform(2);
   1.614 +      TabItems.pausePainting();
   1.615 +
   1.616 +      $canvas.css({
   1.617 +        'transform': transform.transform,
   1.618 +        'transform-origin': transform.transformOrigin
   1.619 +      });
   1.620 +
   1.621 +      $canvas.animate({ "transform": "scale(1.0)" }, {
   1.622 +        duration: 300,
   1.623 +        easing: 'cubic-bezier', // note that this is legal easing, even without parameters
   1.624 +        complete: function() {
   1.625 +          TabItems.resumePainting();
   1.626 +          onZoomDone();
   1.627 +        }
   1.628 +      });
   1.629 +    } else {
   1.630 +      onZoomDone();
   1.631 +    }
   1.632 +  },
   1.633 +
   1.634 +  // ----------
   1.635 +  // Function: getZoomTransform
   1.636 +  // Returns the transform function which represents the maximum bounds of the
   1.637 +  // tab thumbnail in the zoom animation.
   1.638 +  getZoomTransform: function TabItem_getZoomTransform(scaleCheat) {
   1.639 +    // Taking the bounds of the container (as opposed to the canvas) makes us
   1.640 +    // immune to any transformations applied to the canvas.
   1.641 +    let { left, top, width, height, right, bottom } = this.$container.bounds();
   1.642 +
   1.643 +    let { innerWidth: windowWidth, innerHeight: windowHeight } = window;
   1.644 +
   1.645 +    // The scaleCheat is a clever way to speed up the zoom-in code.
   1.646 +    // Because image scaling is slowest on big images, we cheat and stop
   1.647 +    // the image at scaled-down size and placed accordingly. Because the
   1.648 +    // animation is fast, you can't see the difference but it feels a lot
   1.649 +    // zippier. The only trick is choosing the right animation function so
   1.650 +    // that you don't see a change in percieved animation speed from frame #1
   1.651 +    // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame
   1.652 +    // of real animation). Choosing an animation that starts fast is key.
   1.653 +
   1.654 +    if (!scaleCheat)
   1.655 +      scaleCheat = 1.7;
   1.656 +
   1.657 +    let zoomWidth = width + (window.innerWidth - width) / scaleCheat;
   1.658 +    let zoomScaleFactor = zoomWidth / width;
   1.659 +
   1.660 +    let zoomHeight = height * zoomScaleFactor;
   1.661 +    let zoomTop = top * (1 - 1/scaleCheat);
   1.662 +    let zoomLeft = left * (1 - 1/scaleCheat);
   1.663 +
   1.664 +    let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100;
   1.665 +    let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100;
   1.666 +
   1.667 +    return {
   1.668 +      transformOrigin: xOrigin + "% " + yOrigin + "%",
   1.669 +      transform: "scale(" + zoomScaleFactor + ")"
   1.670 +    };
   1.671 +  },
   1.672 +
   1.673 +  // ----------
   1.674 +  // Function: updateCanvas
   1.675 +  // Updates the tabitem's canvas.
   1.676 +  updateCanvas: function TabItem_updateCanvas() {
   1.677 +    // ___ thumbnail
   1.678 +    let $canvas = this.$canvas;
   1.679 +    if (!this.canvasSizeForced) {
   1.680 +      let w = $canvas.width();
   1.681 +      let h = $canvas.height();
   1.682 +      if (w != $canvas[0].width || h != $canvas[0].height) {
   1.683 +        $canvas[0].width = w;
   1.684 +        $canvas[0].height = h;
   1.685 +      }
   1.686 +    }
   1.687 +
   1.688 +    TabItems._lastUpdateTime = Date.now();
   1.689 +    this._lastTabUpdateTime = TabItems._lastUpdateTime;
   1.690 +
   1.691 +    if (this.tabCanvas)
   1.692 +      this.tabCanvas.paint();
   1.693 +
   1.694 +    // ___ cache
   1.695 +    if (this.isShowingCachedData())
   1.696 +      this.hideCachedData();
   1.697 +  }
   1.698 +});
   1.699 +
   1.700 +// ##########
   1.701 +// Class: TabItems
   1.702 +// Singleton for managing <TabItem>s
   1.703 +let TabItems = {
   1.704 +  minTabWidth: 40,
   1.705 +  tabWidth: 160,
   1.706 +  tabHeight: 120,
   1.707 +  tabAspect: 0, // set in init
   1.708 +  invTabAspect: 0, // set in init  
   1.709 +  fontSize: 9,
   1.710 +  fontSizeRange: new Range(8,15),
   1.711 +  _fragment: null,
   1.712 +  items: [],
   1.713 +  paintingPaused: 0,
   1.714 +  _tabsWaitingForUpdate: null,
   1.715 +  _heartbeat: null, // see explanation at startHeartbeat() below
   1.716 +  _heartbeatTiming: 200, // milliseconds between calls
   1.717 +  _maxTimeForUpdating: 200, // milliseconds that consecutive updates can take
   1.718 +  _lastUpdateTime: Date.now(),
   1.719 +  _eventListeners: [],
   1.720 +  _pauseUpdateForTest: false,
   1.721 +  _reconnectingPaused: false,
   1.722 +  tabItemPadding: {},
   1.723 +  _mozAfterPaintHandler: null,
   1.724 +
   1.725 +  // ----------
   1.726 +  // Function: toString
   1.727 +  // Prints [TabItems count=count] for debug use
   1.728 +  toString: function TabItems_toString() {
   1.729 +    return "[TabItems count=" + this.items.length + "]";
   1.730 +  },
   1.731 +
   1.732 +  // ----------
   1.733 +  // Function: init
   1.734 +  // Set up the necessary tracking to maintain the <TabItems>s.
   1.735 +  init: function TabItems_init() {
   1.736 +    Utils.assert(window.AllTabs, "AllTabs must be initialized first");
   1.737 +    let self = this;
   1.738 +    
   1.739 +    // Set up tab priority queue
   1.740 +    this._tabsWaitingForUpdate = new TabPriorityQueue();
   1.741 +    this.minTabHeight = this.minTabWidth * this.tabHeight / this.tabWidth;
   1.742 +    this.tabAspect = this.tabHeight / this.tabWidth;
   1.743 +    this.invTabAspect = 1 / this.tabAspect;
   1.744 +
   1.745 +    let $canvas = iQ("<canvas>")
   1.746 +      .attr('moz-opaque', '');
   1.747 +    $canvas.appendTo(iQ("body"));
   1.748 +    $canvas.hide();
   1.749 +
   1.750 +    let mm = gWindow.messageManager;
   1.751 +    this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this);
   1.752 +    mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
   1.753 +
   1.754 +    // When a tab is opened, create the TabItem
   1.755 +    this._eventListeners.open = function (event) {
   1.756 +      let tab = event.target;
   1.757 +
   1.758 +      if (!tab.pinned)
   1.759 +        self.link(tab);
   1.760 +    }
   1.761 +    // When a tab's content is loaded, show the canvas and hide the cached data
   1.762 +    // if necessary.
   1.763 +    this._eventListeners.attrModified = function (event) {
   1.764 +      let tab = event.target;
   1.765 +
   1.766 +      if (!tab.pinned)
   1.767 +        self.update(tab);
   1.768 +    }
   1.769 +    // When a tab is closed, unlink.
   1.770 +    this._eventListeners.close = function (event) {
   1.771 +      let tab = event.target;
   1.772 +
   1.773 +      // XXX bug #635975 - don't unlink the tab if the dom window is closing.
   1.774 +      if (!tab.pinned && !UI.isDOMWindowClosing)
   1.775 +        self.unlink(tab);
   1.776 +    }
   1.777 +    for (let name in this._eventListeners) {
   1.778 +      AllTabs.register(name, this._eventListeners[name]);
   1.779 +    }
   1.780 +
   1.781 +    let activeGroupItem = GroupItems.getActiveGroupItem();
   1.782 +    let activeGroupItemId = activeGroupItem ? activeGroupItem.id : null;
   1.783 +    // For each tab, create the link.
   1.784 +    AllTabs.tabs.forEach(function (tab) {
   1.785 +      if (tab.pinned)
   1.786 +        return;
   1.787 +
   1.788 +      let options = {immediately: true};
   1.789 +      // if tab is visible in the tabstrip and doesn't have any data stored in 
   1.790 +      // the session store (see TabItem__reconnect), it implies that it is a 
   1.791 +      // new tab which is created before Panorama is initialized. Therefore, 
   1.792 +      // passing the active group id to the link() method for setting it up.
   1.793 +      if (!tab.hidden && activeGroupItemId)
   1.794 +         options.groupItemId = activeGroupItemId;
   1.795 +      self.link(tab, options);
   1.796 +      self.update(tab);
   1.797 +    });
   1.798 +  },
   1.799 +
   1.800 +  // ----------
   1.801 +  // Function: uninit
   1.802 +  uninit: function TabItems_uninit() {
   1.803 +    let mm = gWindow.messageManager;
   1.804 +    mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
   1.805 +
   1.806 +    for (let name in this._eventListeners) {
   1.807 +      AllTabs.unregister(name, this._eventListeners[name]);
   1.808 +    }
   1.809 +    this.items.forEach(function(tabItem) {
   1.810 +      delete tabItem.tab._tabViewTabItem;
   1.811 +
   1.812 +      for (let x in tabItem) {
   1.813 +        if (typeof tabItem[x] == "object")
   1.814 +          tabItem[x] = null;
   1.815 +      }
   1.816 +    });
   1.817 +
   1.818 +    this.items = null;
   1.819 +    this._eventListeners = null;
   1.820 +    this._lastUpdateTime = null;
   1.821 +    this._tabsWaitingForUpdate.clear();
   1.822 +  },
   1.823 +
   1.824 +  // ----------
   1.825 +  // Function: fragment
   1.826 +  // Return a DocumentFragment which has a single <div> child. This child node
   1.827 +  // will act as a template for all TabItem containers.
   1.828 +  // The first call of this function caches the DocumentFragment in _fragment.
   1.829 +  fragment: function TabItems_fragment() {
   1.830 +    if (this._fragment)
   1.831 +      return this._fragment;
   1.832 +
   1.833 +    let div = document.createElement("div");
   1.834 +    div.classList.add("tab");
   1.835 +    div.innerHTML = "<div class='thumb'>" +
   1.836 +            "<img class='cached-thumb' style='display:none'/><canvas moz-opaque/></div>" +
   1.837 +            "<div class='favicon'><img/></div>" +
   1.838 +            "<span class='tab-title'>&nbsp;</span>" +
   1.839 +            "<div class='close'></div>";
   1.840 +    this._fragment = document.createDocumentFragment();
   1.841 +    this._fragment.appendChild(div);
   1.842 +
   1.843 +    return this._fragment;
   1.844 +  },
   1.845 +
   1.846 +  // Function: _isComplete
   1.847 +  // Checks whether the xul:tab has fully loaded and calls a callback with a 
   1.848 +  // boolean indicates whether the tab is loaded or not.
   1.849 +  _isComplete: function TabItems__isComplete(tab, callback) {
   1.850 +    Utils.assertThrow(tab, "tab");
   1.851 +
   1.852 +    // A pending tab can't be complete, yet.
   1.853 +    if (tab.hasAttribute("pending")) {
   1.854 +      setTimeout(() => callback(false));
   1.855 +      return;
   1.856 +    }
   1.857 +
   1.858 +    let mm = tab.linkedBrowser.messageManager;
   1.859 +    let message = "Panorama:isDocumentLoaded";
   1.860 +
   1.861 +    mm.addMessageListener(message, function onMessage(cx) {
   1.862 +      mm.removeMessageListener(cx.name, onMessage);
   1.863 +      callback(cx.json.isLoaded);
   1.864 +    });
   1.865 +    mm.sendAsyncMessage(message);
   1.866 +  },
   1.867 +
   1.868 +  // ----------
   1.869 +  // Function: onMozAfterPaint
   1.870 +  // Called when a web page is painted.
   1.871 +  onMozAfterPaint: function TabItems_onMozAfterPaint(cx) {
   1.872 +    let index = gBrowser.browsers.indexOf(cx.target);
   1.873 +    if (index == -1)
   1.874 +      return;
   1.875 +
   1.876 +    let tab = gBrowser.tabs[index];
   1.877 +    if (!tab.pinned)
   1.878 +      this.update(tab);
   1.879 +  },
   1.880 +
   1.881 +  // ----------
   1.882 +  // Function: update
   1.883 +  // Takes in a xul:tab.
   1.884 +  update: function TabItems_update(tab) {
   1.885 +    try {
   1.886 +      Utils.assertThrow(tab, "tab");
   1.887 +      Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
   1.888 +      Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
   1.889 +
   1.890 +      let shouldDefer = (
   1.891 +        this.isPaintingPaused() ||
   1.892 +        this._tabsWaitingForUpdate.hasItems() ||
   1.893 +        Date.now() - this._lastUpdateTime < this._heartbeatTiming
   1.894 +      );
   1.895 +
   1.896 +      if (shouldDefer) {
   1.897 +        this._tabsWaitingForUpdate.push(tab);
   1.898 +        this.startHeartbeat();
   1.899 +      } else
   1.900 +        this._update(tab);
   1.901 +    } catch(e) {
   1.902 +      Utils.log(e);
   1.903 +    }
   1.904 +  },
   1.905 +
   1.906 +  // ----------
   1.907 +  // Function: _update
   1.908 +  // Takes in a xul:tab.
   1.909 +  //
   1.910 +  // Parameters:
   1.911 +  //   tab - a xul tab to update
   1.912 +  //   options - an object with additional parameters, see below
   1.913 +  //
   1.914 +  // Possible options:
   1.915 +  //   force - true to always update the tab item even if it's incomplete
   1.916 +  _update: function TabItems__update(tab, options) {
   1.917 +    try {
   1.918 +      if (this._pauseUpdateForTest)
   1.919 +        return;
   1.920 +
   1.921 +      Utils.assertThrow(tab, "tab");
   1.922 +
   1.923 +      // ___ get the TabItem
   1.924 +      Utils.assertThrow(tab._tabViewTabItem, "must already be linked");
   1.925 +      let tabItem = tab._tabViewTabItem;
   1.926 +
   1.927 +      // Even if the page hasn't loaded, display the favicon and title
   1.928 +      // ___ icon
   1.929 +      FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) {
   1.930 +        let favImage = tabItem.$favImage[0];
   1.931 +        let fav = tabItem.$fav;
   1.932 +        if (iconUrl) {
   1.933 +          if (favImage.src != iconUrl)
   1.934 +            favImage.src = iconUrl;
   1.935 +          fav.show();
   1.936 +        } else {
   1.937 +          if (favImage.hasAttribute("src"))
   1.938 +            favImage.removeAttribute("src");
   1.939 +          fav.hide();
   1.940 +        }
   1.941 +        tabItem._sendToSubscribers("iconUpdated");
   1.942 +      });
   1.943 +
   1.944 +      // ___ label
   1.945 +      let label = tab.label;
   1.946 +      let $name = tabItem.$tabTitle;
   1.947 +      if ($name.text() != label)
   1.948 +        $name.text(label);
   1.949 +
   1.950 +      // ___ remove from waiting list now that we have no other
   1.951 +      // early returns
   1.952 +      this._tabsWaitingForUpdate.remove(tab);
   1.953 +
   1.954 +      // ___ URL
   1.955 +      let tabUrl = tab.linkedBrowser.currentURI.spec;
   1.956 +      let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl);
   1.957 +      tabItem.$container.attr("title", tooltip);
   1.958 +
   1.959 +      // ___ Make sure the tab is complete and ready for updating.
   1.960 +      if (options && options.force) {
   1.961 +        tabItem.updateCanvas();
   1.962 +        tabItem._sendToSubscribers("updated");
   1.963 +      } else {
   1.964 +        this._isComplete(tab, function TabItems__update_isComplete(isComplete) {
   1.965 +          if (!Utils.isValidXULTab(tab) || tab.pinned)
   1.966 +            return;
   1.967 +
   1.968 +          if (isComplete) {
   1.969 +            tabItem.updateCanvas();
   1.970 +            tabItem._sendToSubscribers("updated");
   1.971 +          } else {
   1.972 +            this._tabsWaitingForUpdate.push(tab);
   1.973 +          }
   1.974 +        }.bind(this));
   1.975 +      }
   1.976 +    } catch(e) {
   1.977 +      Utils.log(e);
   1.978 +    }
   1.979 +  },
   1.980 +
   1.981 +  // ----------
   1.982 +  // Function: link
   1.983 +  // Takes in a xul:tab, creates a TabItem for it and adds it to the scene. 
   1.984 +  link: function TabItems_link(tab, options) {
   1.985 +    try {
   1.986 +      Utils.assertThrow(tab, "tab");
   1.987 +      Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
   1.988 +      Utils.assertThrow(!tab._tabViewTabItem, "shouldn't already be linked");
   1.989 +      new TabItem(tab, options); // sets tab._tabViewTabItem to itself
   1.990 +    } catch(e) {
   1.991 +      Utils.log(e);
   1.992 +    }
   1.993 +  },
   1.994 +
   1.995 +  // ----------
   1.996 +  // Function: unlink
   1.997 +  // Takes in a xul:tab and destroys the TabItem associated with it. 
   1.998 +  unlink: function TabItems_unlink(tab) {
   1.999 +    try {
  1.1000 +      Utils.assertThrow(tab, "tab");
  1.1001 +      Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
  1.1002 +      // note that it's ok to unlink an app tab; see .handleTabUnpin
  1.1003 +
  1.1004 +      this.unregister(tab._tabViewTabItem);
  1.1005 +      tab._tabViewTabItem._sendToSubscribers("close");
  1.1006 +      tab._tabViewTabItem.$container.remove();
  1.1007 +      tab._tabViewTabItem.removeTrenches();
  1.1008 +      Items.unsquish(null, tab._tabViewTabItem);
  1.1009 +
  1.1010 +      tab._tabViewTabItem.tab = null;
  1.1011 +      tab._tabViewTabItem.tabCanvas.tab = null;
  1.1012 +      tab._tabViewTabItem.tabCanvas = null;
  1.1013 +      tab._tabViewTabItem = null;
  1.1014 +      Storage.saveTab(tab, null);
  1.1015 +
  1.1016 +      this._tabsWaitingForUpdate.remove(tab);
  1.1017 +    } catch(e) {
  1.1018 +      Utils.log(e);
  1.1019 +    }
  1.1020 +  },
  1.1021 +
  1.1022 +  // ----------
  1.1023 +  // when a tab becomes pinned, destroy its TabItem
  1.1024 +  handleTabPin: function TabItems_handleTabPin(xulTab) {
  1.1025 +    this.unlink(xulTab);
  1.1026 +  },
  1.1027 +
  1.1028 +  // ----------
  1.1029 +  // when a tab becomes unpinned, create a TabItem for it
  1.1030 +  handleTabUnpin: function TabItems_handleTabUnpin(xulTab) {
  1.1031 +    this.link(xulTab);
  1.1032 +    this.update(xulTab);
  1.1033 +  },
  1.1034 +
  1.1035 +  // ----------
  1.1036 +  // Function: startHeartbeat
  1.1037 +  // Start a new heartbeat if there isn't one already started.
  1.1038 +  // The heartbeat is a chain of setTimeout calls that allows us to spread
  1.1039 +  // out update calls over a period of time.
  1.1040 +  // _heartbeat is used to make sure that we don't add multiple 
  1.1041 +  // setTimeout chains.
  1.1042 +  startHeartbeat: function TabItems_startHeartbeat() {
  1.1043 +    if (!this._heartbeat) {
  1.1044 +      let self = this;
  1.1045 +      this._heartbeat = setTimeout(function() {
  1.1046 +        self._checkHeartbeat();
  1.1047 +      }, this._heartbeatTiming);
  1.1048 +    }
  1.1049 +  },
  1.1050 +
  1.1051 +  // ----------
  1.1052 +  // Function: _checkHeartbeat
  1.1053 +  // This periodically checks for tabs waiting to be updated, and calls
  1.1054 +  // _update on them.
  1.1055 +  // Should only be called by startHeartbeat and resumePainting.
  1.1056 +  _checkHeartbeat: function TabItems__checkHeartbeat() {
  1.1057 +    this._heartbeat = null;
  1.1058 +
  1.1059 +    if (this.isPaintingPaused())
  1.1060 +      return;
  1.1061 +
  1.1062 +    // restart the heartbeat to update all waiting tabs once the UI becomes idle
  1.1063 +    if (!UI.isIdle()) {
  1.1064 +      this.startHeartbeat();
  1.1065 +      return;
  1.1066 +    }
  1.1067 +
  1.1068 +    let accumTime = 0;
  1.1069 +    let items = this._tabsWaitingForUpdate.getItems();
  1.1070 +    // Do as many updates as we can fit into a "perceived" amount
  1.1071 +    // of time, which is tunable.
  1.1072 +    while (accumTime < this._maxTimeForUpdating && items.length) {
  1.1073 +      let updateBegin = Date.now();
  1.1074 +      this._update(items.pop());
  1.1075 +      let updateEnd = Date.now();
  1.1076 +
  1.1077 +      // Maintain a simple average of time for each tabitem update
  1.1078 +      // We can use this as a base by which to delay things like
  1.1079 +      // tab zooming, so there aren't any hitches.
  1.1080 +      let deltaTime = updateEnd - updateBegin;
  1.1081 +      accumTime += deltaTime;
  1.1082 +    }
  1.1083 +
  1.1084 +    if (this._tabsWaitingForUpdate.hasItems())
  1.1085 +      this.startHeartbeat();
  1.1086 +  },
  1.1087 +
  1.1088 +  // ----------
  1.1089 +  // Function: pausePainting
  1.1090 +  // Tells TabItems to stop updating thumbnails (so you can do
  1.1091 +  // animations without thumbnail paints causing stutters).
  1.1092 +  // pausePainting can be called multiple times, but every call to
  1.1093 +  // pausePainting needs to be mirrored with a call to <resumePainting>.
  1.1094 +  pausePainting: function TabItems_pausePainting() {
  1.1095 +    this.paintingPaused++;
  1.1096 +    if (this._heartbeat) {
  1.1097 +      clearTimeout(this._heartbeat);
  1.1098 +      this._heartbeat = null;
  1.1099 +    }
  1.1100 +  },
  1.1101 +
  1.1102 +  // ----------
  1.1103 +  // Function: resumePainting
  1.1104 +  // Undoes a call to <pausePainting>. For instance, if you called
  1.1105 +  // pausePainting three times in a row, you'll need to call resumePainting
  1.1106 +  // three times before TabItems will start updating thumbnails again.
  1.1107 +  resumePainting: function TabItems_resumePainting() {
  1.1108 +    this.paintingPaused--;
  1.1109 +    Utils.assert(this.paintingPaused > -1, "paintingPaused should not go below zero");
  1.1110 +    if (!this.isPaintingPaused())
  1.1111 +      this.startHeartbeat();
  1.1112 +  },
  1.1113 +
  1.1114 +  // ----------
  1.1115 +  // Function: isPaintingPaused
  1.1116 +  // Returns a boolean indicating whether painting
  1.1117 +  // is paused or not.
  1.1118 +  isPaintingPaused: function TabItems_isPaintingPaused() {
  1.1119 +    return this.paintingPaused > 0;
  1.1120 +  },
  1.1121 +
  1.1122 +  // ----------
  1.1123 +  // Function: pauseReconnecting
  1.1124 +  // Don't reconnect any new tabs until resume is called.
  1.1125 +  pauseReconnecting: function TabItems_pauseReconnecting() {
  1.1126 +    Utils.assertThrow(!this._reconnectingPaused, "shouldn't already be paused");
  1.1127 +
  1.1128 +    this._reconnectingPaused = true;
  1.1129 +  },
  1.1130 +  
  1.1131 +  // ----------
  1.1132 +  // Function: resumeReconnecting
  1.1133 +  // Reconnect all of the tabs that were created since we paused.
  1.1134 +  resumeReconnecting: function TabItems_resumeReconnecting() {
  1.1135 +    Utils.assertThrow(this._reconnectingPaused, "should already be paused");
  1.1136 +
  1.1137 +    this._reconnectingPaused = false;
  1.1138 +    this.items.forEach(function(item) {
  1.1139 +      if (!item._reconnected)
  1.1140 +        item._reconnect();
  1.1141 +    });
  1.1142 +  },
  1.1143 +  
  1.1144 +  // ----------
  1.1145 +  // Function: reconnectingPaused
  1.1146 +  // Returns true if reconnecting is paused.
  1.1147 +  reconnectingPaused: function TabItems_reconnectingPaused() {
  1.1148 +    return this._reconnectingPaused;
  1.1149 +  },
  1.1150 +  
  1.1151 +  // ----------
  1.1152 +  // Function: register
  1.1153 +  // Adds the given <TabItem> to the master list.
  1.1154 +  register: function TabItems_register(item) {
  1.1155 +    Utils.assert(item && item.isAnItem, 'item must be a TabItem');
  1.1156 +    Utils.assert(this.items.indexOf(item) == -1, 'only register once per item');
  1.1157 +    this.items.push(item);
  1.1158 +  },
  1.1159 +
  1.1160 +  // ----------
  1.1161 +  // Function: unregister
  1.1162 +  // Removes the given <TabItem> from the master list.
  1.1163 +  unregister: function TabItems_unregister(item) {
  1.1164 +    var index = this.items.indexOf(item);
  1.1165 +    if (index != -1)
  1.1166 +      this.items.splice(index, 1);
  1.1167 +  },
  1.1168 +
  1.1169 +  // ----------
  1.1170 +  // Function: getItems
  1.1171 +  // Returns a copy of the master array of <TabItem>s.
  1.1172 +  getItems: function TabItems_getItems() {
  1.1173 +    return Utils.copy(this.items);
  1.1174 +  },
  1.1175 +
  1.1176 +  // ----------
  1.1177 +  // Function: saveAll
  1.1178 +  // Saves all open <TabItem>s.
  1.1179 +  saveAll: function TabItems_saveAll() {
  1.1180 +    let tabItems = this.getItems();
  1.1181 +
  1.1182 +    tabItems.forEach(function TabItems_saveAll_forEach(tabItem) {
  1.1183 +      tabItem.save();
  1.1184 +    });
  1.1185 +  },
  1.1186 +
  1.1187 +  // ----------
  1.1188 +  // Function: storageSanity
  1.1189 +  // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage)
  1.1190 +  // and returns true if it looks valid.
  1.1191 +  // TODO: this is a stub, please implement
  1.1192 +  storageSanity: function TabItems_storageSanity(data) {
  1.1193 +    return true;
  1.1194 +  },
  1.1195 +
  1.1196 +  // ----------
  1.1197 +  // Function: getFontSizeFromWidth
  1.1198 +  // Private method that returns the fontsize to use given the tab's width
  1.1199 +  getFontSizeFromWidth: function TabItem_getFontSizeFromWidth(width) {
  1.1200 +    let widthRange = new Range(0, TabItems.tabWidth);
  1.1201 +    let proportion = widthRange.proportion(width - TabItems.tabItemPadding.x, true);
  1.1202 +    // proportion is in [0,1]
  1.1203 +    return TabItems.fontSizeRange.scale(proportion);
  1.1204 +  },
  1.1205 +
  1.1206 +  // ----------
  1.1207 +  // Function: _getWidthForHeight
  1.1208 +  // Private method that returns the tabitem width given a height.
  1.1209 +  _getWidthForHeight: function TabItems__getWidthForHeight(height) {
  1.1210 +    return height * TabItems.invTabAspect;
  1.1211 +  },
  1.1212 +
  1.1213 +  // ----------
  1.1214 +  // Function: _getHeightForWidth
  1.1215 +  // Private method that returns the tabitem height given a width.
  1.1216 +  _getHeightForWidth: function TabItems__getHeightForWidth(width) {
  1.1217 +    return width * TabItems.tabAspect;
  1.1218 +  },
  1.1219 +
  1.1220 +  // ----------
  1.1221 +  // Function: calcValidSize
  1.1222 +  // Pass in a desired size, and receive a size based on proper title
  1.1223 +  // size and aspect ratio.
  1.1224 +  calcValidSize: function TabItems_calcValidSize(size, options) {
  1.1225 +    Utils.assert(Utils.isPoint(size), 'input is a Point');
  1.1226 +
  1.1227 +    let width = Math.max(TabItems.minTabWidth, size.x);
  1.1228 +    let showTitle = !options || !options.hideTitle;
  1.1229 +    let titleSize = showTitle ? TabItems.fontSizeRange.max : 0;
  1.1230 +    let height = Math.max(TabItems.minTabHeight, size.y - titleSize);
  1.1231 +    let retSize = new Point(width, height);
  1.1232 +
  1.1233 +    if (size.x > -1)
  1.1234 +      retSize.y = this._getHeightForWidth(width);
  1.1235 +    if (size.y > -1)
  1.1236 +      retSize.x = this._getWidthForHeight(height);
  1.1237 +
  1.1238 +    if (size.x > -1 && size.y > -1) {
  1.1239 +      if (retSize.x < size.x)
  1.1240 +        retSize.y = this._getHeightForWidth(retSize.x);
  1.1241 +      else
  1.1242 +        retSize.x = this._getWidthForHeight(retSize.y);
  1.1243 +    }
  1.1244 +
  1.1245 +    if (showTitle)
  1.1246 +      retSize.y += titleSize;
  1.1247 +
  1.1248 +    return retSize;
  1.1249 +  }
  1.1250 +};
  1.1251 +
  1.1252 +// ##########
  1.1253 +// Class: TabPriorityQueue
  1.1254 +// Container that returns tab items in a priority order
  1.1255 +// Current implementation assigns tab to either a high priority
  1.1256 +// or low priority queue, and toggles which queue items are popped
  1.1257 +// from. This guarantees that high priority items which are constantly
  1.1258 +// being added will not eclipse changes for lower priority items.
  1.1259 +function TabPriorityQueue() {
  1.1260 +};
  1.1261 +
  1.1262 +TabPriorityQueue.prototype = {
  1.1263 +  _low: [], // low priority queue
  1.1264 +  _high: [], // high priority queue
  1.1265 +
  1.1266 +  // ----------
  1.1267 +  // Function: toString
  1.1268 +  // Prints [TabPriorityQueue count=count] for debug use
  1.1269 +  toString: function TabPriorityQueue_toString() {
  1.1270 +    return "[TabPriorityQueue count=" + (this._low.length + this._high.length) + "]";
  1.1271 +  },
  1.1272 +
  1.1273 +  // ----------
  1.1274 +  // Function: clear
  1.1275 +  // Empty the update queue
  1.1276 +  clear: function TabPriorityQueue_clear() {
  1.1277 +    this._low = [];
  1.1278 +    this._high = [];
  1.1279 +  },
  1.1280 +
  1.1281 +  // ----------
  1.1282 +  // Function: hasItems
  1.1283 +  // Return whether pending items exist
  1.1284 +  hasItems: function TabPriorityQueue_hasItems() {
  1.1285 +    return (this._low.length > 0) || (this._high.length > 0);
  1.1286 +  },
  1.1287 +
  1.1288 +  // ----------
  1.1289 +  // Function: getItems
  1.1290 +  // Returns all queued items, ordered from low to high priority
  1.1291 +  getItems: function TabPriorityQueue_getItems() {
  1.1292 +    return this._low.concat(this._high);
  1.1293 +  },
  1.1294 +
  1.1295 +  // ----------
  1.1296 +  // Function: push
  1.1297 +  // Add an item to be prioritized
  1.1298 +  push: function TabPriorityQueue_push(tab) {
  1.1299 +    // Push onto correct priority queue.
  1.1300 +    // It's only low priority if it's in a stack, and isn't the top,
  1.1301 +    // and the stack isn't expanded.
  1.1302 +    // If it already exists in the destination queue,
  1.1303 +    // leave it. If it exists in a different queue, remove it first and push
  1.1304 +    // onto new queue.
  1.1305 +    let item = tab._tabViewTabItem;
  1.1306 +    if (item.parent && (item.parent.isStacked() &&
  1.1307 +      !item.parent.isTopOfStack(item) &&
  1.1308 +      !item.parent.expanded)) {
  1.1309 +      let idx = this._high.indexOf(tab);
  1.1310 +      if (idx != -1) {
  1.1311 +        this._high.splice(idx, 1);
  1.1312 +        this._low.unshift(tab);
  1.1313 +      } else if (this._low.indexOf(tab) == -1)
  1.1314 +        this._low.unshift(tab);
  1.1315 +    } else {
  1.1316 +      let idx = this._low.indexOf(tab);
  1.1317 +      if (idx != -1) {
  1.1318 +        this._low.splice(idx, 1);
  1.1319 +        this._high.unshift(tab);
  1.1320 +      } else if (this._high.indexOf(tab) == -1)
  1.1321 +        this._high.unshift(tab);
  1.1322 +    }
  1.1323 +  },
  1.1324 +
  1.1325 +  // ----------
  1.1326 +  // Function: pop
  1.1327 +  // Remove and return the next item in priority order
  1.1328 +  pop: function TabPriorityQueue_pop() {
  1.1329 +    let ret = null;
  1.1330 +    if (this._high.length)
  1.1331 +      ret = this._high.pop();
  1.1332 +    else if (this._low.length)
  1.1333 +      ret = this._low.pop();
  1.1334 +    return ret;
  1.1335 +  },
  1.1336 +
  1.1337 +  // ----------
  1.1338 +  // Function: peek
  1.1339 +  // Return the next item in priority order, without removing it
  1.1340 +  peek: function TabPriorityQueue_peek() {
  1.1341 +    let ret = null;
  1.1342 +    if (this._high.length)
  1.1343 +      ret = this._high[this._high.length-1];
  1.1344 +    else if (this._low.length)
  1.1345 +      ret = this._low[this._low.length-1];
  1.1346 +    return ret;
  1.1347 +  },
  1.1348 +
  1.1349 +  // ----------
  1.1350 +  // Function: remove
  1.1351 +  // Remove the passed item
  1.1352 +  remove: function TabPriorityQueue_remove(tab) {
  1.1353 +    let index = this._high.indexOf(tab);
  1.1354 +    if (index != -1)
  1.1355 +      this._high.splice(index, 1);
  1.1356 +    else {
  1.1357 +      index = this._low.indexOf(tab);
  1.1358 +      if (index != -1)
  1.1359 +        this._low.splice(index, 1);
  1.1360 +    }
  1.1361 +  }
  1.1362 +};
  1.1363 +
  1.1364 +// ##########
  1.1365 +// Class: TabCanvas
  1.1366 +// Takes care of the actual canvas for the tab thumbnail
  1.1367 +// Does not need to be accessed from outside of tabitems.js
  1.1368 +function TabCanvas(tab, canvas) {
  1.1369 +  this.tab = tab;
  1.1370 +  this.canvas = canvas;
  1.1371 +};
  1.1372 +
  1.1373 +TabCanvas.prototype = Utils.extend(new Subscribable(), {
  1.1374 +  // ----------
  1.1375 +  // Function: toString
  1.1376 +  // Prints [TabCanvas (tab)] for debug use
  1.1377 +  toString: function TabCanvas_toString() {
  1.1378 +    return "[TabCanvas (" + this.tab + ")]";
  1.1379 +  },
  1.1380 +
  1.1381 +  // ----------
  1.1382 +  // Function: paint
  1.1383 +  paint: function TabCanvas_paint(evt) {
  1.1384 +    var w = this.canvas.width;
  1.1385 +    var h = this.canvas.height;
  1.1386 +    if (!w || !h)
  1.1387 +      return;
  1.1388 +
  1.1389 +    if (!this.tab.linkedBrowser.contentWindow) {
  1.1390 +      Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
  1.1391 +      return;
  1.1392 +    }
  1.1393 +
  1.1394 +    let win = this.tab.linkedBrowser.contentWindow;
  1.1395 +    gPageThumbnails.captureToCanvas(win, this.canvas);
  1.1396 +
  1.1397 +    this._sendToSubscribers("painted");
  1.1398 +  },
  1.1399 +
  1.1400 +  // ----------
  1.1401 +  // Function: toImageData
  1.1402 +  toImageData: function TabCanvas_toImageData() {
  1.1403 +    return this.canvas.toDataURL("image/png");
  1.1404 +  }
  1.1405 +});

mercurial