1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 // An <Item> that represents a tab. Also implements the <Subscribable> interface.
14 function TabItem(tab, options) {
15 Utils.assert(tab, "tab");
18 // register this as the tab's tabItem
19 this.tab._tabViewTabItem = this;
25 document.body.appendChild(TabItems.fragment().cloneNode(true));
27 // The document fragment contains just one Node
28 // As per DOM3 appendChild: it will then be the last child
29 let div = document.body.lastChild;
32 this._showsCachedData = false;
33 this.canvasSizeForced = false;
34 this.$thumb = iQ('.thumb', $div);
35 this.$fav = iQ('.favicon', $div);
36 this.$tabTitle = iQ('.tab-title', $div);
37 this.$canvas = iQ('.thumb canvas', $div);
38 this.$cachedThumb = iQ('img.cached-thumb', $div);
39 this.$favImage = iQ('.favicon>img', $div);
40 this.$close = iQ('.close', $div);
42 this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
45 this.isATabItem = true;
46 this.keepProportional = true;
47 this._hasBeenDrawn = false;
48 this._reconnected = false;
49 this.isDragging = false;
50 this.isStacked = false;
52 // Read off the total vertical and horizontal padding on the tab container
53 // and cache this value, as it must be the same for every TabItem.
54 if (Utils.isEmptyObject(TabItems.tabItemPadding)) {
55 TabItems.tabItemPadding.x = parseInt($div.css('padding-left'))
56 + parseInt($div.css('padding-right'));
58 TabItems.tabItemPadding.y = parseInt($div.css('padding-top'))
59 + parseInt($div.css('padding-bottom'));
62 this.bounds = new Rect(0,0,1,1);
64 this._lastTabUpdateTime = Date.now();
66 // ___ superclass setup
70 // override dropOptions with custom tabitem methods
71 this.dropOptions.drop = function(e) {
72 let groupItem = drag.info.item.parent;
73 groupItem.add(drag.info.$el);
81 $div.mousedown(function(e) {
82 if (!Utils.isRightClick(e))
83 self.lastMouseDownTarget = e.target;
86 $div.mouseup(function(e) {
87 var same = (e.target == self.lastMouseDownTarget);
88 self.lastMouseDownTarget = null;
92 // press close button or middle mouse click
93 if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) {
94 self.closedManually = true;
97 if (!Items.item(this).isDragging)
102 this.droppable(true);
104 this.$close.attr("title", tabbrowserString("tabs.closeTab"));
106 TabItems.register(this);
108 // ___ reconnect to data from Storage
109 if (!TabItems.reconnectingPaused())
110 this._reconnect(options);
113 TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
115 // Function: toString
116 // Prints [TabItem (tab)] for debug use
117 toString: function TabItem_toString() {
118 return "[TabItem (" + this.tab + ")]";
122 // Function: forceCanvasSize
123 // Repaints the thumbnail with the given resolution, and forces it
124 // to stay that resolution until unforceCanvasSize is called.
125 forceCanvasSize: function TabItem_forceCanvasSize(w, h) {
126 this.canvasSizeForced = true;
127 this.$canvas[0].width = w;
128 this.$canvas[0].height = h;
129 this.tabCanvas.paint();
133 // Function: unforceCanvasSize
134 // Stops holding the thumbnail resolution; allows it to shift to the
135 // size of thumbnail on screen. Note that this call does not nest, unlike
136 // <TabItems.resumePainting>; if you call forceCanvasSize multiple
137 // times, you just need a single unforce to clear them all.
138 unforceCanvasSize: function TabItem_unforceCanvasSize() {
139 this.canvasSizeForced = false;
143 // Function: isShowingCachedData
144 // Returns a boolean indicates whether the cached data is being displayed or
146 isShowingCachedData: function TabItem_isShowingCachedData() {
147 return this._showsCachedData;
151 // Function: showCachedData
152 // Shows the cached data i.e. image and title. Note: this method should only
153 // be called at browser startup with the cached data avaliable.
154 showCachedData: function TabItem_showCachedData() {
155 let {title, url} = this.getTabState();
156 let thumbnailURL = gPageThumbnails.getThumbnailURL(url);
158 this.$cachedThumb.attr("src", thumbnailURL).show();
159 this.$canvas.css({opacity: 0});
161 let tooltip = (title && title != url ? title + "\n" + url : url);
162 this.$tabTitle.text(title).attr("title", tooltip);
163 this._showsCachedData = true;
167 // Function: hideCachedData
168 // Hides the cached data i.e. image and title and show the canvas.
169 hideCachedData: function TabItem_hideCachedData() {
170 this.$cachedThumb.attr("src", "").hide();
171 this.$canvas.css({opacity: 1.0});
172 this._showsCachedData = false;
176 // Function: getStorageData
177 // Get data to be used for persistent storage of this object.
178 getStorageData: function TabItem_getStorageData() {
180 groupID: (this.parent ? this.parent.id : 0)
182 if (this.parent && this.parent.getActiveTab() == this)
190 // Store persistent for this object.
191 save: function TabItem_save() {
193 if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save
196 let data = this.getStorageData();
197 if (TabItems.storageSanity(data))
198 Storage.saveTab(this.tab, data);
200 Utils.log("Error in saving tab value: "+e);
205 // Function: _getCurrentTabStateEntry
206 // Returns the current tab state's active history entry.
207 _getCurrentTabStateEntry: function TabItem__getCurrentTabStateEntry() {
208 let tabState = Storage.getTabState(this.tab);
211 let index = (tabState.index || tabState.entries.length) - 1;
212 if (index in tabState.entries)
213 return tabState.entries[index];
220 // Function: getTabState
221 // Returns the current tab state, i.e. the title and URL of the active
223 getTabState: function TabItem_getTabState() {
224 let entry = this._getCurrentTabStateEntry();
234 url = this.tab.linkedBrowser.currentURI.spec;
237 return {title: title, url: url};
241 // Function: _reconnect
242 // Load the reciever's persistent data from storage. If there is none,
243 // treats it as a new tab.
246 // options - an object with additional parameters, see below
249 // groupItemId - if the tab doesn't have any data associated with it and
250 // groupItemId is available, add the tab to that group.
251 _reconnect: function TabItem__reconnect(options) {
252 Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
253 Utils.assertThrow(this.tab, "should have a xul:tab");
255 let tabData = Storage.getTabData(this.tab);
258 if (tabData && TabItems.storageSanity(tabData)) {
259 // Show the cached data while we're waiting for the tabItem to be updated.
260 // If the tab isn't restored yet this acts as a placeholder until it is.
261 this.showCachedData();
264 this.parent.remove(this, {immediately: true});
267 groupItem = GroupItems.groupItem(tabData.groupID);
269 groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds});
272 groupItem.add(this, {immediately: true});
274 // restore the active tab for each group between browser sessions
276 groupItem.setActiveTab(this);
278 // if it matches the selected tab or no active tab and the browser
279 // tab is hidden, the active group item would be set.
280 if (this.tab.selected ||
281 (!GroupItems.getActiveGroupItem() && !this.tab.hidden))
282 UI.setActive(this.parent);
285 if (options && options.groupItemId)
286 groupItem = GroupItems.groupItem(options.groupItemId);
289 groupItem.add(this, {immediately: true});
291 // create tab group by double click is handled in UI_init().
292 GroupItems.newTab(this, {immediately: true});
296 this._reconnected = true;
298 this._sendToSubscribers("reconnected");
302 // Function: setHidden
303 // Hide/unhide this item
304 setHidden: function TabItem_setHidden(val) {
306 this.addClass("tabHidden");
308 this.removeClass("tabHidden");
313 // Function: getHidden
314 // Return hide state of item
315 getHidden: function TabItem_getHidden() {
320 // Function: setBounds
321 // Moves this item to the specified location and size.
324 // rect - a <Rect> giving the new bounds
325 // immediately - true if it should not animate; default false
326 // options - an object with additional parameters, see below
329 // force - true to always update the DOM even if the bounds haven't changed; default false
330 setBounds: function TabItem_setBounds(inRect, immediately, options) {
331 Utils.assert(Utils.isRect(inRect), 'TabItem.setBounds: rect is not a real rectangle!');
336 // force the input size to be valid
337 let validSize = TabItems.calcValidSize(
338 new Point(inRect.width, inRect.height),
339 {hideTitle: (this.isStacked || options.hideTitle === true)});
340 let rect = new Rect(inRect.left, inRect.top,
341 validSize.x, validSize.y);
345 if (rect.left != this.bounds.left || options.force)
346 css.left = rect.left;
348 if (rect.top != this.bounds.top || options.force)
351 if (rect.width != this.bounds.width || options.force) {
352 css.width = rect.width - TabItems.tabItemPadding.x;
353 css.fontSize = TabItems.getFontSizeFromWidth(rect.width);
354 css.fontSize += 'px';
357 if (rect.height != this.bounds.height || options.force) {
358 css.height = rect.height - TabItems.tabItemPadding.y;
360 css.height -= TabItems.fontSizeRange.max;
363 if (Utils.isEmptyObject(css))
366 this.bounds.copy(rect);
368 // If this is a brand new tab don't animate it in from
369 // a random location (i.e., from [0,0]). Instead, just
370 // have it appear where it should be.
371 if (immediately || (!this._hasBeenDrawn)) {
372 this.$container.css(css);
374 TabItems.pausePainting();
375 this.$container.animate(css, {
377 easing: "tabviewBounce",
378 complete: function() {
379 TabItems.resumePainting();
384 if (css.fontSize && !(this.parent && this.parent.isStacked())) {
385 if (css.fontSize < TabItems.fontSizeRange.min)
386 immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut();
388 immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn();
392 TabItems.update(this.tab);
394 let widthRange, proportion;
396 if (this.parent && this.parent.isStacked()) {
398 this.$fav.css({top:0, right:0});
400 this.$fav.css({top:0, left:0});
402 widthRange = new Range(70, 90);
403 proportion = widthRange.proportion(css.width); // between 0 and 1
406 this.$fav.css({top:4, right:2});
408 this.$fav.css({top:4, left:4});
410 widthRange = new Range(40, 45);
411 proportion = widthRange.proportion(css.width); // between 0 and 1
414 if (proportion <= .1)
417 this.$close.show().css({opacity:proportion});
419 var pad = 1 + 5 * proportion;
420 var alphaRange = new Range(0.1,0.2);
422 "-moz-padding-start": pad + "px",
423 "-moz-padding-end": pad + 2 + "px",
424 "padding-top": pad + "px",
425 "padding-bottom": pad + "px",
426 "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")",
430 this._hasBeenDrawn = true;
432 UI.clearShouldResizeItems();
434 rect = this.getBounds(); // ensure that it's a <Rect>
436 Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!');
438 if (!this.parent && Utils.isValidXULTab(this.tab))
439 this.setTrenches(rect);
446 // Sets the z-index for this item.
447 setZ: function TabItem_setZ(value) {
449 this.$container.css({zIndex: value});
454 // Closes this item (actually closes the tab associated with it, which automatically
457 // groupClose - true if this method is called by group close action.
458 // Returns true if this tab is removed.
459 close: function TabItem_close(groupClose) {
460 // When the last tab is closed, put a new tab into closing tab's group. If
461 // closing tab doesn't belong to a group and no empty group, create a new
462 // one for the new tab.
463 if (!groupClose && gBrowser.tabs.length == 1) {
464 let group = this.tab._tabViewTabItem.parent;
465 group.newTab(null, { closedLastTab: true });
468 // when "TabClose" event is fired, the browser tab is about to close and our
469 // item "close" is fired before the browser tab actually get closed.
470 // Therefore, we need "tabRemoved" event below.
471 gBrowser.removeTab(this.tab);
472 let tabClosed = !this.tab;
475 this._sendToSubscribers("tabRemoved");
477 // No need to explicitly delete the tab data, becasue sessionstore data
478 // associated with the tab will automatically go away
483 // Function: addClass
484 // Adds the specified CSS class to this item's container DOM element.
485 addClass: function TabItem_addClass(className) {
486 this.$container.addClass(className);
490 // Function: removeClass
491 // Removes the specified CSS class from this item's container DOM element.
492 removeClass: function TabItem_removeClass(className) {
493 this.$container.removeClass(className);
497 // Function: makeActive
498 // Updates this item to visually indicate that it's active.
499 makeActive: function TabItem_makeActive() {
500 this.$container.addClass("focus");
503 this.parent.setActiveTab(this);
507 // Function: makeDeactive
508 // Updates this item to visually indicate that it's not active.
509 makeDeactive: function TabItem_makeDeactive() {
510 this.$container.removeClass("focus");
515 // Allows you to select the tab and zoom in on it, thereby bringing you
516 // to the tab in Firefox to interact with.
518 // isNewBlankTab - boolean indicates whether it is a newly opened blank tab.
519 zoomIn: function TabItem_zoomIn(isNewBlankTab) {
520 // don't allow zoom in if its group is hidden
521 if (this.parent && this.parent.hidden)
525 let $tabEl = this.$container;
526 let $canvas = this.$canvas;
531 TabItems._update(this.tab, {force: true});
536 function onZoomDone() {
537 $canvas.css({ 'transform': null });
538 $tabEl.removeClass("front");
542 // tab might not be selected because hideTabView() is invoked after
543 // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab
545 UI.onTabSelect(gBrowser.selectedTab);
548 gWindow.gURLBar.focus();
550 if (self.parent && self.parent.expanded)
551 self.parent.collapse();
553 self._sendToSubscribers("zoomedIn");
556 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
558 let transform = this.getZoomTransform();
559 TabItems.pausePainting();
561 if (this.parent && this.parent.expanded)
562 $tabEl.removeClass("stack-trayed");
563 $tabEl.addClass("front");
565 .css({ 'transform-origin': transform.transformOrigin })
566 .animate({ 'transform': transform.transform }, {
569 complete: function() {
572 setTimeout(function() {
573 TabItems.resumePainting();
578 setTimeout(onZoomDone, 0);
584 // Handles the zoom down animation after returning to TabView.
585 // It is expected that this routine will be called from the chrome thread
588 // complete - a function to call after the zoom down animation
589 zoomOut: function TabItem_zoomOut(complete) {
590 let $tab = this.$container, $canvas = this.$canvas;
593 let onZoomDone = function onZoomDone() {
594 $tab.removeClass("front");
595 $canvas.css("transform", null);
597 if (typeof complete == "function")
602 TabItems._update(this.tab, {force: true});
604 $tab.addClass("front");
606 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
608 // The scaleCheat of 2 here is a clever way to speed up the zoom-out
609 // code. See getZoomTransform() below.
610 let transform = this.getZoomTransform(2);
611 TabItems.pausePainting();
614 'transform': transform.transform,
615 'transform-origin': transform.transformOrigin
618 $canvas.animate({ "transform": "scale(1.0)" }, {
620 easing: 'cubic-bezier', // note that this is legal easing, even without parameters
621 complete: function() {
622 TabItems.resumePainting();
632 // Function: getZoomTransform
633 // Returns the transform function which represents the maximum bounds of the
634 // tab thumbnail in the zoom animation.
635 getZoomTransform: function TabItem_getZoomTransform(scaleCheat) {
636 // Taking the bounds of the container (as opposed to the canvas) makes us
637 // immune to any transformations applied to the canvas.
638 let { left, top, width, height, right, bottom } = this.$container.bounds();
640 let { innerWidth: windowWidth, innerHeight: windowHeight } = window;
642 // The scaleCheat is a clever way to speed up the zoom-in code.
643 // Because image scaling is slowest on big images, we cheat and stop
644 // the image at scaled-down size and placed accordingly. Because the
645 // animation is fast, you can't see the difference but it feels a lot
646 // zippier. The only trick is choosing the right animation function so
647 // that you don't see a change in percieved animation speed from frame #1
648 // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame
649 // of real animation). Choosing an animation that starts fast is key.
654 let zoomWidth = width + (window.innerWidth - width) / scaleCheat;
655 let zoomScaleFactor = zoomWidth / width;
657 let zoomHeight = height * zoomScaleFactor;
658 let zoomTop = top * (1 - 1/scaleCheat);
659 let zoomLeft = left * (1 - 1/scaleCheat);
661 let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100;
662 let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100;
665 transformOrigin: xOrigin + "% " + yOrigin + "%",
666 transform: "scale(" + zoomScaleFactor + ")"
671 // Function: updateCanvas
672 // Updates the tabitem's canvas.
673 updateCanvas: function TabItem_updateCanvas() {
675 let $canvas = this.$canvas;
676 if (!this.canvasSizeForced) {
677 let w = $canvas.width();
678 let h = $canvas.height();
679 if (w != $canvas[0].width || h != $canvas[0].height) {
680 $canvas[0].width = w;
681 $canvas[0].height = h;
685 TabItems._lastUpdateTime = Date.now();
686 this._lastTabUpdateTime = TabItems._lastUpdateTime;
689 this.tabCanvas.paint();
692 if (this.isShowingCachedData())
693 this.hideCachedData();
699 // Singleton for managing <TabItem>s
704 tabAspect: 0, // set in init
705 invTabAspect: 0, // set in init
707 fontSizeRange: new Range(8,15),
711 _tabsWaitingForUpdate: null,
712 _heartbeat: null, // see explanation at startHeartbeat() below
713 _heartbeatTiming: 200, // milliseconds between calls
714 _maxTimeForUpdating: 200, // milliseconds that consecutive updates can take
715 _lastUpdateTime: Date.now(),
717 _pauseUpdateForTest: false,
718 _reconnectingPaused: false,
720 _mozAfterPaintHandler: null,
723 // Function: toString
724 // Prints [TabItems count=count] for debug use
725 toString: function TabItems_toString() {
726 return "[TabItems count=" + this.items.length + "]";
731 // Set up the necessary tracking to maintain the <TabItems>s.
732 init: function TabItems_init() {
733 Utils.assert(window.AllTabs, "AllTabs must be initialized first");
736 // Set up tab priority queue
737 this._tabsWaitingForUpdate = new TabPriorityQueue();
738 this.minTabHeight = this.minTabWidth * this.tabHeight / this.tabWidth;
739 this.tabAspect = this.tabHeight / this.tabWidth;
740 this.invTabAspect = 1 / this.tabAspect;
742 let $canvas = iQ("<canvas>")
743 .attr('moz-opaque', '');
744 $canvas.appendTo(iQ("body"));
747 let mm = gWindow.messageManager;
748 this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this);
749 mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
751 // When a tab is opened, create the TabItem
752 this._eventListeners.open = function (event) {
753 let tab = event.target;
758 // When a tab's content is loaded, show the canvas and hide the cached data
760 this._eventListeners.attrModified = function (event) {
761 let tab = event.target;
766 // When a tab is closed, unlink.
767 this._eventListeners.close = function (event) {
768 let tab = event.target;
770 // XXX bug #635975 - don't unlink the tab if the dom window is closing.
771 if (!tab.pinned && !UI.isDOMWindowClosing)
774 for (let name in this._eventListeners) {
775 AllTabs.register(name, this._eventListeners[name]);
778 let activeGroupItem = GroupItems.getActiveGroupItem();
779 let activeGroupItemId = activeGroupItem ? activeGroupItem.id : null;
780 // For each tab, create the link.
781 AllTabs.tabs.forEach(function (tab) {
785 let options = {immediately: true};
786 // if tab is visible in the tabstrip and doesn't have any data stored in
787 // the session store (see TabItem__reconnect), it implies that it is a
788 // new tab which is created before Panorama is initialized. Therefore,
789 // passing the active group id to the link() method for setting it up.
790 if (!tab.hidden && activeGroupItemId)
791 options.groupItemId = activeGroupItemId;
792 self.link(tab, options);
799 uninit: function TabItems_uninit() {
800 let mm = gWindow.messageManager;
801 mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
803 for (let name in this._eventListeners) {
804 AllTabs.unregister(name, this._eventListeners[name]);
806 this.items.forEach(function(tabItem) {
807 delete tabItem.tab._tabViewTabItem;
809 for (let x in tabItem) {
810 if (typeof tabItem[x] == "object")
816 this._eventListeners = null;
817 this._lastUpdateTime = null;
818 this._tabsWaitingForUpdate.clear();
822 // Function: fragment
823 // Return a DocumentFragment which has a single <div> child. This child node
824 // will act as a template for all TabItem containers.
825 // The first call of this function caches the DocumentFragment in _fragment.
826 fragment: function TabItems_fragment() {
828 return this._fragment;
830 let div = document.createElement("div");
831 div.classList.add("tab");
832 div.innerHTML = "<div class='thumb'>" +
833 "<img class='cached-thumb' style='display:none'/><canvas moz-opaque/></div>" +
834 "<div class='favicon'><img/></div>" +
835 "<span class='tab-title'> </span>" +
836 "<div class='close'></div>";
837 this._fragment = document.createDocumentFragment();
838 this._fragment.appendChild(div);
840 return this._fragment;
843 // Function: _isComplete
844 // Checks whether the xul:tab has fully loaded and calls a callback with a
845 // boolean indicates whether the tab is loaded or not.
846 _isComplete: function TabItems__isComplete(tab, callback) {
847 Utils.assertThrow(tab, "tab");
849 // A pending tab can't be complete, yet.
850 if (tab.hasAttribute("pending")) {
851 setTimeout(() => callback(false));
855 let mm = tab.linkedBrowser.messageManager;
856 let message = "Panorama:isDocumentLoaded";
858 mm.addMessageListener(message, function onMessage(cx) {
859 mm.removeMessageListener(cx.name, onMessage);
860 callback(cx.json.isLoaded);
862 mm.sendAsyncMessage(message);
866 // Function: onMozAfterPaint
867 // Called when a web page is painted.
868 onMozAfterPaint: function TabItems_onMozAfterPaint(cx) {
869 let index = gBrowser.browsers.indexOf(cx.target);
873 let tab = gBrowser.tabs[index];
880 // Takes in a xul:tab.
881 update: function TabItems_update(tab) {
883 Utils.assertThrow(tab, "tab");
884 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
885 Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
888 this.isPaintingPaused() ||
889 this._tabsWaitingForUpdate.hasItems() ||
890 Date.now() - this._lastUpdateTime < this._heartbeatTiming
894 this._tabsWaitingForUpdate.push(tab);
895 this.startHeartbeat();
905 // Takes in a xul:tab.
908 // tab - a xul tab to update
909 // options - an object with additional parameters, see below
912 // force - true to always update the tab item even if it's incomplete
913 _update: function TabItems__update(tab, options) {
915 if (this._pauseUpdateForTest)
918 Utils.assertThrow(tab, "tab");
920 // ___ get the TabItem
921 Utils.assertThrow(tab._tabViewTabItem, "must already be linked");
922 let tabItem = tab._tabViewTabItem;
924 // Even if the page hasn't loaded, display the favicon and title
926 FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) {
927 let favImage = tabItem.$favImage[0];
928 let fav = tabItem.$fav;
930 if (favImage.src != iconUrl)
931 favImage.src = iconUrl;
934 if (favImage.hasAttribute("src"))
935 favImage.removeAttribute("src");
938 tabItem._sendToSubscribers("iconUpdated");
942 let label = tab.label;
943 let $name = tabItem.$tabTitle;
944 if ($name.text() != label)
947 // ___ remove from waiting list now that we have no other
949 this._tabsWaitingForUpdate.remove(tab);
952 let tabUrl = tab.linkedBrowser.currentURI.spec;
953 let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl);
954 tabItem.$container.attr("title", tooltip);
956 // ___ Make sure the tab is complete and ready for updating.
957 if (options && options.force) {
958 tabItem.updateCanvas();
959 tabItem._sendToSubscribers("updated");
961 this._isComplete(tab, function TabItems__update_isComplete(isComplete) {
962 if (!Utils.isValidXULTab(tab) || tab.pinned)
966 tabItem.updateCanvas();
967 tabItem._sendToSubscribers("updated");
969 this._tabsWaitingForUpdate.push(tab);
980 // Takes in a xul:tab, creates a TabItem for it and adds it to the scene.
981 link: function TabItems_link(tab, options) {
983 Utils.assertThrow(tab, "tab");
984 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
985 Utils.assertThrow(!tab._tabViewTabItem, "shouldn't already be linked");
986 new TabItem(tab, options); // sets tab._tabViewTabItem to itself
994 // Takes in a xul:tab and destroys the TabItem associated with it.
995 unlink: function TabItems_unlink(tab) {
997 Utils.assertThrow(tab, "tab");
998 Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
999 // note that it's ok to unlink an app tab; see .handleTabUnpin
1001 this.unregister(tab._tabViewTabItem);
1002 tab._tabViewTabItem._sendToSubscribers("close");
1003 tab._tabViewTabItem.$container.remove();
1004 tab._tabViewTabItem.removeTrenches();
1005 Items.unsquish(null, tab._tabViewTabItem);
1007 tab._tabViewTabItem.tab = null;
1008 tab._tabViewTabItem.tabCanvas.tab = null;
1009 tab._tabViewTabItem.tabCanvas = null;
1010 tab._tabViewTabItem = null;
1011 Storage.saveTab(tab, null);
1013 this._tabsWaitingForUpdate.remove(tab);
1020 // when a tab becomes pinned, destroy its TabItem
1021 handleTabPin: function TabItems_handleTabPin(xulTab) {
1022 this.unlink(xulTab);
1026 // when a tab becomes unpinned, create a TabItem for it
1027 handleTabUnpin: function TabItems_handleTabUnpin(xulTab) {
1029 this.update(xulTab);
1033 // Function: startHeartbeat
1034 // Start a new heartbeat if there isn't one already started.
1035 // The heartbeat is a chain of setTimeout calls that allows us to spread
1036 // out update calls over a period of time.
1037 // _heartbeat is used to make sure that we don't add multiple
1038 // setTimeout chains.
1039 startHeartbeat: function TabItems_startHeartbeat() {
1040 if (!this._heartbeat) {
1042 this._heartbeat = setTimeout(function() {
1043 self._checkHeartbeat();
1044 }, this._heartbeatTiming);
1049 // Function: _checkHeartbeat
1050 // This periodically checks for tabs waiting to be updated, and calls
1052 // Should only be called by startHeartbeat and resumePainting.
1053 _checkHeartbeat: function TabItems__checkHeartbeat() {
1054 this._heartbeat = null;
1056 if (this.isPaintingPaused())
1059 // restart the heartbeat to update all waiting tabs once the UI becomes idle
1061 this.startHeartbeat();
1066 let items = this._tabsWaitingForUpdate.getItems();
1067 // Do as many updates as we can fit into a "perceived" amount
1068 // of time, which is tunable.
1069 while (accumTime < this._maxTimeForUpdating && items.length) {
1070 let updateBegin = Date.now();
1071 this._update(items.pop());
1072 let updateEnd = Date.now();
1074 // Maintain a simple average of time for each tabitem update
1075 // We can use this as a base by which to delay things like
1076 // tab zooming, so there aren't any hitches.
1077 let deltaTime = updateEnd - updateBegin;
1078 accumTime += deltaTime;
1081 if (this._tabsWaitingForUpdate.hasItems())
1082 this.startHeartbeat();
1086 // Function: pausePainting
1087 // Tells TabItems to stop updating thumbnails (so you can do
1088 // animations without thumbnail paints causing stutters).
1089 // pausePainting can be called multiple times, but every call to
1090 // pausePainting needs to be mirrored with a call to <resumePainting>.
1091 pausePainting: function TabItems_pausePainting() {
1092 this.paintingPaused++;
1093 if (this._heartbeat) {
1094 clearTimeout(this._heartbeat);
1095 this._heartbeat = null;
1100 // Function: resumePainting
1101 // Undoes a call to <pausePainting>. For instance, if you called
1102 // pausePainting three times in a row, you'll need to call resumePainting
1103 // three times before TabItems will start updating thumbnails again.
1104 resumePainting: function TabItems_resumePainting() {
1105 this.paintingPaused--;
1106 Utils.assert(this.paintingPaused > -1, "paintingPaused should not go below zero");
1107 if (!this.isPaintingPaused())
1108 this.startHeartbeat();
1112 // Function: isPaintingPaused
1113 // Returns a boolean indicating whether painting
1114 // is paused or not.
1115 isPaintingPaused: function TabItems_isPaintingPaused() {
1116 return this.paintingPaused > 0;
1120 // Function: pauseReconnecting
1121 // Don't reconnect any new tabs until resume is called.
1122 pauseReconnecting: function TabItems_pauseReconnecting() {
1123 Utils.assertThrow(!this._reconnectingPaused, "shouldn't already be paused");
1125 this._reconnectingPaused = true;
1129 // Function: resumeReconnecting
1130 // Reconnect all of the tabs that were created since we paused.
1131 resumeReconnecting: function TabItems_resumeReconnecting() {
1132 Utils.assertThrow(this._reconnectingPaused, "should already be paused");
1134 this._reconnectingPaused = false;
1135 this.items.forEach(function(item) {
1136 if (!item._reconnected)
1142 // Function: reconnectingPaused
1143 // Returns true if reconnecting is paused.
1144 reconnectingPaused: function TabItems_reconnectingPaused() {
1145 return this._reconnectingPaused;
1149 // Function: register
1150 // Adds the given <TabItem> to the master list.
1151 register: function TabItems_register(item) {
1152 Utils.assert(item && item.isAnItem, 'item must be a TabItem');
1153 Utils.assert(this.items.indexOf(item) == -1, 'only register once per item');
1154 this.items.push(item);
1158 // Function: unregister
1159 // Removes the given <TabItem> from the master list.
1160 unregister: function TabItems_unregister(item) {
1161 var index = this.items.indexOf(item);
1163 this.items.splice(index, 1);
1167 // Function: getItems
1168 // Returns a copy of the master array of <TabItem>s.
1169 getItems: function TabItems_getItems() {
1170 return Utils.copy(this.items);
1174 // Function: saveAll
1175 // Saves all open <TabItem>s.
1176 saveAll: function TabItems_saveAll() {
1177 let tabItems = this.getItems();
1179 tabItems.forEach(function TabItems_saveAll_forEach(tabItem) {
1185 // Function: storageSanity
1186 // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage)
1187 // and returns true if it looks valid.
1188 // TODO: this is a stub, please implement
1189 storageSanity: function TabItems_storageSanity(data) {
1194 // Function: getFontSizeFromWidth
1195 // Private method that returns the fontsize to use given the tab's width
1196 getFontSizeFromWidth: function TabItem_getFontSizeFromWidth(width) {
1197 let widthRange = new Range(0, TabItems.tabWidth);
1198 let proportion = widthRange.proportion(width - TabItems.tabItemPadding.x, true);
1199 // proportion is in [0,1]
1200 return TabItems.fontSizeRange.scale(proportion);
1204 // Function: _getWidthForHeight
1205 // Private method that returns the tabitem width given a height.
1206 _getWidthForHeight: function TabItems__getWidthForHeight(height) {
1207 return height * TabItems.invTabAspect;
1211 // Function: _getHeightForWidth
1212 // Private method that returns the tabitem height given a width.
1213 _getHeightForWidth: function TabItems__getHeightForWidth(width) {
1214 return width * TabItems.tabAspect;
1218 // Function: calcValidSize
1219 // Pass in a desired size, and receive a size based on proper title
1220 // size and aspect ratio.
1221 calcValidSize: function TabItems_calcValidSize(size, options) {
1222 Utils.assert(Utils.isPoint(size), 'input is a Point');
1224 let width = Math.max(TabItems.minTabWidth, size.x);
1225 let showTitle = !options || !options.hideTitle;
1226 let titleSize = showTitle ? TabItems.fontSizeRange.max : 0;
1227 let height = Math.max(TabItems.minTabHeight, size.y - titleSize);
1228 let retSize = new Point(width, height);
1231 retSize.y = this._getHeightForWidth(width);
1233 retSize.x = this._getWidthForHeight(height);
1235 if (size.x > -1 && size.y > -1) {
1236 if (retSize.x < size.x)
1237 retSize.y = this._getHeightForWidth(retSize.x);
1239 retSize.x = this._getWidthForHeight(retSize.y);
1243 retSize.y += titleSize;
1250 // Class: TabPriorityQueue
1251 // Container that returns tab items in a priority order
1252 // Current implementation assigns tab to either a high priority
1253 // or low priority queue, and toggles which queue items are popped
1254 // from. This guarantees that high priority items which are constantly
1255 // being added will not eclipse changes for lower priority items.
1256 function TabPriorityQueue() {
1259 TabPriorityQueue.prototype = {
1260 _low: [], // low priority queue
1261 _high: [], // high priority queue
1264 // Function: toString
1265 // Prints [TabPriorityQueue count=count] for debug use
1266 toString: function TabPriorityQueue_toString() {
1267 return "[TabPriorityQueue count=" + (this._low.length + this._high.length) + "]";
1272 // Empty the update queue
1273 clear: function TabPriorityQueue_clear() {
1279 // Function: hasItems
1280 // Return whether pending items exist
1281 hasItems: function TabPriorityQueue_hasItems() {
1282 return (this._low.length > 0) || (this._high.length > 0);
1286 // Function: getItems
1287 // Returns all queued items, ordered from low to high priority
1288 getItems: function TabPriorityQueue_getItems() {
1289 return this._low.concat(this._high);
1294 // Add an item to be prioritized
1295 push: function TabPriorityQueue_push(tab) {
1296 // Push onto correct priority queue.
1297 // It's only low priority if it's in a stack, and isn't the top,
1298 // and the stack isn't expanded.
1299 // If it already exists in the destination queue,
1300 // leave it. If it exists in a different queue, remove it first and push
1302 let item = tab._tabViewTabItem;
1303 if (item.parent && (item.parent.isStacked() &&
1304 !item.parent.isTopOfStack(item) &&
1305 !item.parent.expanded)) {
1306 let idx = this._high.indexOf(tab);
1308 this._high.splice(idx, 1);
1309 this._low.unshift(tab);
1310 } else if (this._low.indexOf(tab) == -1)
1311 this._low.unshift(tab);
1313 let idx = this._low.indexOf(tab);
1315 this._low.splice(idx, 1);
1316 this._high.unshift(tab);
1317 } else if (this._high.indexOf(tab) == -1)
1318 this._high.unshift(tab);
1324 // Remove and return the next item in priority order
1325 pop: function TabPriorityQueue_pop() {
1327 if (this._high.length)
1328 ret = this._high.pop();
1329 else if (this._low.length)
1330 ret = this._low.pop();
1336 // Return the next item in priority order, without removing it
1337 peek: function TabPriorityQueue_peek() {
1339 if (this._high.length)
1340 ret = this._high[this._high.length-1];
1341 else if (this._low.length)
1342 ret = this._low[this._low.length-1];
1348 // Remove the passed item
1349 remove: function TabPriorityQueue_remove(tab) {
1350 let index = this._high.indexOf(tab);
1352 this._high.splice(index, 1);
1354 index = this._low.indexOf(tab);
1356 this._low.splice(index, 1);
1363 // Takes care of the actual canvas for the tab thumbnail
1364 // Does not need to be accessed from outside of tabitems.js
1365 function TabCanvas(tab, canvas) {
1367 this.canvas = canvas;
1370 TabCanvas.prototype = Utils.extend(new Subscribable(), {
1372 // Function: toString
1373 // Prints [TabCanvas (tab)] for debug use
1374 toString: function TabCanvas_toString() {
1375 return "[TabCanvas (" + this.tab + ")]";
1380 paint: function TabCanvas_paint(evt) {
1381 var w = this.canvas.width;
1382 var h = this.canvas.height;
1386 if (!this.tab.linkedBrowser.contentWindow) {
1387 Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
1391 let win = this.tab.linkedBrowser.contentWindow;
1392 gPageThumbnails.captureToCanvas(win, this.canvas);
1394 this._sendToSubscribers("painted");
1398 // Function: toImageData
1399 toImageData: function TabCanvas_toImageData() {
1400 return this.canvas.toDataURL("image/png");