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