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'> </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 +});