diff -r 000000000000 -r 6474c204b198 browser/components/tabview/tabitems.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/tabitems.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1402 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// ********** +// Title: tabitems.js + +// ########## +// Class: TabItem +// An that represents a tab. Also implements the interface. +// +// Parameters: +// tab - a xul:tab +function TabItem(tab, options) { + Utils.assert(tab, "tab"); + + this.tab = tab; + // register this as the tab's tabItem + this.tab._tabViewTabItem = this; + + if (!options) + options = {}; + + // ___ set up div + document.body.appendChild(TabItems.fragment().cloneNode(true)); + + // The document fragment contains just one Node + // As per DOM3 appendChild: it will then be the last child + let div = document.body.lastChild; + let $div = iQ(div); + + this._showsCachedData = false; + this.canvasSizeForced = false; + this.$thumb = iQ('.thumb', $div); + this.$fav = iQ('.favicon', $div); + this.$tabTitle = iQ('.tab-title', $div); + this.$canvas = iQ('.thumb canvas', $div); + this.$cachedThumb = iQ('img.cached-thumb', $div); + this.$favImage = iQ('.favicon>img', $div); + this.$close = iQ('.close', $div); + + this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]); + + this._hidden = false; + this.isATabItem = true; + this.keepProportional = true; + this._hasBeenDrawn = false; + this._reconnected = false; + this.isDragging = false; + this.isStacked = false; + + // Read off the total vertical and horizontal padding on the tab container + // and cache this value, as it must be the same for every TabItem. + if (Utils.isEmptyObject(TabItems.tabItemPadding)) { + TabItems.tabItemPadding.x = parseInt($div.css('padding-left')) + + parseInt($div.css('padding-right')); + + TabItems.tabItemPadding.y = parseInt($div.css('padding-top')) + + parseInt($div.css('padding-bottom')); + } + + this.bounds = new Rect(0,0,1,1); + + this._lastTabUpdateTime = Date.now(); + + // ___ superclass setup + this._init(div); + + // ___ drag/drop + // override dropOptions with custom tabitem methods + this.dropOptions.drop = function(e) { + let groupItem = drag.info.item.parent; + groupItem.add(drag.info.$el); + }; + + this.draggable(); + + let self = this; + + // ___ more div setup + $div.mousedown(function(e) { + if (!Utils.isRightClick(e)) + self.lastMouseDownTarget = e.target; + }); + + $div.mouseup(function(e) { + var same = (e.target == self.lastMouseDownTarget); + self.lastMouseDownTarget = null; + if (!same) + return; + + // press close button or middle mouse click + if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) { + self.closedManually = true; + self.close(); + } else { + if (!Items.item(this).isDragging) + self.zoomIn(); + } + }); + + this.droppable(true); + + this.$close.attr("title", tabbrowserString("tabs.closeTab")); + + TabItems.register(this); + + // ___ reconnect to data from Storage + if (!TabItems.reconnectingPaused()) + this._reconnect(options); +}; + +TabItem.prototype = Utils.extend(new Item(), new Subscribable(), { + // ---------- + // Function: toString + // Prints [TabItem (tab)] for debug use + toString: function TabItem_toString() { + return "[TabItem (" + this.tab + ")]"; + }, + + // ---------- + // Function: forceCanvasSize + // Repaints the thumbnail with the given resolution, and forces it + // to stay that resolution until unforceCanvasSize is called. + forceCanvasSize: function TabItem_forceCanvasSize(w, h) { + this.canvasSizeForced = true; + this.$canvas[0].width = w; + this.$canvas[0].height = h; + this.tabCanvas.paint(); + }, + + // ---------- + // Function: unforceCanvasSize + // Stops holding the thumbnail resolution; allows it to shift to the + // size of thumbnail on screen. Note that this call does not nest, unlike + // ; if you call forceCanvasSize multiple + // times, you just need a single unforce to clear them all. + unforceCanvasSize: function TabItem_unforceCanvasSize() { + this.canvasSizeForced = false; + }, + + // ---------- + // Function: isShowingCachedData + // Returns a boolean indicates whether the cached data is being displayed or + // not. + isShowingCachedData: function TabItem_isShowingCachedData() { + return this._showsCachedData; + }, + + // ---------- + // Function: showCachedData + // Shows the cached data i.e. image and title. Note: this method should only + // be called at browser startup with the cached data avaliable. + showCachedData: function TabItem_showCachedData() { + let {title, url} = this.getTabState(); + let thumbnailURL = gPageThumbnails.getThumbnailURL(url); + + this.$cachedThumb.attr("src", thumbnailURL).show(); + this.$canvas.css({opacity: 0}); + + let tooltip = (title && title != url ? title + "\n" + url : url); + this.$tabTitle.text(title).attr("title", tooltip); + this._showsCachedData = true; + }, + + // ---------- + // Function: hideCachedData + // Hides the cached data i.e. image and title and show the canvas. + hideCachedData: function TabItem_hideCachedData() { + this.$cachedThumb.attr("src", "").hide(); + this.$canvas.css({opacity: 1.0}); + this._showsCachedData = false; + }, + + // ---------- + // Function: getStorageData + // Get data to be used for persistent storage of this object. + getStorageData: function TabItem_getStorageData() { + let data = { + groupID: (this.parent ? this.parent.id : 0) + }; + if (this.parent && this.parent.getActiveTab() == this) + data.active = true; + + return data; + }, + + // ---------- + // Function: save + // Store persistent for this object. + save: function TabItem_save() { + try { + if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save + return; + + let data = this.getStorageData(); + if (TabItems.storageSanity(data)) + Storage.saveTab(this.tab, data); + } catch(e) { + Utils.log("Error in saving tab value: "+e); + } + }, + + // ---------- + // Function: _getCurrentTabStateEntry + // Returns the current tab state's active history entry. + _getCurrentTabStateEntry: function TabItem__getCurrentTabStateEntry() { + let tabState = Storage.getTabState(this.tab); + + if (tabState) { + let index = (tabState.index || tabState.entries.length) - 1; + if (index in tabState.entries) + return tabState.entries[index]; + } + + return null; + }, + + // ---------- + // Function: getTabState + // Returns the current tab state, i.e. the title and URL of the active + // history entry. + getTabState: function TabItem_getTabState() { + let entry = this._getCurrentTabStateEntry(); + let title = ""; + let url = ""; + + if (entry) { + if (entry.title) + title = entry.title; + + url = entry.url; + } else { + url = this.tab.linkedBrowser.currentURI.spec; + } + + return {title: title, url: url}; + }, + + // ---------- + // Function: _reconnect + // Load the reciever's persistent data from storage. If there is none, + // treats it as a new tab. + // + // Parameters: + // options - an object with additional parameters, see below + // + // Possible options: + // groupItemId - if the tab doesn't have any data associated with it and + // groupItemId is available, add the tab to that group. + _reconnect: function TabItem__reconnect(options) { + Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected"); + Utils.assertThrow(this.tab, "should have a xul:tab"); + + let tabData = Storage.getTabData(this.tab); + let groupItem; + + if (tabData && TabItems.storageSanity(tabData)) { + // Show the cached data while we're waiting for the tabItem to be updated. + // If the tab isn't restored yet this acts as a placeholder until it is. + this.showCachedData(); + + if (this.parent) + this.parent.remove(this, {immediately: true}); + + if (tabData.groupID) + groupItem = GroupItems.groupItem(tabData.groupID); + else + groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds}); + + if (groupItem) { + groupItem.add(this, {immediately: true}); + + // restore the active tab for each group between browser sessions + if (tabData.active) + groupItem.setActiveTab(this); + + // if it matches the selected tab or no active tab and the browser + // tab is hidden, the active group item would be set. + if (this.tab.selected || + (!GroupItems.getActiveGroupItem() && !this.tab.hidden)) + UI.setActive(this.parent); + } + } else { + if (options && options.groupItemId) + groupItem = GroupItems.groupItem(options.groupItemId); + + if (groupItem) { + groupItem.add(this, {immediately: true}); + } else { + // create tab group by double click is handled in UI_init(). + GroupItems.newTab(this, {immediately: true}); + } + } + + this._reconnected = true; + this.save(); + this._sendToSubscribers("reconnected"); + }, + + // ---------- + // Function: setHidden + // Hide/unhide this item + setHidden: function TabItem_setHidden(val) { + if (val) + this.addClass("tabHidden"); + else + this.removeClass("tabHidden"); + this._hidden = val; + }, + + // ---------- + // Function: getHidden + // Return hide state of item + getHidden: function TabItem_getHidden() { + return this._hidden; + }, + + // ---------- + // Function: setBounds + // Moves this item to the specified location and size. + // + // Parameters: + // rect - a giving the new bounds + // immediately - true if it should not animate; default false + // options - an object with additional parameters, see below + // + // Possible options: + // force - true to always update the DOM even if the bounds haven't changed; default false + setBounds: function TabItem_setBounds(inRect, immediately, options) { + Utils.assert(Utils.isRect(inRect), 'TabItem.setBounds: rect is not a real rectangle!'); + + if (!options) + options = {}; + + // force the input size to be valid + let validSize = TabItems.calcValidSize( + new Point(inRect.width, inRect.height), + {hideTitle: (this.isStacked || options.hideTitle === true)}); + let rect = new Rect(inRect.left, inRect.top, + validSize.x, validSize.y); + + var css = {}; + + if (rect.left != this.bounds.left || options.force) + css.left = rect.left; + + if (rect.top != this.bounds.top || options.force) + css.top = rect.top; + + if (rect.width != this.bounds.width || options.force) { + css.width = rect.width - TabItems.tabItemPadding.x; + css.fontSize = TabItems.getFontSizeFromWidth(rect.width); + css.fontSize += 'px'; + } + + if (rect.height != this.bounds.height || options.force) { + css.height = rect.height - TabItems.tabItemPadding.y; + if (!this.isStacked) + css.height -= TabItems.fontSizeRange.max; + } + + if (Utils.isEmptyObject(css)) + return; + + this.bounds.copy(rect); + + // If this is a brand new tab don't animate it in from + // a random location (i.e., from [0,0]). Instead, just + // have it appear where it should be. + if (immediately || (!this._hasBeenDrawn)) { + this.$container.css(css); + } else { + TabItems.pausePainting(); + this.$container.animate(css, { + duration: 200, + easing: "tabviewBounce", + complete: function() { + TabItems.resumePainting(); + } + }); + } + + if (css.fontSize && !(this.parent && this.parent.isStacked())) { + if (css.fontSize < TabItems.fontSizeRange.min) + immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut(); + else + immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn(); + } + + if (css.width) { + TabItems.update(this.tab); + + let widthRange, proportion; + + if (this.parent && this.parent.isStacked()) { + if (UI.rtl) { + this.$fav.css({top:0, right:0}); + } else { + this.$fav.css({top:0, left:0}); + } + widthRange = new Range(70, 90); + proportion = widthRange.proportion(css.width); // between 0 and 1 + } else { + if (UI.rtl) { + this.$fav.css({top:4, right:2}); + } else { + this.$fav.css({top:4, left:4}); + } + widthRange = new Range(40, 45); + proportion = widthRange.proportion(css.width); // between 0 and 1 + } + + if (proportion <= .1) + this.$close.hide(); + else + this.$close.show().css({opacity:proportion}); + + var pad = 1 + 5 * proportion; + var alphaRange = new Range(0.1,0.2); + this.$fav.css({ + "-moz-padding-start": pad + "px", + "-moz-padding-end": pad + 2 + "px", + "padding-top": pad + "px", + "padding-bottom": pad + "px", + "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")", + }); + } + + this._hasBeenDrawn = true; + + UI.clearShouldResizeItems(); + + rect = this.getBounds(); // ensure that it's a + + Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!'); + + if (!this.parent && Utils.isValidXULTab(this.tab)) + this.setTrenches(rect); + + this.save(); + }, + + // ---------- + // Function: setZ + // Sets the z-index for this item. + setZ: function TabItem_setZ(value) { + this.zIndex = value; + this.$container.css({zIndex: value}); + }, + + // ---------- + // Function: close + // Closes this item (actually closes the tab associated with it, which automatically + // closes the item. + // Parameters: + // groupClose - true if this method is called by group close action. + // Returns true if this tab is removed. + close: function TabItem_close(groupClose) { + // When the last tab is closed, put a new tab into closing tab's group. If + // closing tab doesn't belong to a group and no empty group, create a new + // one for the new tab. + if (!groupClose && gBrowser.tabs.length == 1) { + let group = this.tab._tabViewTabItem.parent; + group.newTab(null, { closedLastTab: true }); + } + + // when "TabClose" event is fired, the browser tab is about to close and our + // item "close" is fired before the browser tab actually get closed. + // Therefore, we need "tabRemoved" event below. + gBrowser.removeTab(this.tab); + let tabClosed = !this.tab; + + if (tabClosed) + this._sendToSubscribers("tabRemoved"); + + // No need to explicitly delete the tab data, becasue sessionstore data + // associated with the tab will automatically go away + return tabClosed; + }, + + // ---------- + // Function: addClass + // Adds the specified CSS class to this item's container DOM element. + addClass: function TabItem_addClass(className) { + this.$container.addClass(className); + }, + + // ---------- + // Function: removeClass + // Removes the specified CSS class from this item's container DOM element. + removeClass: function TabItem_removeClass(className) { + this.$container.removeClass(className); + }, + + // ---------- + // Function: makeActive + // Updates this item to visually indicate that it's active. + makeActive: function TabItem_makeActive() { + this.$container.addClass("focus"); + + if (this.parent) + this.parent.setActiveTab(this); + }, + + // ---------- + // Function: makeDeactive + // Updates this item to visually indicate that it's not active. + makeDeactive: function TabItem_makeDeactive() { + this.$container.removeClass("focus"); + }, + + // ---------- + // Function: zoomIn + // Allows you to select the tab and zoom in on it, thereby bringing you + // to the tab in Firefox to interact with. + // Parameters: + // isNewBlankTab - boolean indicates whether it is a newly opened blank tab. + zoomIn: function TabItem_zoomIn(isNewBlankTab) { + // don't allow zoom in if its group is hidden + if (this.parent && this.parent.hidden) + return; + + let self = this; + let $tabEl = this.$container; + let $canvas = this.$canvas; + + Search.hide(); + + UI.setActive(this); + TabItems._update(this.tab, {force: true}); + + // Zoom in! + let tab = this.tab; + + function onZoomDone() { + $canvas.css({ 'transform': null }); + $tabEl.removeClass("front"); + + UI.goToTab(tab); + + // tab might not be selected because hideTabView() is invoked after + // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab + if (!tab.selected) { + UI.onTabSelect(gBrowser.selectedTab); + } else { + if (isNewBlankTab) + gWindow.gURLBar.focus(); + } + if (self.parent && self.parent.expanded) + self.parent.collapse(); + + self._sendToSubscribers("zoomedIn"); + } + + let animateZoom = gPrefBranch.getBoolPref("animate_zoom"); + if (animateZoom) { + let transform = this.getZoomTransform(); + TabItems.pausePainting(); + + if (this.parent && this.parent.expanded) + $tabEl.removeClass("stack-trayed"); + $tabEl.addClass("front"); + $canvas + .css({ 'transform-origin': transform.transformOrigin }) + .animate({ 'transform': transform.transform }, { + duration: 230, + easing: 'fast', + complete: function() { + onZoomDone(); + + setTimeout(function() { + TabItems.resumePainting(); + }, 0); + } + }); + } else { + setTimeout(onZoomDone, 0); + } + }, + + // ---------- + // Function: zoomOut + // Handles the zoom down animation after returning to TabView. + // It is expected that this routine will be called from the chrome thread + // + // Parameters: + // complete - a function to call after the zoom down animation + zoomOut: function TabItem_zoomOut(complete) { + let $tab = this.$container, $canvas = this.$canvas; + var self = this; + + let onZoomDone = function onZoomDone() { + $tab.removeClass("front"); + $canvas.css("transform", null); + + if (typeof complete == "function") + complete(); + }; + + UI.setActive(this); + TabItems._update(this.tab, {force: true}); + + $tab.addClass("front"); + + let animateZoom = gPrefBranch.getBoolPref("animate_zoom"); + if (animateZoom) { + // The scaleCheat of 2 here is a clever way to speed up the zoom-out + // code. See getZoomTransform() below. + let transform = this.getZoomTransform(2); + TabItems.pausePainting(); + + $canvas.css({ + 'transform': transform.transform, + 'transform-origin': transform.transformOrigin + }); + + $canvas.animate({ "transform": "scale(1.0)" }, { + duration: 300, + easing: 'cubic-bezier', // note that this is legal easing, even without parameters + complete: function() { + TabItems.resumePainting(); + onZoomDone(); + } + }); + } else { + onZoomDone(); + } + }, + + // ---------- + // Function: getZoomTransform + // Returns the transform function which represents the maximum bounds of the + // tab thumbnail in the zoom animation. + getZoomTransform: function TabItem_getZoomTransform(scaleCheat) { + // Taking the bounds of the container (as opposed to the canvas) makes us + // immune to any transformations applied to the canvas. + let { left, top, width, height, right, bottom } = this.$container.bounds(); + + let { innerWidth: windowWidth, innerHeight: windowHeight } = window; + + // The scaleCheat is a clever way to speed up the zoom-in code. + // Because image scaling is slowest on big images, we cheat and stop + // the image at scaled-down size and placed accordingly. Because the + // animation is fast, you can't see the difference but it feels a lot + // zippier. The only trick is choosing the right animation function so + // that you don't see a change in percieved animation speed from frame #1 + // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame + // of real animation). Choosing an animation that starts fast is key. + + if (!scaleCheat) + scaleCheat = 1.7; + + let zoomWidth = width + (window.innerWidth - width) / scaleCheat; + let zoomScaleFactor = zoomWidth / width; + + let zoomHeight = height * zoomScaleFactor; + let zoomTop = top * (1 - 1/scaleCheat); + let zoomLeft = left * (1 - 1/scaleCheat); + + let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100; + let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100; + + return { + transformOrigin: xOrigin + "% " + yOrigin + "%", + transform: "scale(" + zoomScaleFactor + ")" + }; + }, + + // ---------- + // Function: updateCanvas + // Updates the tabitem's canvas. + updateCanvas: function TabItem_updateCanvas() { + // ___ thumbnail + let $canvas = this.$canvas; + if (!this.canvasSizeForced) { + let w = $canvas.width(); + let h = $canvas.height(); + if (w != $canvas[0].width || h != $canvas[0].height) { + $canvas[0].width = w; + $canvas[0].height = h; + } + } + + TabItems._lastUpdateTime = Date.now(); + this._lastTabUpdateTime = TabItems._lastUpdateTime; + + if (this.tabCanvas) + this.tabCanvas.paint(); + + // ___ cache + if (this.isShowingCachedData()) + this.hideCachedData(); + } +}); + +// ########## +// Class: TabItems +// Singleton for managing s +let TabItems = { + minTabWidth: 40, + tabWidth: 160, + tabHeight: 120, + tabAspect: 0, // set in init + invTabAspect: 0, // set in init + fontSize: 9, + fontSizeRange: new Range(8,15), + _fragment: null, + items: [], + paintingPaused: 0, + _tabsWaitingForUpdate: null, + _heartbeat: null, // see explanation at startHeartbeat() below + _heartbeatTiming: 200, // milliseconds between calls + _maxTimeForUpdating: 200, // milliseconds that consecutive updates can take + _lastUpdateTime: Date.now(), + _eventListeners: [], + _pauseUpdateForTest: false, + _reconnectingPaused: false, + tabItemPadding: {}, + _mozAfterPaintHandler: null, + + // ---------- + // Function: toString + // Prints [TabItems count=count] for debug use + toString: function TabItems_toString() { + return "[TabItems count=" + this.items.length + "]"; + }, + + // ---------- + // Function: init + // Set up the necessary tracking to maintain the s. + init: function TabItems_init() { + Utils.assert(window.AllTabs, "AllTabs must be initialized first"); + let self = this; + + // Set up tab priority queue + this._tabsWaitingForUpdate = new TabPriorityQueue(); + this.minTabHeight = this.minTabWidth * this.tabHeight / this.tabWidth; + this.tabAspect = this.tabHeight / this.tabWidth; + this.invTabAspect = 1 / this.tabAspect; + + let $canvas = iQ("") + .attr('moz-opaque', ''); + $canvas.appendTo(iQ("body")); + $canvas.hide(); + + let mm = gWindow.messageManager; + this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this); + mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler); + + // When a tab is opened, create the TabItem + this._eventListeners.open = function (event) { + let tab = event.target; + + if (!tab.pinned) + self.link(tab); + } + // When a tab's content is loaded, show the canvas and hide the cached data + // if necessary. + this._eventListeners.attrModified = function (event) { + let tab = event.target; + + if (!tab.pinned) + self.update(tab); + } + // When a tab is closed, unlink. + this._eventListeners.close = function (event) { + let tab = event.target; + + // XXX bug #635975 - don't unlink the tab if the dom window is closing. + if (!tab.pinned && !UI.isDOMWindowClosing) + self.unlink(tab); + } + for (let name in this._eventListeners) { + AllTabs.register(name, this._eventListeners[name]); + } + + let activeGroupItem = GroupItems.getActiveGroupItem(); + let activeGroupItemId = activeGroupItem ? activeGroupItem.id : null; + // For each tab, create the link. + AllTabs.tabs.forEach(function (tab) { + if (tab.pinned) + return; + + let options = {immediately: true}; + // if tab is visible in the tabstrip and doesn't have any data stored in + // the session store (see TabItem__reconnect), it implies that it is a + // new tab which is created before Panorama is initialized. Therefore, + // passing the active group id to the link() method for setting it up. + if (!tab.hidden && activeGroupItemId) + options.groupItemId = activeGroupItemId; + self.link(tab, options); + self.update(tab); + }); + }, + + // ---------- + // Function: uninit + uninit: function TabItems_uninit() { + let mm = gWindow.messageManager; + mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler); + + for (let name in this._eventListeners) { + AllTabs.unregister(name, this._eventListeners[name]); + } + this.items.forEach(function(tabItem) { + delete tabItem.tab._tabViewTabItem; + + for (let x in tabItem) { + if (typeof tabItem[x] == "object") + tabItem[x] = null; + } + }); + + this.items = null; + this._eventListeners = null; + this._lastUpdateTime = null; + this._tabsWaitingForUpdate.clear(); + }, + + // ---------- + // Function: fragment + // Return a DocumentFragment which has a single
child. This child node + // will act as a template for all TabItem containers. + // The first call of this function caches the DocumentFragment in _fragment. + fragment: function TabItems_fragment() { + if (this._fragment) + return this._fragment; + + let div = document.createElement("div"); + div.classList.add("tab"); + div.innerHTML = "
" + + "
" + + "
" + + " " + + "
"; + this._fragment = document.createDocumentFragment(); + this._fragment.appendChild(div); + + return this._fragment; + }, + + // Function: _isComplete + // Checks whether the xul:tab has fully loaded and calls a callback with a + // boolean indicates whether the tab is loaded or not. + _isComplete: function TabItems__isComplete(tab, callback) { + Utils.assertThrow(tab, "tab"); + + // A pending tab can't be complete, yet. + if (tab.hasAttribute("pending")) { + setTimeout(() => callback(false)); + return; + } + + let mm = tab.linkedBrowser.messageManager; + let message = "Panorama:isDocumentLoaded"; + + mm.addMessageListener(message, function onMessage(cx) { + mm.removeMessageListener(cx.name, onMessage); + callback(cx.json.isLoaded); + }); + mm.sendAsyncMessage(message); + }, + + // ---------- + // Function: onMozAfterPaint + // Called when a web page is painted. + onMozAfterPaint: function TabItems_onMozAfterPaint(cx) { + let index = gBrowser.browsers.indexOf(cx.target); + if (index == -1) + return; + + let tab = gBrowser.tabs[index]; + if (!tab.pinned) + this.update(tab); + }, + + // ---------- + // Function: update + // Takes in a xul:tab. + update: function TabItems_update(tab) { + try { + Utils.assertThrow(tab, "tab"); + Utils.assertThrow(!tab.pinned, "shouldn't be an app tab"); + Utils.assertThrow(tab._tabViewTabItem, "should already be linked"); + + let shouldDefer = ( + this.isPaintingPaused() || + this._tabsWaitingForUpdate.hasItems() || + Date.now() - this._lastUpdateTime < this._heartbeatTiming + ); + + if (shouldDefer) { + this._tabsWaitingForUpdate.push(tab); + this.startHeartbeat(); + } else + this._update(tab); + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // Function: _update + // Takes in a xul:tab. + // + // Parameters: + // tab - a xul tab to update + // options - an object with additional parameters, see below + // + // Possible options: + // force - true to always update the tab item even if it's incomplete + _update: function TabItems__update(tab, options) { + try { + if (this._pauseUpdateForTest) + return; + + Utils.assertThrow(tab, "tab"); + + // ___ get the TabItem + Utils.assertThrow(tab._tabViewTabItem, "must already be linked"); + let tabItem = tab._tabViewTabItem; + + // Even if the page hasn't loaded, display the favicon and title + // ___ icon + FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) { + let favImage = tabItem.$favImage[0]; + let fav = tabItem.$fav; + if (iconUrl) { + if (favImage.src != iconUrl) + favImage.src = iconUrl; + fav.show(); + } else { + if (favImage.hasAttribute("src")) + favImage.removeAttribute("src"); + fav.hide(); + } + tabItem._sendToSubscribers("iconUpdated"); + }); + + // ___ label + let label = tab.label; + let $name = tabItem.$tabTitle; + if ($name.text() != label) + $name.text(label); + + // ___ remove from waiting list now that we have no other + // early returns + this._tabsWaitingForUpdate.remove(tab); + + // ___ URL + let tabUrl = tab.linkedBrowser.currentURI.spec; + let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl); + tabItem.$container.attr("title", tooltip); + + // ___ Make sure the tab is complete and ready for updating. + if (options && options.force) { + tabItem.updateCanvas(); + tabItem._sendToSubscribers("updated"); + } else { + this._isComplete(tab, function TabItems__update_isComplete(isComplete) { + if (!Utils.isValidXULTab(tab) || tab.pinned) + return; + + if (isComplete) { + tabItem.updateCanvas(); + tabItem._sendToSubscribers("updated"); + } else { + this._tabsWaitingForUpdate.push(tab); + } + }.bind(this)); + } + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // Function: link + // Takes in a xul:tab, creates a TabItem for it and adds it to the scene. + link: function TabItems_link(tab, options) { + try { + Utils.assertThrow(tab, "tab"); + Utils.assertThrow(!tab.pinned, "shouldn't be an app tab"); + Utils.assertThrow(!tab._tabViewTabItem, "shouldn't already be linked"); + new TabItem(tab, options); // sets tab._tabViewTabItem to itself + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // Function: unlink + // Takes in a xul:tab and destroys the TabItem associated with it. + unlink: function TabItems_unlink(tab) { + try { + Utils.assertThrow(tab, "tab"); + Utils.assertThrow(tab._tabViewTabItem, "should already be linked"); + // note that it's ok to unlink an app tab; see .handleTabUnpin + + this.unregister(tab._tabViewTabItem); + tab._tabViewTabItem._sendToSubscribers("close"); + tab._tabViewTabItem.$container.remove(); + tab._tabViewTabItem.removeTrenches(); + Items.unsquish(null, tab._tabViewTabItem); + + tab._tabViewTabItem.tab = null; + tab._tabViewTabItem.tabCanvas.tab = null; + tab._tabViewTabItem.tabCanvas = null; + tab._tabViewTabItem = null; + Storage.saveTab(tab, null); + + this._tabsWaitingForUpdate.remove(tab); + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // when a tab becomes pinned, destroy its TabItem + handleTabPin: function TabItems_handleTabPin(xulTab) { + this.unlink(xulTab); + }, + + // ---------- + // when a tab becomes unpinned, create a TabItem for it + handleTabUnpin: function TabItems_handleTabUnpin(xulTab) { + this.link(xulTab); + this.update(xulTab); + }, + + // ---------- + // Function: startHeartbeat + // Start a new heartbeat if there isn't one already started. + // The heartbeat is a chain of setTimeout calls that allows us to spread + // out update calls over a period of time. + // _heartbeat is used to make sure that we don't add multiple + // setTimeout chains. + startHeartbeat: function TabItems_startHeartbeat() { + if (!this._heartbeat) { + let self = this; + this._heartbeat = setTimeout(function() { + self._checkHeartbeat(); + }, this._heartbeatTiming); + } + }, + + // ---------- + // Function: _checkHeartbeat + // This periodically checks for tabs waiting to be updated, and calls + // _update on them. + // Should only be called by startHeartbeat and resumePainting. + _checkHeartbeat: function TabItems__checkHeartbeat() { + this._heartbeat = null; + + if (this.isPaintingPaused()) + return; + + // restart the heartbeat to update all waiting tabs once the UI becomes idle + if (!UI.isIdle()) { + this.startHeartbeat(); + return; + } + + let accumTime = 0; + let items = this._tabsWaitingForUpdate.getItems(); + // Do as many updates as we can fit into a "perceived" amount + // of time, which is tunable. + while (accumTime < this._maxTimeForUpdating && items.length) { + let updateBegin = Date.now(); + this._update(items.pop()); + let updateEnd = Date.now(); + + // Maintain a simple average of time for each tabitem update + // We can use this as a base by which to delay things like + // tab zooming, so there aren't any hitches. + let deltaTime = updateEnd - updateBegin; + accumTime += deltaTime; + } + + if (this._tabsWaitingForUpdate.hasItems()) + this.startHeartbeat(); + }, + + // ---------- + // Function: pausePainting + // Tells TabItems to stop updating thumbnails (so you can do + // animations without thumbnail paints causing stutters). + // pausePainting can be called multiple times, but every call to + // pausePainting needs to be mirrored with a call to . + pausePainting: function TabItems_pausePainting() { + this.paintingPaused++; + if (this._heartbeat) { + clearTimeout(this._heartbeat); + this._heartbeat = null; + } + }, + + // ---------- + // Function: resumePainting + // Undoes a call to . For instance, if you called + // pausePainting three times in a row, you'll need to call resumePainting + // three times before TabItems will start updating thumbnails again. + resumePainting: function TabItems_resumePainting() { + this.paintingPaused--; + Utils.assert(this.paintingPaused > -1, "paintingPaused should not go below zero"); + if (!this.isPaintingPaused()) + this.startHeartbeat(); + }, + + // ---------- + // Function: isPaintingPaused + // Returns a boolean indicating whether painting + // is paused or not. + isPaintingPaused: function TabItems_isPaintingPaused() { + return this.paintingPaused > 0; + }, + + // ---------- + // Function: pauseReconnecting + // Don't reconnect any new tabs until resume is called. + pauseReconnecting: function TabItems_pauseReconnecting() { + Utils.assertThrow(!this._reconnectingPaused, "shouldn't already be paused"); + + this._reconnectingPaused = true; + }, + + // ---------- + // Function: resumeReconnecting + // Reconnect all of the tabs that were created since we paused. + resumeReconnecting: function TabItems_resumeReconnecting() { + Utils.assertThrow(this._reconnectingPaused, "should already be paused"); + + this._reconnectingPaused = false; + this.items.forEach(function(item) { + if (!item._reconnected) + item._reconnect(); + }); + }, + + // ---------- + // Function: reconnectingPaused + // Returns true if reconnecting is paused. + reconnectingPaused: function TabItems_reconnectingPaused() { + return this._reconnectingPaused; + }, + + // ---------- + // Function: register + // Adds the given to the master list. + register: function TabItems_register(item) { + Utils.assert(item && item.isAnItem, 'item must be a TabItem'); + Utils.assert(this.items.indexOf(item) == -1, 'only register once per item'); + this.items.push(item); + }, + + // ---------- + // Function: unregister + // Removes the given from the master list. + unregister: function TabItems_unregister(item) { + var index = this.items.indexOf(item); + if (index != -1) + this.items.splice(index, 1); + }, + + // ---------- + // Function: getItems + // Returns a copy of the master array of s. + getItems: function TabItems_getItems() { + return Utils.copy(this.items); + }, + + // ---------- + // Function: saveAll + // Saves all open s. + saveAll: function TabItems_saveAll() { + let tabItems = this.getItems(); + + tabItems.forEach(function TabItems_saveAll_forEach(tabItem) { + tabItem.save(); + }); + }, + + // ---------- + // Function: storageSanity + // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage) + // and returns true if it looks valid. + // TODO: this is a stub, please implement + storageSanity: function TabItems_storageSanity(data) { + return true; + }, + + // ---------- + // Function: getFontSizeFromWidth + // Private method that returns the fontsize to use given the tab's width + getFontSizeFromWidth: function TabItem_getFontSizeFromWidth(width) { + let widthRange = new Range(0, TabItems.tabWidth); + let proportion = widthRange.proportion(width - TabItems.tabItemPadding.x, true); + // proportion is in [0,1] + return TabItems.fontSizeRange.scale(proportion); + }, + + // ---------- + // Function: _getWidthForHeight + // Private method that returns the tabitem width given a height. + _getWidthForHeight: function TabItems__getWidthForHeight(height) { + return height * TabItems.invTabAspect; + }, + + // ---------- + // Function: _getHeightForWidth + // Private method that returns the tabitem height given a width. + _getHeightForWidth: function TabItems__getHeightForWidth(width) { + return width * TabItems.tabAspect; + }, + + // ---------- + // Function: calcValidSize + // Pass in a desired size, and receive a size based on proper title + // size and aspect ratio. + calcValidSize: function TabItems_calcValidSize(size, options) { + Utils.assert(Utils.isPoint(size), 'input is a Point'); + + let width = Math.max(TabItems.minTabWidth, size.x); + let showTitle = !options || !options.hideTitle; + let titleSize = showTitle ? TabItems.fontSizeRange.max : 0; + let height = Math.max(TabItems.minTabHeight, size.y - titleSize); + let retSize = new Point(width, height); + + if (size.x > -1) + retSize.y = this._getHeightForWidth(width); + if (size.y > -1) + retSize.x = this._getWidthForHeight(height); + + if (size.x > -1 && size.y > -1) { + if (retSize.x < size.x) + retSize.y = this._getHeightForWidth(retSize.x); + else + retSize.x = this._getWidthForHeight(retSize.y); + } + + if (showTitle) + retSize.y += titleSize; + + return retSize; + } +}; + +// ########## +// Class: TabPriorityQueue +// Container that returns tab items in a priority order +// Current implementation assigns tab to either a high priority +// or low priority queue, and toggles which queue items are popped +// from. This guarantees that high priority items which are constantly +// being added will not eclipse changes for lower priority items. +function TabPriorityQueue() { +}; + +TabPriorityQueue.prototype = { + _low: [], // low priority queue + _high: [], // high priority queue + + // ---------- + // Function: toString + // Prints [TabPriorityQueue count=count] for debug use + toString: function TabPriorityQueue_toString() { + return "[TabPriorityQueue count=" + (this._low.length + this._high.length) + "]"; + }, + + // ---------- + // Function: clear + // Empty the update queue + clear: function TabPriorityQueue_clear() { + this._low = []; + this._high = []; + }, + + // ---------- + // Function: hasItems + // Return whether pending items exist + hasItems: function TabPriorityQueue_hasItems() { + return (this._low.length > 0) || (this._high.length > 0); + }, + + // ---------- + // Function: getItems + // Returns all queued items, ordered from low to high priority + getItems: function TabPriorityQueue_getItems() { + return this._low.concat(this._high); + }, + + // ---------- + // Function: push + // Add an item to be prioritized + push: function TabPriorityQueue_push(tab) { + // Push onto correct priority queue. + // It's only low priority if it's in a stack, and isn't the top, + // and the stack isn't expanded. + // If it already exists in the destination queue, + // leave it. If it exists in a different queue, remove it first and push + // onto new queue. + let item = tab._tabViewTabItem; + if (item.parent && (item.parent.isStacked() && + !item.parent.isTopOfStack(item) && + !item.parent.expanded)) { + let idx = this._high.indexOf(tab); + if (idx != -1) { + this._high.splice(idx, 1); + this._low.unshift(tab); + } else if (this._low.indexOf(tab) == -1) + this._low.unshift(tab); + } else { + let idx = this._low.indexOf(tab); + if (idx != -1) { + this._low.splice(idx, 1); + this._high.unshift(tab); + } else if (this._high.indexOf(tab) == -1) + this._high.unshift(tab); + } + }, + + // ---------- + // Function: pop + // Remove and return the next item in priority order + pop: function TabPriorityQueue_pop() { + let ret = null; + if (this._high.length) + ret = this._high.pop(); + else if (this._low.length) + ret = this._low.pop(); + return ret; + }, + + // ---------- + // Function: peek + // Return the next item in priority order, without removing it + peek: function TabPriorityQueue_peek() { + let ret = null; + if (this._high.length) + ret = this._high[this._high.length-1]; + else if (this._low.length) + ret = this._low[this._low.length-1]; + return ret; + }, + + // ---------- + // Function: remove + // Remove the passed item + remove: function TabPriorityQueue_remove(tab) { + let index = this._high.indexOf(tab); + if (index != -1) + this._high.splice(index, 1); + else { + index = this._low.indexOf(tab); + if (index != -1) + this._low.splice(index, 1); + } + } +}; + +// ########## +// Class: TabCanvas +// Takes care of the actual canvas for the tab thumbnail +// Does not need to be accessed from outside of tabitems.js +function TabCanvas(tab, canvas) { + this.tab = tab; + this.canvas = canvas; +}; + +TabCanvas.prototype = Utils.extend(new Subscribable(), { + // ---------- + // Function: toString + // Prints [TabCanvas (tab)] for debug use + toString: function TabCanvas_toString() { + return "[TabCanvas (" + this.tab + ")]"; + }, + + // ---------- + // Function: paint + paint: function TabCanvas_paint(evt) { + var w = this.canvas.width; + var h = this.canvas.height; + if (!w || !h) + return; + + if (!this.tab.linkedBrowser.contentWindow) { + Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()'); + return; + } + + let win = this.tab.linkedBrowser.contentWindow; + gPageThumbnails.captureToCanvas(win, this.canvas); + + this._sendToSubscribers("painted"); + }, + + // ---------- + // Function: toImageData + toImageData: function TabCanvas_toImageData() { + return this.canvas.toDataURL("image/png"); + } +});