Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
5 // **********
6 // Title: tabitems.js
8 // ##########
9 // Class: TabItem
10 // An <Item> that represents a tab. Also implements the <Subscribable> interface.
11 //
12 // Parameters:
13 // tab - a xul:tab
14 function TabItem(tab, options) {
15 Utils.assert(tab, "tab");
17 this.tab = tab;
18 // register this as the tab's tabItem
19 this.tab._tabViewTabItem = this;
21 if (!options)
22 options = {};
24 // ___ set up div
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;
30 let $div = iQ(div);
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]);
44 this._hidden = false;
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'));
60 }
62 this.bounds = new Rect(0,0,1,1);
64 this._lastTabUpdateTime = Date.now();
66 // ___ superclass setup
67 this._init(div);
69 // ___ drag/drop
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);
74 };
76 this.draggable();
78 let self = this;
80 // ___ more div setup
81 $div.mousedown(function(e) {
82 if (!Utils.isRightClick(e))
83 self.lastMouseDownTarget = e.target;
84 });
86 $div.mouseup(function(e) {
87 var same = (e.target == self.lastMouseDownTarget);
88 self.lastMouseDownTarget = null;
89 if (!same)
90 return;
92 // press close button or middle mouse click
93 if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) {
94 self.closedManually = true;
95 self.close();
96 } else {
97 if (!Items.item(this).isDragging)
98 self.zoomIn();
99 }
100 });
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);
111 };
113 TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
114 // ----------
115 // Function: toString
116 // Prints [TabItem (tab)] for debug use
117 toString: function TabItem_toString() {
118 return "[TabItem (" + this.tab + ")]";
119 },
121 // ----------
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();
130 },
132 // ----------
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;
140 },
142 // ----------
143 // Function: isShowingCachedData
144 // Returns a boolean indicates whether the cached data is being displayed or
145 // not.
146 isShowingCachedData: function TabItem_isShowingCachedData() {
147 return this._showsCachedData;
148 },
150 // ----------
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;
164 },
166 // ----------
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;
173 },
175 // ----------
176 // Function: getStorageData
177 // Get data to be used for persistent storage of this object.
178 getStorageData: function TabItem_getStorageData() {
179 let data = {
180 groupID: (this.parent ? this.parent.id : 0)
181 };
182 if (this.parent && this.parent.getActiveTab() == this)
183 data.active = true;
185 return data;
186 },
188 // ----------
189 // Function: save
190 // Store persistent for this object.
191 save: function TabItem_save() {
192 try {
193 if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save
194 return;
196 let data = this.getStorageData();
197 if (TabItems.storageSanity(data))
198 Storage.saveTab(this.tab, data);
199 } catch(e) {
200 Utils.log("Error in saving tab value: "+e);
201 }
202 },
204 // ----------
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);
210 if (tabState) {
211 let index = (tabState.index || tabState.entries.length) - 1;
212 if (index in tabState.entries)
213 return tabState.entries[index];
214 }
216 return null;
217 },
219 // ----------
220 // Function: getTabState
221 // Returns the current tab state, i.e. the title and URL of the active
222 // history entry.
223 getTabState: function TabItem_getTabState() {
224 let entry = this._getCurrentTabStateEntry();
225 let title = "";
226 let url = "";
228 if (entry) {
229 if (entry.title)
230 title = entry.title;
232 url = entry.url;
233 } else {
234 url = this.tab.linkedBrowser.currentURI.spec;
235 }
237 return {title: title, url: url};
238 },
240 // ----------
241 // Function: _reconnect
242 // Load the reciever's persistent data from storage. If there is none,
243 // treats it as a new tab.
244 //
245 // Parameters:
246 // options - an object with additional parameters, see below
247 //
248 // Possible options:
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);
256 let groupItem;
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();
263 if (this.parent)
264 this.parent.remove(this, {immediately: true});
266 if (tabData.groupID)
267 groupItem = GroupItems.groupItem(tabData.groupID);
268 else
269 groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds});
271 if (groupItem) {
272 groupItem.add(this, {immediately: true});
274 // restore the active tab for each group between browser sessions
275 if (tabData.active)
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);
283 }
284 } else {
285 if (options && options.groupItemId)
286 groupItem = GroupItems.groupItem(options.groupItemId);
288 if (groupItem) {
289 groupItem.add(this, {immediately: true});
290 } else {
291 // create tab group by double click is handled in UI_init().
292 GroupItems.newTab(this, {immediately: true});
293 }
294 }
296 this._reconnected = true;
297 this.save();
298 this._sendToSubscribers("reconnected");
299 },
301 // ----------
302 // Function: setHidden
303 // Hide/unhide this item
304 setHidden: function TabItem_setHidden(val) {
305 if (val)
306 this.addClass("tabHidden");
307 else
308 this.removeClass("tabHidden");
309 this._hidden = val;
310 },
312 // ----------
313 // Function: getHidden
314 // Return hide state of item
315 getHidden: function TabItem_getHidden() {
316 return this._hidden;
317 },
319 // ----------
320 // Function: setBounds
321 // Moves this item to the specified location and size.
322 //
323 // Parameters:
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
327 //
328 // Possible options:
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!');
333 if (!options)
334 options = {};
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);
343 var css = {};
345 if (rect.left != this.bounds.left || options.force)
346 css.left = rect.left;
348 if (rect.top != this.bounds.top || options.force)
349 css.top = rect.top;
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';
355 }
357 if (rect.height != this.bounds.height || options.force) {
358 css.height = rect.height - TabItems.tabItemPadding.y;
359 if (!this.isStacked)
360 css.height -= TabItems.fontSizeRange.max;
361 }
363 if (Utils.isEmptyObject(css))
364 return;
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);
373 } else {
374 TabItems.pausePainting();
375 this.$container.animate(css, {
376 duration: 200,
377 easing: "tabviewBounce",
378 complete: function() {
379 TabItems.resumePainting();
380 }
381 });
382 }
384 if (css.fontSize && !(this.parent && this.parent.isStacked())) {
385 if (css.fontSize < TabItems.fontSizeRange.min)
386 immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut();
387 else
388 immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn();
389 }
391 if (css.width) {
392 TabItems.update(this.tab);
394 let widthRange, proportion;
396 if (this.parent && this.parent.isStacked()) {
397 if (UI.rtl) {
398 this.$fav.css({top:0, right:0});
399 } else {
400 this.$fav.css({top:0, left:0});
401 }
402 widthRange = new Range(70, 90);
403 proportion = widthRange.proportion(css.width); // between 0 and 1
404 } else {
405 if (UI.rtl) {
406 this.$fav.css({top:4, right:2});
407 } else {
408 this.$fav.css({top:4, left:4});
409 }
410 widthRange = new Range(40, 45);
411 proportion = widthRange.proportion(css.width); // between 0 and 1
412 }
414 if (proportion <= .1)
415 this.$close.hide();
416 else
417 this.$close.show().css({opacity:proportion});
419 var pad = 1 + 5 * proportion;
420 var alphaRange = new Range(0.1,0.2);
421 this.$fav.css({
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) +")",
427 });
428 }
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);
441 this.save();
442 },
444 // ----------
445 // Function: setZ
446 // Sets the z-index for this item.
447 setZ: function TabItem_setZ(value) {
448 this.zIndex = value;
449 this.$container.css({zIndex: value});
450 },
452 // ----------
453 // Function: close
454 // Closes this item (actually closes the tab associated with it, which automatically
455 // closes the item.
456 // Parameters:
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 });
466 }
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;
474 if (tabClosed)
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
479 return tabClosed;
480 },
482 // ----------
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);
487 },
489 // ----------
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);
494 },
496 // ----------
497 // Function: makeActive
498 // Updates this item to visually indicate that it's active.
499 makeActive: function TabItem_makeActive() {
500 this.$container.addClass("focus");
502 if (this.parent)
503 this.parent.setActiveTab(this);
504 },
506 // ----------
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");
511 },
513 // ----------
514 // Function: zoomIn
515 // Allows you to select the tab and zoom in on it, thereby bringing you
516 // to the tab in Firefox to interact with.
517 // Parameters:
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)
522 return;
524 let self = this;
525 let $tabEl = this.$container;
526 let $canvas = this.$canvas;
528 Search.hide();
530 UI.setActive(this);
531 TabItems._update(this.tab, {force: true});
533 // Zoom in!
534 let tab = this.tab;
536 function onZoomDone() {
537 $canvas.css({ 'transform': null });
538 $tabEl.removeClass("front");
540 UI.goToTab(tab);
542 // tab might not be selected because hideTabView() is invoked after
543 // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab
544 if (!tab.selected) {
545 UI.onTabSelect(gBrowser.selectedTab);
546 } else {
547 if (isNewBlankTab)
548 gWindow.gURLBar.focus();
549 }
550 if (self.parent && self.parent.expanded)
551 self.parent.collapse();
553 self._sendToSubscribers("zoomedIn");
554 }
556 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
557 if (animateZoom) {
558 let transform = this.getZoomTransform();
559 TabItems.pausePainting();
561 if (this.parent && this.parent.expanded)
562 $tabEl.removeClass("stack-trayed");
563 $tabEl.addClass("front");
564 $canvas
565 .css({ 'transform-origin': transform.transformOrigin })
566 .animate({ 'transform': transform.transform }, {
567 duration: 230,
568 easing: 'fast',
569 complete: function() {
570 onZoomDone();
572 setTimeout(function() {
573 TabItems.resumePainting();
574 }, 0);
575 }
576 });
577 } else {
578 setTimeout(onZoomDone, 0);
579 }
580 },
582 // ----------
583 // Function: zoomOut
584 // Handles the zoom down animation after returning to TabView.
585 // It is expected that this routine will be called from the chrome thread
586 //
587 // Parameters:
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;
591 var self = this;
593 let onZoomDone = function onZoomDone() {
594 $tab.removeClass("front");
595 $canvas.css("transform", null);
597 if (typeof complete == "function")
598 complete();
599 };
601 UI.setActive(this);
602 TabItems._update(this.tab, {force: true});
604 $tab.addClass("front");
606 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
607 if (animateZoom) {
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();
613 $canvas.css({
614 'transform': transform.transform,
615 'transform-origin': transform.transformOrigin
616 });
618 $canvas.animate({ "transform": "scale(1.0)" }, {
619 duration: 300,
620 easing: 'cubic-bezier', // note that this is legal easing, even without parameters
621 complete: function() {
622 TabItems.resumePainting();
623 onZoomDone();
624 }
625 });
626 } else {
627 onZoomDone();
628 }
629 },
631 // ----------
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.
651 if (!scaleCheat)
652 scaleCheat = 1.7;
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;
664 return {
665 transformOrigin: xOrigin + "% " + yOrigin + "%",
666 transform: "scale(" + zoomScaleFactor + ")"
667 };
668 },
670 // ----------
671 // Function: updateCanvas
672 // Updates the tabitem's canvas.
673 updateCanvas: function TabItem_updateCanvas() {
674 // ___ thumbnail
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;
682 }
683 }
685 TabItems._lastUpdateTime = Date.now();
686 this._lastTabUpdateTime = TabItems._lastUpdateTime;
688 if (this.tabCanvas)
689 this.tabCanvas.paint();
691 // ___ cache
692 if (this.isShowingCachedData())
693 this.hideCachedData();
694 }
695 });
697 // ##########
698 // Class: TabItems
699 // Singleton for managing <TabItem>s
700 let TabItems = {
701 minTabWidth: 40,
702 tabWidth: 160,
703 tabHeight: 120,
704 tabAspect: 0, // set in init
705 invTabAspect: 0, // set in init
706 fontSize: 9,
707 fontSizeRange: new Range(8,15),
708 _fragment: null,
709 items: [],
710 paintingPaused: 0,
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(),
716 _eventListeners: [],
717 _pauseUpdateForTest: false,
718 _reconnectingPaused: false,
719 tabItemPadding: {},
720 _mozAfterPaintHandler: null,
722 // ----------
723 // Function: toString
724 // Prints [TabItems count=count] for debug use
725 toString: function TabItems_toString() {
726 return "[TabItems count=" + this.items.length + "]";
727 },
729 // ----------
730 // Function: init
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");
734 let self = this;
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"));
745 $canvas.hide();
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;
755 if (!tab.pinned)
756 self.link(tab);
757 }
758 // When a tab's content is loaded, show the canvas and hide the cached data
759 // if necessary.
760 this._eventListeners.attrModified = function (event) {
761 let tab = event.target;
763 if (!tab.pinned)
764 self.update(tab);
765 }
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)
772 self.unlink(tab);
773 }
774 for (let name in this._eventListeners) {
775 AllTabs.register(name, this._eventListeners[name]);
776 }
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) {
782 if (tab.pinned)
783 return;
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);
793 self.update(tab);
794 });
795 },
797 // ----------
798 // Function: uninit
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]);
805 }
806 this.items.forEach(function(tabItem) {
807 delete tabItem.tab._tabViewTabItem;
809 for (let x in tabItem) {
810 if (typeof tabItem[x] == "object")
811 tabItem[x] = null;
812 }
813 });
815 this.items = null;
816 this._eventListeners = null;
817 this._lastUpdateTime = null;
818 this._tabsWaitingForUpdate.clear();
819 },
821 // ----------
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() {
827 if (this._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;
841 },
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));
852 return;
853 }
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);
861 });
862 mm.sendAsyncMessage(message);
863 },
865 // ----------
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);
870 if (index == -1)
871 return;
873 let tab = gBrowser.tabs[index];
874 if (!tab.pinned)
875 this.update(tab);
876 },
878 // ----------
879 // Function: update
880 // Takes in a xul:tab.
881 update: function TabItems_update(tab) {
882 try {
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");
887 let shouldDefer = (
888 this.isPaintingPaused() ||
889 this._tabsWaitingForUpdate.hasItems() ||
890 Date.now() - this._lastUpdateTime < this._heartbeatTiming
891 );
893 if (shouldDefer) {
894 this._tabsWaitingForUpdate.push(tab);
895 this.startHeartbeat();
896 } else
897 this._update(tab);
898 } catch(e) {
899 Utils.log(e);
900 }
901 },
903 // ----------
904 // Function: _update
905 // Takes in a xul:tab.
906 //
907 // Parameters:
908 // tab - a xul tab to update
909 // options - an object with additional parameters, see below
910 //
911 // Possible options:
912 // force - true to always update the tab item even if it's incomplete
913 _update: function TabItems__update(tab, options) {
914 try {
915 if (this._pauseUpdateForTest)
916 return;
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
925 // ___ icon
926 FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) {
927 let favImage = tabItem.$favImage[0];
928 let fav = tabItem.$fav;
929 if (iconUrl) {
930 if (favImage.src != iconUrl)
931 favImage.src = iconUrl;
932 fav.show();
933 } else {
934 if (favImage.hasAttribute("src"))
935 favImage.removeAttribute("src");
936 fav.hide();
937 }
938 tabItem._sendToSubscribers("iconUpdated");
939 });
941 // ___ label
942 let label = tab.label;
943 let $name = tabItem.$tabTitle;
944 if ($name.text() != label)
945 $name.text(label);
947 // ___ remove from waiting list now that we have no other
948 // early returns
949 this._tabsWaitingForUpdate.remove(tab);
951 // ___ URL
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");
960 } else {
961 this._isComplete(tab, function TabItems__update_isComplete(isComplete) {
962 if (!Utils.isValidXULTab(tab) || tab.pinned)
963 return;
965 if (isComplete) {
966 tabItem.updateCanvas();
967 tabItem._sendToSubscribers("updated");
968 } else {
969 this._tabsWaitingForUpdate.push(tab);
970 }
971 }.bind(this));
972 }
973 } catch(e) {
974 Utils.log(e);
975 }
976 },
978 // ----------
979 // Function: link
980 // Takes in a xul:tab, creates a TabItem for it and adds it to the scene.
981 link: function TabItems_link(tab, options) {
982 try {
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
987 } catch(e) {
988 Utils.log(e);
989 }
990 },
992 // ----------
993 // Function: unlink
994 // Takes in a xul:tab and destroys the TabItem associated with it.
995 unlink: function TabItems_unlink(tab) {
996 try {
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);
1014 } catch(e) {
1015 Utils.log(e);
1016 }
1017 },
1019 // ----------
1020 // when a tab becomes pinned, destroy its TabItem
1021 handleTabPin: function TabItems_handleTabPin(xulTab) {
1022 this.unlink(xulTab);
1023 },
1025 // ----------
1026 // when a tab becomes unpinned, create a TabItem for it
1027 handleTabUnpin: function TabItems_handleTabUnpin(xulTab) {
1028 this.link(xulTab);
1029 this.update(xulTab);
1030 },
1032 // ----------
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) {
1041 let self = this;
1042 this._heartbeat = setTimeout(function() {
1043 self._checkHeartbeat();
1044 }, this._heartbeatTiming);
1045 }
1046 },
1048 // ----------
1049 // Function: _checkHeartbeat
1050 // This periodically checks for tabs waiting to be updated, and calls
1051 // _update on them.
1052 // Should only be called by startHeartbeat and resumePainting.
1053 _checkHeartbeat: function TabItems__checkHeartbeat() {
1054 this._heartbeat = null;
1056 if (this.isPaintingPaused())
1057 return;
1059 // restart the heartbeat to update all waiting tabs once the UI becomes idle
1060 if (!UI.isIdle()) {
1061 this.startHeartbeat();
1062 return;
1063 }
1065 let accumTime = 0;
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;
1079 }
1081 if (this._tabsWaitingForUpdate.hasItems())
1082 this.startHeartbeat();
1083 },
1085 // ----------
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;
1096 }
1097 },
1099 // ----------
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();
1109 },
1111 // ----------
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;
1117 },
1119 // ----------
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;
1126 },
1128 // ----------
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)
1137 item._reconnect();
1138 });
1139 },
1141 // ----------
1142 // Function: reconnectingPaused
1143 // Returns true if reconnecting is paused.
1144 reconnectingPaused: function TabItems_reconnectingPaused() {
1145 return this._reconnectingPaused;
1146 },
1148 // ----------
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);
1155 },
1157 // ----------
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);
1162 if (index != -1)
1163 this.items.splice(index, 1);
1164 },
1166 // ----------
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);
1171 },
1173 // ----------
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) {
1180 tabItem.save();
1181 });
1182 },
1184 // ----------
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) {
1190 return true;
1191 },
1193 // ----------
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);
1201 },
1203 // ----------
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;
1208 },
1210 // ----------
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;
1215 },
1217 // ----------
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);
1230 if (size.x > -1)
1231 retSize.y = this._getHeightForWidth(width);
1232 if (size.y > -1)
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);
1238 else
1239 retSize.x = this._getWidthForHeight(retSize.y);
1240 }
1242 if (showTitle)
1243 retSize.y += titleSize;
1245 return retSize;
1246 }
1247 };
1249 // ##########
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() {
1257 };
1259 TabPriorityQueue.prototype = {
1260 _low: [], // low priority queue
1261 _high: [], // high priority queue
1263 // ----------
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) + "]";
1268 },
1270 // ----------
1271 // Function: clear
1272 // Empty the update queue
1273 clear: function TabPriorityQueue_clear() {
1274 this._low = [];
1275 this._high = [];
1276 },
1278 // ----------
1279 // Function: hasItems
1280 // Return whether pending items exist
1281 hasItems: function TabPriorityQueue_hasItems() {
1282 return (this._low.length > 0) || (this._high.length > 0);
1283 },
1285 // ----------
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);
1290 },
1292 // ----------
1293 // Function: push
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
1301 // onto new queue.
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);
1307 if (idx != -1) {
1308 this._high.splice(idx, 1);
1309 this._low.unshift(tab);
1310 } else if (this._low.indexOf(tab) == -1)
1311 this._low.unshift(tab);
1312 } else {
1313 let idx = this._low.indexOf(tab);
1314 if (idx != -1) {
1315 this._low.splice(idx, 1);
1316 this._high.unshift(tab);
1317 } else if (this._high.indexOf(tab) == -1)
1318 this._high.unshift(tab);
1319 }
1320 },
1322 // ----------
1323 // Function: pop
1324 // Remove and return the next item in priority order
1325 pop: function TabPriorityQueue_pop() {
1326 let ret = null;
1327 if (this._high.length)
1328 ret = this._high.pop();
1329 else if (this._low.length)
1330 ret = this._low.pop();
1331 return ret;
1332 },
1334 // ----------
1335 // Function: peek
1336 // Return the next item in priority order, without removing it
1337 peek: function TabPriorityQueue_peek() {
1338 let ret = null;
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];
1343 return ret;
1344 },
1346 // ----------
1347 // Function: remove
1348 // Remove the passed item
1349 remove: function TabPriorityQueue_remove(tab) {
1350 let index = this._high.indexOf(tab);
1351 if (index != -1)
1352 this._high.splice(index, 1);
1353 else {
1354 index = this._low.indexOf(tab);
1355 if (index != -1)
1356 this._low.splice(index, 1);
1357 }
1358 }
1359 };
1361 // ##########
1362 // Class: TabCanvas
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) {
1366 this.tab = tab;
1367 this.canvas = canvas;
1368 };
1370 TabCanvas.prototype = Utils.extend(new Subscribable(), {
1371 // ----------
1372 // Function: toString
1373 // Prints [TabCanvas (tab)] for debug use
1374 toString: function TabCanvas_toString() {
1375 return "[TabCanvas (" + this.tab + ")]";
1376 },
1378 // ----------
1379 // Function: paint
1380 paint: function TabCanvas_paint(evt) {
1381 var w = this.canvas.width;
1382 var h = this.canvas.height;
1383 if (!w || !h)
1384 return;
1386 if (!this.tab.linkedBrowser.contentWindow) {
1387 Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
1388 return;
1389 }
1391 let win = this.tab.linkedBrowser.contentWindow;
1392 gPageThumbnails.captureToCanvas(win, this.canvas);
1394 this._sendToSubscribers("painted");
1395 },
1397 // ----------
1398 // Function: toImageData
1399 toImageData: function TabCanvas_toImageData() {
1400 return this.canvas.toDataURL("image/png");
1401 }
1402 });