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: groupItems.js
8 // ##########
9 // Class: GroupItem
10 // A single groupItem in the TabView window. Descended from <Item>.
11 // Note that it implements the <Subscribable> interface.
12 //
13 // ----------
14 // Constructor: GroupItem
15 //
16 // Parameters:
17 // listOfEls - an array of DOM elements for tabs to be added to this groupItem
18 // options - various options for this groupItem (see below). In addition, gets passed
19 // to <add> along with the elements provided.
20 //
21 // Possible options:
22 // id - specifies the groupItem's id; otherwise automatically generated
23 // userSize - see <Item.userSize>; default is null
24 // bounds - a <Rect>; otherwise based on the locations of the provided elements
25 // container - a DOM element to use as the container for this groupItem; otherwise will create
26 // title - the title for the groupItem; otherwise blank
27 // focusTitle - focus the title's input field after creation
28 // dontPush - true if this groupItem shouldn't push away or snap on creation; default is false
29 // immediately - true if we want all placement immediately, not with animation
30 function GroupItem(listOfEls, options) {
31 if (!options)
32 options = {};
34 this._inited = false;
35 this._uninited = false;
36 this._children = []; // an array of Items
37 this.isAGroupItem = true;
38 this.id = options.id || GroupItems.getNextID();
39 this._isStacked = false;
40 this.expanded = null;
41 this.hidden = false;
42 this.fadeAwayUndoButtonDelay = 15000;
43 this.fadeAwayUndoButtonDuration = 300;
45 this.keepProportional = false;
46 this._frozenItemSizeData = {};
48 this._onChildClose = this._onChildClose.bind(this);
50 // Variable: _activeTab
51 // The <TabItem> for the groupItem's active tab.
52 this._activeTab = null;
54 if (Utils.isPoint(options.userSize))
55 this.userSize = new Point(options.userSize);
57 var self = this;
59 var rectToBe;
60 if (options.bounds) {
61 Utils.assert(Utils.isRect(options.bounds), "options.bounds must be a Rect");
62 rectToBe = new Rect(options.bounds);
63 }
65 if (!rectToBe) {
66 rectToBe = GroupItems.getBoundingBox(listOfEls);
67 rectToBe.inset(-42, -42);
68 }
70 var $container = options.container;
71 let immediately = options.immediately || $container ? true : false;
72 if (!$container) {
73 $container = iQ('<div>')
74 .addClass('groupItem')
75 .css({position: 'absolute'})
76 .css(rectToBe);
77 }
79 this.bounds = $container.bounds();
81 this.isDragging = false;
82 $container
83 .css({zIndex: -100})
84 .attr("data-id", this.id)
85 .appendTo("body");
87 // ___ Resizer
88 this.$resizer = iQ("<div>")
89 .addClass('resizer')
90 .appendTo($container)
91 .hide();
93 // ___ Titlebar
94 var html =
95 "<div class='title-container'>" +
96 "<input class='name' />" +
97 "<div class='title-shield' />" +
98 "</div>";
100 this.$titlebar = iQ('<div>')
101 .addClass('titlebar')
102 .html(html)
103 .appendTo($container);
105 this.$closeButton = iQ('<div>')
106 .addClass('close')
107 .click(function() {
108 self.closeAll();
109 })
110 .attr("title", tabviewString("groupItem.closeGroup"))
111 .appendTo($container);
113 // ___ Title
114 this.$titleContainer = iQ('.title-container', this.$titlebar);
115 this.$title = iQ('.name', this.$titlebar).attr('placeholder', this.defaultName);
116 this.$titleShield = iQ('.title-shield', this.$titlebar);
117 this.setTitle(options.title);
119 var handleKeyPress = function (e) {
120 if (e.keyCode == KeyEvent.DOM_VK_ESCAPE ||
121 e.keyCode == KeyEvent.DOM_VK_RETURN) {
122 (self.$title)[0].blur();
123 self.$title
124 .addClass("transparentBorder")
125 .one("mouseout", function() {
126 self.$title.removeClass("transparentBorder");
127 });
128 e.stopPropagation();
129 e.preventDefault();
130 }
131 };
133 var handleKeyUp = function(e) {
134 // NOTE: When user commits or cancels IME composition, the last key
135 // event fires only a keyup event. Then, we shouldn't take any
136 // reactions but we should update our status.
137 self.save();
138 };
140 this.$title
141 .blur(function() {
142 self._titleFocused = false;
143 self.$title[0].setSelectionRange(0, 0);
144 self.$titleShield.show();
145 if (self.getTitle())
146 gTabView.firstUseExperienced = true;
147 self.save();
148 })
149 .focus(function() {
150 self._unfreezeItemSize();
151 if (!self._titleFocused) {
152 (self.$title)[0].select();
153 self._titleFocused = true;
154 }
155 })
156 .mousedown(function(e) {
157 e.stopPropagation();
158 })
159 .keypress(handleKeyPress)
160 .keyup(handleKeyUp)
161 .attr("title", tabviewString("groupItem.defaultName"));
163 this.$titleShield
164 .mousedown(function(e) {
165 self.lastMouseDownTarget = (Utils.isLeftClick(e) ? e.target : null);
166 })
167 .mouseup(function(e) {
168 var same = (e.target == self.lastMouseDownTarget);
169 self.lastMouseDownTarget = null;
170 if (!same)
171 return;
173 if (!self.isDragging)
174 self.focusTitle();
175 })
176 .attr("title", tabviewString("groupItem.defaultName"));
178 if (options.focusTitle)
179 this.focusTitle();
181 // ___ Stack Expander
182 this.$expander = iQ("<div/>")
183 .addClass("stackExpander")
184 .appendTo($container)
185 .hide();
187 // ___ app tabs: create app tab tray and populate it
188 let appTabTrayContainer = iQ("<div/>")
189 .addClass("appTabTrayContainer")
190 .appendTo($container);
191 this.$appTabTray = iQ("<div/>")
192 .addClass("appTabTray")
193 .appendTo(appTabTrayContainer);
195 let pinnedTabCount = gBrowser._numPinnedTabs;
196 AllTabs.tabs.forEach(function (xulTab, index) {
197 // only adjust tray when it's the last app tab.
198 if (xulTab.pinned)
199 this.addAppTab(xulTab, {dontAdjustTray: index + 1 < pinnedTabCount});
200 }, this);
202 // ___ Undo Close
203 this.$undoContainer = null;
204 this._undoButtonTimeoutId = null;
206 // ___ Superclass initialization
207 this._init($container[0]);
209 // ___ Children
210 // We explicitly set dontArrange=true to prevent the groupItem from
211 // re-arranging its children after a tabItem has been added. This saves us a
212 // group.arrange() call per child and therefore some tab.setBounds() calls.
213 options.dontArrange = true;
214 listOfEls.forEach(function (el) {
215 self.add(el, options);
216 });
218 // ___ Finish Up
219 this._addHandlers($container);
221 this.setResizable(true, immediately);
223 GroupItems.register(this);
225 // ___ Position
226 this.setBounds(rectToBe, immediately);
227 if (options.dontPush) {
228 this.setZ(drag.zIndex);
229 drag.zIndex++;
230 } else {
231 // Calling snap will also trigger pushAway
232 this.snap(immediately);
233 }
235 if (!options.immediately && listOfEls.length > 0)
236 $container.hide().fadeIn();
238 this._inited = true;
239 this.save();
241 GroupItems.updateGroupCloseButtons();
242 };
244 // ----------
245 GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
246 // ----------
247 // Function: toString
248 // Prints [GroupItem id=id] for debug use
249 toString: function GroupItem_toString() {
250 return "[GroupItem id=" + this.id + "]";
251 },
253 // ----------
254 // Variable: defaultName
255 // The prompt text for the title field.
256 defaultName: tabviewString('groupItem.defaultName'),
258 // -----------
259 // Function: setActiveTab
260 // Sets the active <TabItem> for this groupItem; can be null, but only
261 // if there are no children.
262 setActiveTab: function GroupItem_setActiveTab(tab) {
263 Utils.assertThrow((!tab && this._children.length == 0) || tab.isATabItem,
264 "tab must be null (if no children) or a TabItem");
266 this._activeTab = tab;
268 if (this.isStacked())
269 this.arrange({immediately: true});
270 },
272 // -----------
273 // Function: getActiveTab
274 // Gets the active <TabItem> for this groupItem; can be null, but only
275 // if there are no children.
276 getActiveTab: function GroupItem_getActiveTab() {
277 return this._activeTab;
278 },
280 // ----------
281 // Function: getStorageData
282 // Returns all of the info worth storing about this groupItem.
283 getStorageData: function GroupItem_getStorageData() {
284 var data = {
285 bounds: this.getBounds(),
286 userSize: null,
287 title: this.getTitle(),
288 id: this.id
289 };
291 if (Utils.isPoint(this.userSize))
292 data.userSize = new Point(this.userSize);
294 return data;
295 },
297 // ----------
298 // Function: isEmpty
299 // Returns true if the tab groupItem is empty and unnamed.
300 isEmpty: function GroupItem_isEmpty() {
301 return !this._children.length && !this.getTitle();
302 },
304 // ----------
305 // Function: isStacked
306 // Returns true if this item is in a stacked groupItem.
307 isStacked: function GroupItem_isStacked() {
308 return this._isStacked;
309 },
311 // ----------
312 // Function: isTopOfStack
313 // Returns true if the item is showing on top of this group's stack,
314 // determined by whether the tab is this group's topChild, or
315 // if it doesn't have one, its first child.
316 isTopOfStack: function GroupItem_isTopOfStack(item) {
317 return this.isStacked() && item == this.getTopChild();
318 },
320 // ----------
321 // Function: save
322 // Saves this groupItem to persistent storage.
323 save: function GroupItem_save() {
324 if (!this._inited || this._uninited) // too soon/late to save
325 return;
327 var data = this.getStorageData();
328 if (GroupItems.groupItemStorageSanity(data))
329 Storage.saveGroupItem(gWindow, data);
330 },
332 // ----------
333 // Function: deleteData
334 // Deletes the groupItem in the persistent storage.
335 deleteData: function GroupItem_deleteData() {
336 this._uninited = true;
337 Storage.deleteGroupItem(gWindow, this.id);
338 },
340 // ----------
341 // Function: getTitle
342 // Returns the title of this groupItem as a string.
343 getTitle: function GroupItem_getTitle() {
344 return this.$title ? this.$title.val() : '';
345 },
347 // ----------
348 // Function: setTitle
349 // Sets the title of this groupItem with the given string
350 setTitle: function GroupItem_setTitle(value) {
351 this.$title.val(value);
352 this.save();
353 },
355 // ----------
356 // Function: focusTitle
357 // Hide the title's shield and focus the underlying input field.
358 focusTitle: function GroupItem_focusTitle() {
359 this.$titleShield.hide();
360 this.$title[0].focus();
361 },
363 // ----------
364 // Function: adjustAppTabTray
365 // Used to adjust the appTabTray size, to split the appTabIcons across
366 // multiple columns when needed - if the groupItem size is too small.
367 //
368 // Parameters:
369 // arrangeGroup - rearrange the groupItem if the number of appTab columns
370 // changes. If true, then this.arrange() is called, otherwise not.
371 adjustAppTabTray: function GroupItem_adjustAppTabTray(arrangeGroup) {
372 let icons = iQ(".appTabIcon", this.$appTabTray);
373 let container = iQ(this.$appTabTray[0].parentNode);
374 if (!icons.length) {
375 // There are no icons, so hide the appTabTray if needed.
376 if (parseInt(container.css("width")) != 0) {
377 this.$appTabTray.css("-moz-column-count", "auto");
378 this.$appTabTray.css("height", 0);
379 container.css("width", 0);
380 container.css("height", 0);
382 if (container.hasClass("appTabTrayContainerTruncated"))
383 container.removeClass("appTabTrayContainerTruncated");
385 if (arrangeGroup)
386 this.arrange();
387 }
388 return;
389 }
391 let iconBounds = iQ(icons[0]).bounds();
392 let boxBounds = this.getBounds();
393 let contentHeight = boxBounds.height -
394 parseInt(container.css("top")) -
395 this.$resizer.height();
396 let rows = Math.floor(contentHeight / iconBounds.height);
397 let columns = Math.ceil(icons.length / rows);
398 let columnsGap = parseInt(this.$appTabTray.css("-moz-column-gap"));
399 let iconWidth = iconBounds.width + columnsGap;
400 let maxColumns = Math.floor((boxBounds.width * 0.20) / iconWidth);
402 Utils.assert(rows > 0 && columns > 0 && maxColumns > 0,
403 "make sure the calculated rows, columns and maxColumns are correct");
405 if (columns > maxColumns)
406 container.addClass("appTabTrayContainerTruncated");
407 else if (container.hasClass("appTabTrayContainerTruncated"))
408 container.removeClass("appTabTrayContainerTruncated");
410 // Need to drop the -moz- prefix when Gecko makes it obsolete.
411 // See bug 629452.
412 if (parseInt(this.$appTabTray.css("-moz-column-count")) != columns)
413 this.$appTabTray.css("-moz-column-count", columns);
415 if (parseInt(this.$appTabTray.css("height")) != contentHeight) {
416 this.$appTabTray.css("height", contentHeight + "px");
417 container.css("height", contentHeight + "px");
418 }
420 let fullTrayWidth = iconWidth * columns - columnsGap;
421 if (parseInt(this.$appTabTray.css("width")) != fullTrayWidth)
422 this.$appTabTray.css("width", fullTrayWidth + "px");
424 let trayWidth = iconWidth * Math.min(columns, maxColumns) - columnsGap;
425 if (parseInt(container.css("width")) != trayWidth) {
426 container.css("width", trayWidth + "px");
428 // Rearrange the groupItem if the width changed.
429 if (arrangeGroup)
430 this.arrange();
431 }
432 },
434 // ----------
435 // Function: getContentBounds
436 // Returns a <Rect> for the groupItem's content area (which doesn't include the title, etc).
437 //
438 // Parameters:
439 // options - an object with additional parameters, see below
440 //
441 // Possible options:
442 // stacked - true to get content bounds for stacked mode
443 getContentBounds: function GroupItem_getContentBounds(options) {
444 let box = this.getBounds();
445 let titleHeight = this.$titlebar.height();
446 box.top += titleHeight;
447 box.height -= titleHeight;
449 let appTabTrayContainer = iQ(this.$appTabTray[0].parentNode);
450 let appTabTrayWidth = appTabTrayContainer.width();
451 if (appTabTrayWidth)
452 appTabTrayWidth += parseInt(appTabTrayContainer.css(UI.rtl ? "left" : "right"));
454 box.width -= appTabTrayWidth;
455 if (UI.rtl) {
456 box.left += appTabTrayWidth;
457 }
459 // Make the computed bounds' "padding" and expand button margin actually be
460 // themeable --OR-- compute this from actual bounds. Bug 586546
461 box.inset(6, 6);
463 // make some room for the expand button in stacked mode
464 if (options && options.stacked)
465 box.height -= this.$expander.height() + 9; // the button height plus padding
467 return box;
468 },
470 // ----------
471 // Function: setBounds
472 // Sets the bounds with the given <Rect>, animating unless "immediately" is false.
473 //
474 // Parameters:
475 // inRect - a <Rect> giving the new bounds
476 // immediately - true if it should not animate; default false
477 // options - an object with additional parameters, see below
478 //
479 // Possible options:
480 // force - true to always update the DOM even if the bounds haven't changed; default false
481 setBounds: function GroupItem_setBounds(inRect, immediately, options) {
482 Utils.assert(Utils.isRect(inRect), 'GroupItem.setBounds: rect is not a real rectangle!');
484 // Validate and conform passed in size
485 let validSize = GroupItems.calcValidSize(
486 new Point(inRect.width, inRect.height));
487 let rect = new Rect(inRect.left, inRect.top, validSize.x, validSize.y);
489 if (!options)
490 options = {};
492 var titleHeight = this.$titlebar.height();
494 // ___ Determine what has changed
495 var css = {};
496 var titlebarCSS = {};
497 var contentCSS = {};
499 if (rect.left != this.bounds.left || options.force)
500 css.left = rect.left;
502 if (rect.top != this.bounds.top || options.force)
503 css.top = rect.top;
505 if (rect.width != this.bounds.width || options.force) {
506 css.width = rect.width;
507 titlebarCSS.width = rect.width;
508 contentCSS.width = rect.width;
509 }
511 if (rect.height != this.bounds.height || options.force) {
512 css.height = rect.height;
513 contentCSS.height = rect.height - titleHeight;
514 }
516 if (Utils.isEmptyObject(css))
517 return;
519 var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top);
520 this.bounds = new Rect(rect);
522 // Make sure the AppTab icons fit the new groupItem size.
523 if (css.width || css.height)
524 this.adjustAppTabTray();
526 // ___ Deal with children
527 if (css.width || css.height) {
528 this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)});
529 } else if (css.left || css.top) {
530 this._children.forEach(function(child) {
531 if (!child.getHidden()) {
532 var box = child.getBounds();
533 child.setPosition(box.left + offset.x, box.top + offset.y, immediately);
534 }
535 });
536 }
538 // ___ Update our representation
539 if (immediately) {
540 iQ(this.container).css(css);
541 this.$titlebar.css(titlebarCSS);
542 } else {
543 TabItems.pausePainting();
544 iQ(this.container).animate(css, {
545 duration: 350,
546 easing: "tabviewBounce",
547 complete: function() {
548 TabItems.resumePainting();
549 }
550 });
552 this.$titlebar.animate(titlebarCSS, {
553 duration: 350
554 });
555 }
557 UI.clearShouldResizeItems();
558 this.setTrenches(rect);
559 this.save();
560 },
562 // ----------
563 // Function: setZ
564 // Set the Z order for the groupItem's container, as well as its children.
565 setZ: function GroupItem_setZ(value) {
566 this.zIndex = value;
568 iQ(this.container).css({zIndex: value});
570 var count = this._children.length;
571 if (count) {
572 var topZIndex = value + count + 1;
573 var zIndex = topZIndex;
574 var self = this;
575 this._children.forEach(function(child) {
576 if (child == self.getTopChild())
577 child.setZ(topZIndex + 1);
578 else {
579 child.setZ(zIndex);
580 zIndex--;
581 }
582 });
583 }
584 },
586 // ----------
587 // Function: close
588 // Closes the groupItem, removing (but not closing) all of its children.
589 //
590 // Parameters:
591 // options - An object with optional settings for this call.
592 //
593 // Options:
594 // immediately - (bool) if true, no animation will be used
595 close: function GroupItem_close(options) {
596 this.removeAll({dontClose: true});
597 GroupItems.unregister(this);
599 // remove unfreeze event handlers, if item size is frozen
600 this._unfreezeItemSize({dontArrange: true});
602 let self = this;
603 let destroyGroup = function () {
604 iQ(self.container).remove();
605 if (self.$undoContainer) {
606 self.$undoContainer.remove();
607 self.$undoContainer = null;
608 }
609 self.removeTrenches();
610 Items.unsquish();
611 self._sendToSubscribers("close");
612 GroupItems.updateGroupCloseButtons();
613 }
615 if (this.hidden || (options && options.immediately)) {
616 destroyGroup();
617 } else {
618 iQ(this.container).animate({
619 opacity: 0,
620 "transform": "scale(.3)",
621 }, {
622 duration: 170,
623 complete: destroyGroup
624 });
625 }
627 this.deleteData();
628 },
630 // ----------
631 // Function: closeAll
632 // Closes the groupItem and all of its children.
633 closeAll: function GroupItem_closeAll() {
634 if (this._children.length > 0) {
635 this._unfreezeItemSize();
636 this._children.forEach(function(child) {
637 iQ(child.container).hide();
638 });
640 iQ(this.container).animate({
641 opacity: 0,
642 "transform": "scale(.3)",
643 }, {
644 duration: 170,
645 complete: function() {
646 iQ(this).hide();
647 }
648 });
650 this.droppable(false);
651 this.removeTrenches();
652 this._createUndoButton();
653 } else
654 this.close();
656 this._makeLastActiveGroupItemActive();
657 },
659 // ----------
660 // Function: _makeClosestTabActive
661 // Make the closest tab external to this group active.
662 // Used when closing the group.
663 _makeClosestTabActive: function GroupItem__makeClosestTabActive() {
664 let closeCenter = this.getBounds().center();
665 // Find closest tab to make active
666 let closestTabItem = UI.getClosestTab(closeCenter);
667 if (closestTabItem)
668 UI.setActive(closestTabItem);
669 },
671 // ----------
672 // Function: _makeLastActiveGroupItemActive
673 // Makes the last active group item active.
674 _makeLastActiveGroupItemActive: function GroupItem__makeLastActiveGroupItemActive() {
675 let groupItem = GroupItems.getLastActiveGroupItem();
676 if (groupItem)
677 UI.setActive(groupItem);
678 else
679 this._makeClosestTabActive();
680 },
682 // ----------
683 // Function: closeIfEmpty
684 // Closes the group if it's empty, is closable, and autoclose is enabled
685 // (see pauseAutoclose()). Returns true if the close occurred and false
686 // otherwise.
687 closeIfEmpty: function GroupItem_closeIfEmpty() {
688 if (this.isEmpty() && !UI._closedLastVisibleTab &&
689 !GroupItems.getUnclosableGroupItemId() && !GroupItems._autoclosePaused) {
690 this.close();
691 return true;
692 }
693 return false;
694 },
696 // ----------
697 // Function: _unhide
698 // Shows the hidden group.
699 //
700 // Parameters:
701 // options - various options (see below)
702 //
703 // Possible options:
704 // immediately - true when no animations should be used
705 _unhide: function GroupItem__unhide(options) {
706 this._cancelFadeAwayUndoButtonTimer();
707 this.hidden = false;
708 this.$undoContainer.remove();
709 this.$undoContainer = null;
710 this.droppable(true);
711 this.setTrenches(this.bounds);
713 let self = this;
715 let finalize = function () {
716 self._children.forEach(function(child) {
717 iQ(child.container).show();
718 });
720 UI.setActive(self);
721 self._sendToSubscribers("groupShown");
722 };
724 let $container = iQ(this.container).show();
726 if (!options || !options.immediately) {
727 $container.animate({
728 "transform": "scale(1)",
729 "opacity": 1
730 }, {
731 duration: 170,
732 complete: finalize
733 });
734 } else {
735 $container.css({"transform": "none", opacity: 1});
736 finalize();
737 }
739 GroupItems.updateGroupCloseButtons();
740 },
742 // ----------
743 // Function: closeHidden
744 // Removes the group item, its children and its container.
745 closeHidden: function GroupItem_closeHidden() {
746 let self = this;
748 this._cancelFadeAwayUndoButtonTimer();
750 // When the last non-empty groupItem is closed and there are no
751 // pinned tabs then create a new group with a blank tab.
752 let remainingGroups = GroupItems.groupItems.filter(function (groupItem) {
753 return (groupItem != self && groupItem.getChildren().length);
754 });
756 let tab = null;
758 if (!gBrowser._numPinnedTabs && !remainingGroups.length) {
759 let emptyGroups = GroupItems.groupItems.filter(function (groupItem) {
760 return (groupItem != self && !groupItem.getChildren().length);
761 });
762 let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup());
763 tab = group.newTab(null, {dontZoomIn: true});
764 }
766 let closed = this.destroy();
768 if (!tab)
769 return;
771 if (closed) {
772 // Let's make the new tab the selected tab.
773 UI.goToTab(tab);
774 } else {
775 // Remove the new tab and group, if this group is no longer closed.
776 tab._tabViewTabItem.parent.destroy({immediately: true});
777 }
778 },
780 // ----------
781 // Function: destroy
782 // Close all tabs linked to children (tabItems), removes all children and
783 // close the groupItem.
784 //
785 // Parameters:
786 // options - An object with optional settings for this call.
787 //
788 // Options:
789 // immediately - (bool) if true, no animation will be used
790 //
791 // Returns true if the groupItem has been closed, or false otherwise. A group
792 // could not have been closed due to a tab with an onUnload handler (that
793 // waits for user interaction).
794 destroy: function GroupItem_destroy(options) {
795 let self = this;
797 // when "TabClose" event is fired, the browser tab is about to close and our
798 // item "close" event is fired. And then, the browser tab gets closed.
799 // In other words, the group "close" event is fired before all browser
800 // tabs in the group are closed. The below code would fire the group "close"
801 // event only after all browser tabs in that group are closed.
802 this._children.concat().forEach(function(child) {
803 child.removeSubscriber("close", self._onChildClose);
805 if (child.close(true)) {
806 self.remove(child, { dontArrange: true });
807 } else {
808 // child.removeSubscriber() must be called before child.close(),
809 // therefore we call child.addSubscriber() if the tab is not removed.
810 child.addSubscriber("close", self._onChildClose);
811 }
812 });
814 if (this._children.length) {
815 if (this.hidden)
816 this.$undoContainer.fadeOut(function() { self._unhide() });
818 return false;
819 } else {
820 this.close(options);
821 return true;
822 }
823 },
825 // ----------
826 // Function: _fadeAwayUndoButton
827 // Fades away the undo button
828 _fadeAwayUndoButton: function GroupItem__fadeAwayUndoButton() {
829 let self = this;
831 if (this.$undoContainer) {
832 // if there is more than one group and other groups are not empty,
833 // fade away the undo button.
834 let shouldFadeAway = false;
836 if (GroupItems.groupItems.length > 1) {
837 shouldFadeAway =
838 GroupItems.groupItems.some(function(groupItem) {
839 return (groupItem != self && groupItem.getChildren().length > 0);
840 });
841 }
843 if (shouldFadeAway) {
844 self.$undoContainer.animate({
845 color: "transparent",
846 opacity: 0
847 }, {
848 duration: this._fadeAwayUndoButtonDuration,
849 complete: function() { self.closeHidden(); }
850 });
851 }
852 }
853 },
855 // ----------
856 // Function: _createUndoButton
857 // Makes the affordance for undo a close group action
858 _createUndoButton: function GroupItem__createUndoButton() {
859 let self = this;
860 this.$undoContainer = iQ("<div/>")
861 .addClass("undo")
862 .attr("type", "button")
863 .attr("data-group-id", this.id)
864 .appendTo("body");
865 iQ("<span/>")
866 .text(tabviewString("groupItem.undoCloseGroup"))
867 .appendTo(this.$undoContainer);
868 let undoClose = iQ("<span/>")
869 .addClass("close")
870 .attr("title", tabviewString("groupItem.discardClosedGroup"))
871 .appendTo(this.$undoContainer);
873 this.$undoContainer.css({
874 left: this.bounds.left + this.bounds.width/2 - iQ(self.$undoContainer).width()/2,
875 top: this.bounds.top + this.bounds.height/2 - iQ(self.$undoContainer).height()/2,
876 "transform": "scale(.1)",
877 opacity: 0
878 });
879 this.hidden = true;
881 // hide group item and show undo container.
882 setTimeout(function() {
883 self.$undoContainer.animate({
884 "transform": "scale(1)",
885 "opacity": 1
886 }, {
887 easing: "tabviewBounce",
888 duration: 170,
889 complete: function() {
890 self._sendToSubscribers("groupHidden");
891 }
892 });
893 }, 50);
895 // add click handlers
896 this.$undoContainer.click(function(e) {
897 // don't do anything if the close button is clicked.
898 if (e.target == undoClose[0])
899 return;
901 self.$undoContainer.fadeOut(function() { self._unhide(); });
902 });
904 undoClose.click(function() {
905 self.$undoContainer.fadeOut(function() { self.closeHidden(); });
906 });
908 this.setupFadeAwayUndoButtonTimer();
909 // Cancel the fadeaway if you move the mouse over the undo
910 // button, and restart the countdown once you move out of it.
911 this.$undoContainer.mouseover(function() {
912 self._cancelFadeAwayUndoButtonTimer();
913 });
914 this.$undoContainer.mouseout(function() {
915 self.setupFadeAwayUndoButtonTimer();
916 });
918 GroupItems.updateGroupCloseButtons();
919 },
921 // ----------
922 // Sets up fade away undo button timeout.
923 setupFadeAwayUndoButtonTimer: function GroupItem_setupFadeAwayUndoButtonTimer() {
924 let self = this;
926 if (!this._undoButtonTimeoutId) {
927 this._undoButtonTimeoutId = setTimeout(function() {
928 self._fadeAwayUndoButton();
929 }, this.fadeAwayUndoButtonDelay);
930 }
931 },
933 // ----------
934 // Cancels the fade away undo button timeout.
935 _cancelFadeAwayUndoButtonTimer: function GroupItem__cancelFadeAwayUndoButtonTimer() {
936 clearTimeout(this._undoButtonTimeoutId);
937 this._undoButtonTimeoutId = null;
938 },
940 // ----------
941 // Function: add
942 // Adds an item to the groupItem.
943 // Parameters:
944 //
945 // a - The item to add. Can be an <Item>, a DOM element or an iQ object.
946 // The latter two must refer to the container of an <Item>.
947 // options - An object with optional settings for this call.
948 //
949 // Options:
950 //
951 // index - (int) if set, add this tab at this index
952 // immediately - (bool) if true, no animation will be used
953 // dontArrange - (bool) if true, will not trigger an arrange on the group
954 add: function GroupItem_add(a, options) {
955 try {
956 var item;
957 var $el;
958 if (a.isAnItem) {
959 item = a;
960 $el = iQ(a.container);
961 } else {
962 $el = iQ(a);
963 item = Items.item($el);
964 }
966 // safeguard to remove the item from its previous group
967 if (item.parent && item.parent !== this)
968 item.parent.remove(item);
970 item.removeTrenches();
972 if (!options)
973 options = {};
975 var self = this;
977 var wasAlreadyInThisGroupItem = false;
978 var oldIndex = this._children.indexOf(item);
979 if (oldIndex != -1) {
980 this._children.splice(oldIndex, 1);
981 wasAlreadyInThisGroupItem = true;
982 }
984 // Insert the tab into the right position.
985 var index = ("index" in options) ? options.index : this._children.length;
986 this._children.splice(index, 0, item);
988 item.setZ(this.getZ() + 1);
990 if (!wasAlreadyInThisGroupItem) {
991 item.droppable(false);
992 item.groupItemData = {};
994 item.addSubscriber("close", this._onChildClose);
995 item.setParent(this);
996 $el.attr("data-group-id", this.id);
998 if (typeof item.setResizable == 'function')
999 item.setResizable(false, options.immediately);
1001 if (item == UI.getActiveTab() || !this._activeTab)
1002 this.setActiveTab(item);
1004 // if it matches the selected tab or no active tab and the browser
1005 // tab is hidden, the active group item would be set.
1006 if (item.tab.selected ||
1007 (!GroupItems.getActiveGroupItem() && !item.tab.hidden))
1008 UI.setActive(this);
1009 }
1011 if (!options.dontArrange)
1012 this.arrange({animate: !options.immediately});
1014 this._unfreezeItemSize({dontArrange: true});
1015 this._sendToSubscribers("childAdded", { item: item });
1017 UI.setReorderTabsOnHide(this);
1018 } catch(e) {
1019 Utils.log('GroupItem.add error', e);
1020 }
1021 },
1023 // ----------
1024 // Function: _onChildClose
1025 // Handles "close" events from the group's children.
1026 //
1027 // Parameters:
1028 // tabItem - The tabItem that is closed.
1029 _onChildClose: function GroupItem__onChildClose(tabItem) {
1030 let count = this._children.length;
1031 let dontArrange = tabItem.closedManually &&
1032 (this.expanded || !this.shouldStack(count));
1033 let dontClose = !tabItem.closedManually && gBrowser._numPinnedTabs > 0;
1034 this.remove(tabItem, {dontArrange: dontArrange, dontClose: dontClose});
1036 if (dontArrange)
1037 this._freezeItemSize(count);
1039 if (this._children.length > 0 && this._activeTab && tabItem.closedManually)
1040 UI.setActive(this);
1041 },
1043 // ----------
1044 // Function: remove
1045 // Removes an item from the groupItem.
1046 // Parameters:
1047 //
1048 // a - The item to remove. Can be an <Item>, a DOM element or an iQ object.
1049 // The latter two must refer to the container of an <Item>.
1050 // options - An optional object with settings for this call. See below.
1051 //
1052 // Possible options:
1053 // dontArrange - don't rearrange the remaining items
1054 // dontClose - don't close the group even if it normally would
1055 // immediately - don't animate
1056 remove: function GroupItem_remove(a, options) {
1057 try {
1058 let $el;
1059 let item;
1061 if (a.isAnItem) {
1062 item = a;
1063 $el = iQ(item.container);
1064 } else {
1065 $el = iQ(a);
1066 item = Items.item($el);
1067 }
1069 if (!options)
1070 options = {};
1072 let index = this._children.indexOf(item);
1073 if (index != -1)
1074 this._children.splice(index, 1);
1076 if (item == this._activeTab || !this._activeTab) {
1077 if (this._children.length > 0)
1078 this._activeTab = this._children[0];
1079 else
1080 this._activeTab = null;
1081 }
1083 $el[0].removeAttribute("data-group-id");
1084 item.setParent(null);
1085 item.removeClass("stacked");
1086 item.isStacked = false;
1087 item.setHidden(false);
1088 item.removeClass("stack-trayed");
1089 item.setRotation(0);
1091 // Force tabItem resize if it's dragged out of a stacked groupItem.
1092 // The tabItems's title will be visible and that's why we need to
1093 // recalculate its height.
1094 if (item.isDragging && this.isStacked())
1095 item.setBounds(item.getBounds(), true, {force: true});
1097 item.droppable(true);
1098 item.removeSubscriber("close", this._onChildClose);
1100 if (typeof item.setResizable == 'function')
1101 item.setResizable(true, options.immediately);
1103 // if a blank tab is selected while restoring a tab the blank tab gets
1104 // removed. we need to keep the group alive for the restored tab.
1105 if (item.isRemovedAfterRestore)
1106 options.dontClose = true;
1108 let closed = options.dontClose ? false : this.closeIfEmpty();
1109 if (closed ||
1110 (this._children.length == 0 && !gBrowser._numPinnedTabs &&
1111 !item.isDragging)) {
1112 this._makeLastActiveGroupItemActive();
1113 } else if (!options.dontArrange) {
1114 this.arrange({animate: !options.immediately});
1115 this._unfreezeItemSize({dontArrange: true});
1116 }
1118 this._sendToSubscribers("childRemoved", { item: item });
1119 } catch(e) {
1120 Utils.log(e);
1121 }
1122 },
1124 // ----------
1125 // Function: removeAll
1126 // Removes all of the groupItem's children.
1127 // The optional "options" param is passed to each remove call.
1128 removeAll: function GroupItem_removeAll(options) {
1129 let self = this;
1130 let newOptions = {dontArrange: true};
1131 if (options)
1132 Utils.extend(newOptions, options);
1134 let toRemove = this._children.concat();
1135 toRemove.forEach(function(child) {
1136 self.remove(child, newOptions);
1137 });
1138 },
1140 // ----------
1141 // Adds the given xul:tab as an app tab in this group's apptab tray
1142 //
1143 // Parameters:
1144 // xulTab - the xul:tab.
1145 // options - change how the app tab is added.
1146 //
1147 // Options:
1148 // position - the position of the app tab should be added to.
1149 // dontAdjustTray - (boolean) if true, do not adjust the tray.
1150 addAppTab: function GroupItem_addAppTab(xulTab, options) {
1151 GroupItems.getAppTabFavIconUrl(xulTab, function(iconUrl) {
1152 // The tab might have been removed or unpinned while waiting.
1153 if (!Utils.isValidXULTab(xulTab) || !xulTab.pinned)
1154 return;
1156 let self = this;
1157 let $appTab = iQ("<img>")
1158 .addClass("appTabIcon")
1159 .attr("src", iconUrl)
1160 .data("xulTab", xulTab)
1161 .mousedown(function GroupItem_addAppTab_onAppTabMousedown(event) {
1162 // stop mousedown propagation to disable group dragging on app tabs
1163 event.stopPropagation();
1164 })
1165 .click(function GroupItem_addAppTab_onAppTabClick(event) {
1166 if (!Utils.isLeftClick(event))
1167 return;
1169 UI.setActive(self, { dontSetActiveTabInGroup: true });
1170 UI.goToTab(iQ(this).data("xulTab"));
1171 });
1173 if (options && "position" in options) {
1174 let children = this.$appTabTray[0].childNodes;
1176 if (options.position >= children.length)
1177 $appTab.appendTo(this.$appTabTray);
1178 else
1179 this.$appTabTray[0].insertBefore($appTab[0], children[options.position]);
1180 } else {
1181 $appTab.appendTo(this.$appTabTray);
1182 }
1183 if (!options || !options.dontAdjustTray)
1184 this.adjustAppTabTray(true);
1186 this._sendToSubscribers("appTabIconAdded", { item: $appTab });
1187 }.bind(this));
1188 },
1190 // ----------
1191 // Removes the given xul:tab as an app tab in this group's apptab tray
1192 removeAppTab: function GroupItem_removeAppTab(xulTab) {
1193 // remove the icon
1194 iQ(".appTabIcon", this.$appTabTray).each(function(icon) {
1195 let $icon = iQ(icon);
1196 if ($icon.data("xulTab") != xulTab)
1197 return true;
1199 $icon.remove();
1200 return false;
1201 });
1203 // adjust the tray
1204 this.adjustAppTabTray(true);
1205 },
1207 // ----------
1208 // Arranges the given xul:tab as an app tab in the group's apptab tray
1209 arrangeAppTab: function GroupItem_arrangeAppTab(xulTab) {
1210 let self = this;
1212 let elements = iQ(".appTabIcon", this.$appTabTray);
1213 let length = elements.length;
1215 elements.each(function(icon) {
1216 let $icon = iQ(icon);
1217 if ($icon.data("xulTab") != xulTab)
1218 return true;
1220 let targetIndex = xulTab._tPos;
1222 $icon.remove({ preserveEventHandlers: true });
1223 if (targetIndex < (length - 1))
1224 self.$appTabTray[0].insertBefore(
1225 icon,
1226 iQ(".appTabIcon:nth-child(" + (targetIndex + 1) + ")", self.$appTabTray)[0]);
1227 else
1228 $icon.appendTo(self.$appTabTray);
1229 return false;
1230 });
1231 },
1233 // ----------
1234 // Function: hideExpandControl
1235 // Hide the control which expands a stacked groupItem into a quick-look view.
1236 hideExpandControl: function GroupItem_hideExpandControl() {
1237 this.$expander.hide();
1238 },
1240 // ----------
1241 // Function: showExpandControl
1242 // Show the control which expands a stacked groupItem into a quick-look view.
1243 showExpandControl: function GroupItem_showExpandControl() {
1244 let parentBB = this.getBounds();
1245 let childBB = this.getChild(0).getBounds();
1246 this.$expander
1247 .show()
1248 .css({
1249 left: parentBB.width/2 - this.$expander.width()/2
1250 });
1251 },
1253 // ----------
1254 // Function: shouldStack
1255 // Returns true if the groupItem, given "count", should stack (instead of
1256 // grid).
1257 shouldStack: function GroupItem_shouldStack(count) {
1258 let bb = this.getContentBounds();
1259 let options = {
1260 return: 'widthAndColumns',
1261 count: count || this._children.length,
1262 hideTitle: false
1263 };
1264 let arrObj = Items.arrange(this._children, bb, options);
1266 let shouldStack = arrObj.childWidth < TabItems.minTabWidth * 1.35;
1267 this._columns = shouldStack ? null : arrObj.columns;
1269 return shouldStack;
1270 },
1272 // ----------
1273 // Function: _freezeItemSize
1274 // Freezes current item size (when removing a child).
1275 //
1276 // Parameters:
1277 // itemCount - the number of children before the last one was removed
1278 _freezeItemSize: function GroupItem__freezeItemSize(itemCount) {
1279 let data = this._frozenItemSizeData;
1281 if (!data.lastItemCount) {
1282 let self = this;
1283 data.lastItemCount = itemCount;
1285 // unfreeze item size when tabview is hidden
1286 data.onTabViewHidden = function () self._unfreezeItemSize();
1287 window.addEventListener('tabviewhidden', data.onTabViewHidden, false);
1289 // we don't need to observe mouse movement when expanded because the
1290 // tray is closed when we leave it and collapse causes unfreezing
1291 if (!self.expanded) {
1292 // unfreeze item size when cursor is moved out of group bounds
1293 data.onMouseMove = function (e) {
1294 let cursor = new Point(e.pageX, e.pageY);
1295 if (!self.bounds.contains(cursor))
1296 self._unfreezeItemSize();
1297 }
1298 iQ(window).mousemove(data.onMouseMove);
1299 }
1300 }
1302 this.arrange({animate: true, count: data.lastItemCount});
1303 },
1305 // ----------
1306 // Function: _unfreezeItemSize
1307 // Unfreezes and updates item size.
1308 //
1309 // Parameters:
1310 // options - various options (see below)
1311 //
1312 // Possible options:
1313 // dontArrange - do not arrange items when unfreezing
1314 _unfreezeItemSize: function GroupItem__unfreezeItemSize(options) {
1315 let data = this._frozenItemSizeData;
1316 if (!data.lastItemCount)
1317 return;
1319 if (!options || !options.dontArrange)
1320 this.arrange({animate: true});
1322 // unbind event listeners
1323 window.removeEventListener('tabviewhidden', data.onTabViewHidden, false);
1324 if (data.onMouseMove)
1325 iQ(window).unbind('mousemove', data.onMouseMove);
1327 // reset freeze status
1328 this._frozenItemSizeData = {};
1329 },
1331 // ----------
1332 // Function: arrange
1333 // Lays out all of the children.
1334 //
1335 // Parameters:
1336 // options - passed to <Items.arrange> or <_stackArrange>, except those below
1337 //
1338 // Options:
1339 // addTab - (boolean) if true, we add one to the child count
1340 // oldDropIndex - if set, we will only set any bounds if the dropIndex has
1341 // changed
1342 // dropPos - (<Point>) a position where a tab is currently positioned, above
1343 // this group.
1344 // animate - (boolean) if true, movement of children will be animated.
1345 //
1346 // Returns:
1347 // dropIndex - an index value for where an item would be dropped, if
1348 // options.dropPos is given.
1349 arrange: function GroupItem_arrange(options) {
1350 if (!options)
1351 options = {};
1353 let childrenToArrange = [];
1354 this._children.forEach(function(child) {
1355 if (child.isDragging)
1356 options.addTab = true;
1357 else
1358 childrenToArrange.push(child);
1359 });
1361 if (GroupItems._arrangePaused) {
1362 GroupItems.pushArrange(this, options);
1363 return false;
1364 }
1366 let shouldStack = this.shouldStack(childrenToArrange.length + (options.addTab ? 1 : 0));
1367 let shouldStackArrange = (shouldStack && !this.expanded);
1368 let box;
1370 // if we should stack and we're not expanded
1371 if (shouldStackArrange) {
1372 this.showExpandControl();
1373 box = this.getContentBounds({stacked: true});
1374 this._stackArrange(childrenToArrange, box, options);
1375 return false;
1376 } else {
1377 this.hideExpandControl();
1378 box = this.getContentBounds();
1379 // a dropIndex is returned
1380 return this._gridArrange(childrenToArrange, box, options);
1381 }
1382 },
1384 // ----------
1385 // Function: _stackArrange
1386 // Arranges the children in a stack.
1387 //
1388 // Parameters:
1389 // childrenToArrange - array of <TabItem> children
1390 // bb - <Rect> to arrange within
1391 // options - see below
1392 //
1393 // Possible "options" properties:
1394 // animate - whether to animate; default: true.
1395 _stackArrange: function GroupItem__stackArrange(childrenToArrange, bb, options) {
1396 if (!options)
1397 options = {};
1398 var animate = "animate" in options ? options.animate : true;
1400 var count = childrenToArrange.length;
1401 if (!count)
1402 return;
1404 let itemAspect = TabItems.tabHeight / TabItems.tabWidth;
1405 let zIndex = this.getZ() + count + 1;
1406 let maxRotation = 35; // degress
1407 let scale = 0.7;
1408 let newTabsPad = 10;
1409 let bbAspect = bb.height / bb.width;
1410 let numInPile = 6;
1411 let angleDelta = 3.5; // degrees
1413 // compute size of the entire stack, modulo rotation.
1414 let size;
1415 if (bbAspect > itemAspect) { // Tall, thin groupItem
1416 size = TabItems.calcValidSize(new Point(bb.width * scale, -1),
1417 {hideTitle:true});
1418 } else { // Short, wide groupItem
1419 size = TabItems.calcValidSize(new Point(-1, bb.height * scale),
1420 {hideTitle:true});
1421 }
1423 // x is the left margin that the stack will have, within the content area (bb)
1424 // y is the vertical margin
1425 var x = (bb.width - size.x) / 2;
1426 var y = Math.min(size.x, (bb.height - size.y) / 2);
1427 var box = new Rect(bb.left + x, bb.top + y, size.x, size.y);
1429 var self = this;
1430 var children = [];
1432 // ensure topChild is the first item in childrenToArrange
1433 let topChild = this.getTopChild();
1434 let topChildPos = childrenToArrange.indexOf(topChild);
1435 if (topChildPos > 0) {
1436 childrenToArrange.splice(topChildPos, 1);
1437 childrenToArrange.unshift(topChild);
1438 }
1440 childrenToArrange.forEach(function GroupItem__stackArrange_order(child) {
1441 // Children are still considered stacked even if they're hidden later.
1442 child.addClass("stacked");
1443 child.isStacked = true;
1444 if (numInPile-- > 0) {
1445 children.push(child);
1446 } else {
1447 child.setHidden(true);
1448 }
1449 });
1451 self._isStacked = true;
1453 let angleAccum = 0;
1454 children.forEach(function GroupItem__stackArrange_apply(child, index) {
1455 child.setZ(zIndex);
1456 zIndex--;
1458 // Force a recalculation of height because we've changed how the title
1459 // is shown.
1460 child.setBounds(box, !animate || child.getHidden(), {force:true});
1461 child.setRotation((UI.rtl ? -1 : 1) * angleAccum);
1462 child.setHidden(false);
1463 angleAccum += angleDelta;
1464 });
1465 },
1467 // ----------
1468 // Function: _gridArrange
1469 // Arranges the children into a grid.
1470 //
1471 // Parameters:
1472 // childrenToArrange - array of <TabItem> children
1473 // box - <Rect> to arrange within
1474 // options - see below
1475 //
1476 // Possible "options" properties:
1477 // animate - whether to animate; default: true.
1478 // z - (int) a z-index to assign the children
1479 // columns - the number of columns to use in the layout, if known in advance
1480 //
1481 // Returns:
1482 // dropIndex - (int) the index at which a dragged item (if there is one) should be added
1483 // if it is dropped. Otherwise (boolean) false.
1484 _gridArrange: function GroupItem__gridArrange(childrenToArrange, box, options) {
1485 let arrangeOptions;
1486 if (this.expanded) {
1487 // if we're expanded, we actually want to use the expanded tray's bounds.
1488 box = new Rect(this.expanded.bounds);
1489 box.inset(8, 8);
1490 arrangeOptions = Utils.extend({}, options, {z: 99999});
1491 } else {
1492 this._isStacked = false;
1493 arrangeOptions = Utils.extend({}, options, {
1494 columns: this._columns
1495 });
1497 childrenToArrange.forEach(function(child) {
1498 child.removeClass("stacked");
1499 child.isStacked = false;
1500 child.setHidden(false);
1501 });
1502 }
1504 if (!childrenToArrange.length)
1505 return false;
1507 // Items.arrange will determine where/how the child items should be
1508 // placed, but will *not* actually move them for us. This is our job.
1509 let result = Items.arrange(childrenToArrange, box, arrangeOptions);
1510 let {dropIndex, rects, columns} = result;
1511 if ("oldDropIndex" in options && options.oldDropIndex === dropIndex)
1512 return dropIndex;
1514 this._columns = columns;
1515 let index = 0;
1516 let self = this;
1517 childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) {
1518 // If dropIndex spacing is active and this is a child after index,
1519 // bump it up one so we actually use the correct rect
1520 // (and skip one for the dropPos)
1521 if (self._dropSpaceActive && index === dropIndex)
1522 index++;
1523 child.setBounds(rects[index], !options.animate);
1524 child.setRotation(0);
1525 if (arrangeOptions.z)
1526 child.setZ(arrangeOptions.z);
1527 index++;
1528 });
1530 return dropIndex;
1531 },
1533 expand: function GroupItem_expand() {
1534 var self = this;
1535 // ___ we're stacked, and command is held down so expand
1536 UI.setActive(this.getTopChild());
1538 var startBounds = this.getChild(0).getBounds();
1539 var $tray = iQ("<div>").css({
1540 top: startBounds.top,
1541 left: startBounds.left,
1542 width: startBounds.width,
1543 height: startBounds.height,
1544 position: "absolute",
1545 zIndex: 99998
1546 }).appendTo("body");
1547 $tray[0].id = "expandedTray";
1549 var w = 180;
1550 var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1;
1551 var padding = 20;
1552 var col = Math.ceil(Math.sqrt(this._children.length));
1553 var row = Math.ceil(this._children.length/col);
1555 var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1));
1556 var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1));
1558 var pos = {left: startBounds.left, top: startBounds.top};
1559 pos.left -= overlayWidth / 3;
1560 pos.top -= overlayHeight / 3;
1562 if (pos.top < 0)
1563 pos.top = 20;
1564 if (pos.left < 0)
1565 pos.left = 20;
1566 if (pos.top + overlayHeight > window.innerHeight)
1567 pos.top = window.innerHeight - overlayHeight - 20;
1568 if (pos.left + overlayWidth > window.innerWidth)
1569 pos.left = window.innerWidth - overlayWidth - 20;
1571 $tray
1572 .animate({
1573 width: overlayWidth,
1574 height: overlayHeight,
1575 top: pos.top,
1576 left: pos.left
1577 }, {
1578 duration: 200,
1579 easing: "tabviewBounce",
1580 complete: function GroupItem_expand_animate_complete() {
1581 self._sendToSubscribers("expanded");
1582 }
1583 })
1584 .addClass("overlay");
1586 this._children.forEach(function(child) {
1587 child.addClass("stack-trayed");
1588 child.setHidden(false);
1589 });
1591 var $shield = iQ('<div>')
1592 .addClass('shield')
1593 .css({
1594 zIndex: 99997
1595 })
1596 .appendTo('body')
1597 .click(function() { // just in case
1598 self.collapse();
1599 });
1601 // There is a race-condition here. If there is
1602 // a mouse-move while the shield is coming up
1603 // it will collapse, which we don't want. Thus,
1604 // we wait a little bit before adding this event
1605 // handler.
1606 setTimeout(function() {
1607 $shield.mouseover(function() {
1608 self.collapse();
1609 });
1610 }, 200);
1612 this.expanded = {
1613 $tray: $tray,
1614 $shield: $shield,
1615 bounds: new Rect(pos.left, pos.top, overlayWidth, overlayHeight)
1616 };
1618 this.arrange();
1619 },
1621 // ----------
1622 // Function: collapse
1623 // Collapses the groupItem from the expanded "tray" mode.
1624 collapse: function GroupItem_collapse() {
1625 if (this.expanded) {
1626 var z = this.getZ();
1627 var box = this.getBounds();
1628 let self = this;
1629 this.expanded.$tray
1630 .css({
1631 zIndex: z + 1
1632 })
1633 .animate({
1634 width: box.width,
1635 height: box.height,
1636 top: box.top,
1637 left: box.left,
1638 opacity: 0
1639 }, {
1640 duration: 350,
1641 easing: "tabviewBounce",
1642 complete: function GroupItem_collapse_animate_complete() {
1643 iQ(this).remove();
1644 self._sendToSubscribers("collapsed");
1645 }
1646 });
1648 this.expanded.$shield.remove();
1649 this.expanded = null;
1651 this._children.forEach(function(child) {
1652 child.removeClass("stack-trayed");
1653 });
1655 this.arrange({z: z + 2});
1656 this._unfreezeItemSize({dontArrange: true});
1657 }
1658 },
1660 // ----------
1661 // Function: _addHandlers
1662 // Helper routine for the constructor; adds various event handlers to the container.
1663 _addHandlers: function GroupItem__addHandlers(container) {
1664 let self = this;
1665 let lastMouseDownTarget;
1667 container.mousedown(function(e) {
1668 let target = e.target;
1669 // only set the last mouse down target if it is a left click, not on the
1670 // close button, not on the expand button, not on the title bar and its
1671 // elements
1672 if (Utils.isLeftClick(e) &&
1673 self.$closeButton[0] != target &&
1674 self.$titlebar[0] != target &&
1675 self.$expander[0] != target &&
1676 !self.$titlebar.contains(target) &&
1677 !self.$appTabTray.contains(target)) {
1678 lastMouseDownTarget = target;
1679 } else {
1680 lastMouseDownTarget = null;
1681 }
1682 });
1683 container.mouseup(function(e) {
1684 let same = (e.target == lastMouseDownTarget);
1685 lastMouseDownTarget = null;
1687 if (same && !self.isDragging) {
1688 if (gBrowser.selectedTab.pinned &&
1689 UI.getActiveTab() != self.getActiveTab() &&
1690 self.getChildren().length > 0) {
1691 UI.setActive(self, { dontSetActiveTabInGroup: true });
1692 UI.goToTab(gBrowser.selectedTab);
1693 } else {
1694 let tabItem = self.getTopChild();
1695 if (tabItem)
1696 tabItem.zoomIn();
1697 else
1698 self.newTab();
1699 }
1700 }
1701 });
1703 let dropIndex = false;
1704 let dropSpaceTimer = null;
1706 // When the _dropSpaceActive flag is turned on on a group, and a tab is
1707 // dragged on top, a space will open up.
1708 this._dropSpaceActive = false;
1710 this.dropOptions.over = function GroupItem_dropOptions_over(event) {
1711 iQ(this.container).addClass("acceptsDrop");
1712 };
1713 this.dropOptions.move = function GroupItem_dropOptions_move(event) {
1714 let oldDropIndex = dropIndex;
1715 let dropPos = drag.info.item.getBounds().center();
1716 let options = {dropPos: dropPos,
1717 addTab: self._dropSpaceActive && drag.info.item.parent != self,
1718 oldDropIndex: oldDropIndex};
1719 let newDropIndex = self.arrange(options);
1720 // If this is a new drop index, start a timer!
1721 if (newDropIndex !== oldDropIndex) {
1722 dropIndex = newDropIndex;
1723 if (this._dropSpaceActive)
1724 return;
1726 if (dropSpaceTimer) {
1727 clearTimeout(dropSpaceTimer);
1728 dropSpaceTimer = null;
1729 }
1731 dropSpaceTimer = setTimeout(function GroupItem_arrange_evaluateDropSpace() {
1732 // Note that dropIndex's scope is GroupItem__addHandlers, but
1733 // newDropIndex's scope is GroupItem_dropOptions_move. Thus,
1734 // dropIndex may change with other movement events before we come
1735 // back and check this. If it's still the same dropIndex, activate
1736 // drop space display!
1737 if (dropIndex === newDropIndex) {
1738 self._dropSpaceActive = true;
1739 dropIndex = self.arrange({dropPos: dropPos,
1740 addTab: drag.info.item.parent != self,
1741 animate: true});
1742 }
1743 dropSpaceTimer = null;
1744 }, 250);
1745 }
1746 };
1747 this.dropOptions.drop = function GroupItem_dropOptions_drop(event) {
1748 iQ(this.container).removeClass("acceptsDrop");
1749 let options = {};
1750 if (this._dropSpaceActive)
1751 this._dropSpaceActive = false;
1753 if (dropSpaceTimer) {
1754 clearTimeout(dropSpaceTimer);
1755 dropSpaceTimer = null;
1756 // If we drop this item before the timed rearrange was executed,
1757 // we won't have an accurate dropIndex value. Get that now.
1758 let dropPos = drag.info.item.getBounds().center();
1759 dropIndex = self.arrange({dropPos: dropPos,
1760 addTab: drag.info.item.parent != self,
1761 animate: true});
1762 }
1764 if (dropIndex !== false)
1765 options = {index: dropIndex};
1766 this.add(drag.info.$el, options);
1767 UI.setActive(this);
1768 dropIndex = false;
1769 };
1770 this.dropOptions.out = function GroupItem_dropOptions_out(event) {
1771 dropIndex = false;
1772 if (this._dropSpaceActive)
1773 this._dropSpaceActive = false;
1775 if (dropSpaceTimer) {
1776 clearTimeout(dropSpaceTimer);
1777 dropSpaceTimer = null;
1778 }
1779 self.arrange();
1780 var groupItem = drag.info.item.parent;
1781 if (groupItem)
1782 groupItem.remove(drag.info.$el, {dontClose: true});
1783 iQ(this.container).removeClass("acceptsDrop");
1784 }
1786 this.draggable();
1787 this.droppable(true);
1789 this.$expander.click(function() {
1790 self.expand();
1791 });
1792 },
1794 // ----------
1795 // Function: setResizable
1796 // Sets whether the groupItem is resizable and updates the UI accordingly.
1797 setResizable: function GroupItem_setResizable(value, immediately) {
1798 var self = this;
1800 this.resizeOptions.minWidth = GroupItems.minGroupWidth;
1801 this.resizeOptions.minHeight = GroupItems.minGroupHeight;
1803 let start = this.resizeOptions.start;
1804 this.resizeOptions.start = function (event) {
1805 start.call(self, event);
1806 self._unfreezeItemSize();
1807 }
1809 if (value) {
1810 immediately ? this.$resizer.show() : this.$resizer.fadeIn();
1811 this.resizable(true);
1812 } else {
1813 immediately ? this.$resizer.hide() : this.$resizer.fadeOut();
1814 this.resizable(false);
1815 }
1816 },
1818 // ----------
1819 // Function: newTab
1820 // Creates a new tab within this groupItem.
1821 // Parameters:
1822 // url - the new tab should open this url as well
1823 // options - the options object
1824 // dontZoomIn - set to true to not zoom into the newly created tab
1825 // closedLastTab - boolean indicates the last tab has just been closed
1826 newTab: function GroupItem_newTab(url, options) {
1827 if (options && options.closedLastTab)
1828 UI.closedLastTabInTabView = true;
1830 UI.setActive(this, { dontSetActiveTabInGroup: true });
1832 let dontZoomIn = !!(options && options.dontZoomIn);
1833 return gBrowser.loadOneTab(url || gWindow.BROWSER_NEW_TAB_URL, { inBackground: dontZoomIn });
1834 },
1836 // ----------
1837 // Function: reorderTabItemsBasedOnTabOrder
1838 // Reorders the tabs in a groupItem based on the arrangment of the tabs
1839 // shown in the tab bar. It does it by sorting the children
1840 // of the groupItem by the positions of their respective tabs in the
1841 // tab bar.
1842 reorderTabItemsBasedOnTabOrder: function GroupItem_reorderTabItemsBasedOnTabOrder() {
1843 this._children.sort(function(a,b) a.tab._tPos - b.tab._tPos);
1845 this.arrange({animate: false});
1846 // this.arrange calls this.save for us
1847 },
1849 // Function: reorderTabsBasedOnTabItemOrder
1850 // Reorders the tabs in the tab bar based on the arrangment of the tabs
1851 // shown in the groupItem.
1852 reorderTabsBasedOnTabItemOrder: function GroupItem_reorderTabsBasedOnTabItemOrder() {
1853 let indices;
1854 let tabs = this._children.map(function (tabItem) tabItem.tab);
1856 tabs.forEach(function (tab, index) {
1857 if (!indices)
1858 indices = tabs.map(function (tab) tab._tPos);
1860 let start = index ? indices[index - 1] + 1 : 0;
1861 let end = index + 1 < indices.length ? indices[index + 1] - 1 : Infinity;
1862 let targetRange = new Range(start, end);
1864 if (!targetRange.contains(tab._tPos)) {
1865 gBrowser.moveTabTo(tab, start);
1866 indices = null;
1867 }
1868 });
1869 },
1871 // ----------
1872 // Function: getTopChild
1873 // Gets the <Item> that should be displayed on top when in stack mode.
1874 getTopChild: function GroupItem_getTopChild() {
1875 if (!this.getChildren().length) {
1876 return null;
1877 }
1879 return this.getActiveTab() || this.getChild(0);
1880 },
1882 // ----------
1883 // Function: getChild
1884 // Returns the nth child tab or null if index is out of range.
1885 //
1886 // Parameters:
1887 // index - the index of the child tab to return, use negative
1888 // numbers to index from the end (-1 is the last child)
1889 getChild: function GroupItem_getChild(index) {
1890 if (index < 0)
1891 index = this._children.length + index;
1892 if (index >= this._children.length || index < 0)
1893 return null;
1894 return this._children[index];
1895 },
1897 // ----------
1898 // Function: getChildren
1899 // Returns all children.
1900 getChildren: function GroupItem_getChildren() {
1901 return this._children;
1902 }
1903 });
1905 // ##########
1906 // Class: GroupItems
1907 // Singleton for managing all <GroupItem>s.
1908 let GroupItems = {
1909 groupItems: [],
1910 nextID: 1,
1911 _inited: false,
1912 _activeGroupItem: null,
1913 _cleanupFunctions: [],
1914 _arrangePaused: false,
1915 _arrangesPending: [],
1916 _removingHiddenGroups: false,
1917 _delayedModUpdates: [],
1918 _autoclosePaused: false,
1919 minGroupHeight: 110,
1920 minGroupWidth: 125,
1921 _lastActiveList: null,
1923 // ----------
1924 // Function: toString
1925 // Prints [GroupItems] for debug use
1926 toString: function GroupItems_toString() {
1927 return "[GroupItems count=" + this.groupItems.length + "]";
1928 },
1930 // ----------
1931 // Function: init
1932 init: function GroupItems_init() {
1933 let self = this;
1935 // setup attr modified handler, and prepare for its uninit
1936 function handleAttrModified(event) {
1937 self._handleAttrModified(event.target);
1938 }
1940 // make sure any closed tabs are removed from the delay update list
1941 function handleClose(event) {
1942 let idx = self._delayedModUpdates.indexOf(event.target);
1943 if (idx != -1)
1944 self._delayedModUpdates.splice(idx, 1);
1945 }
1947 this._lastActiveList = new MRUList();
1949 AllTabs.register("attrModified", handleAttrModified);
1950 AllTabs.register("close", handleClose);
1951 this._cleanupFunctions.push(function() {
1952 AllTabs.unregister("attrModified", handleAttrModified);
1953 AllTabs.unregister("close", handleClose);
1954 });
1955 },
1957 // ----------
1958 // Function: uninit
1959 uninit: function GroupItems_uninit() {
1960 // call our cleanup functions
1961 this._cleanupFunctions.forEach(function(func) {
1962 func();
1963 });
1965 this._cleanupFunctions = [];
1967 // additional clean up
1968 this.groupItems = null;
1969 },
1971 // ----------
1972 // Function: newGroup
1973 // Creates a new empty group.
1974 newGroup: function GroupItems_newGroup() {
1975 let bounds = new Rect(20, 20, 250, 200);
1976 return new GroupItem([], {bounds: bounds, immediately: true});
1977 },
1979 // ----------
1980 // Function: pauseArrange
1981 // Bypass arrange() calls and collect for resolution in
1982 // resumeArrange()
1983 pauseArrange: function GroupItems_pauseArrange() {
1984 Utils.assert(this._arrangePaused == false,
1985 "pauseArrange has been called while already paused");
1986 Utils.assert(this._arrangesPending.length == 0,
1987 "There are bypassed arrange() calls that haven't been resolved");
1988 this._arrangePaused = true;
1989 },
1991 // ----------
1992 // Function: pushArrange
1993 // Push an arrange() call and its arguments onto an array
1994 // to be resolved in resumeArrange()
1995 pushArrange: function GroupItems_pushArrange(groupItem, options) {
1996 Utils.assert(this._arrangePaused,
1997 "Ensure pushArrange() called while arrange()s aren't paused");
1998 let i;
1999 for (i = 0; i < this._arrangesPending.length; i++)
2000 if (this._arrangesPending[i].groupItem === groupItem)
2001 break;
2002 let arrangeInfo = {
2003 groupItem: groupItem,
2004 options: options
2005 };
2006 if (i < this._arrangesPending.length)
2007 this._arrangesPending[i] = arrangeInfo;
2008 else
2009 this._arrangesPending.push(arrangeInfo);
2010 },
2012 // ----------
2013 // Function: resumeArrange
2014 // Resolve bypassed and collected arrange() calls
2015 resumeArrange: function GroupItems_resumeArrange() {
2016 this._arrangePaused = false;
2017 for (let i = 0; i < this._arrangesPending.length; i++) {
2018 let g = this._arrangesPending[i];
2019 g.groupItem.arrange(g.options);
2020 }
2021 this._arrangesPending = [];
2022 },
2024 // ----------
2025 // Function: _handleAttrModified
2026 // watch for icon changes on app tabs
2027 _handleAttrModified: function GroupItems__handleAttrModified(xulTab) {
2028 if (!UI.isTabViewVisible()) {
2029 if (this._delayedModUpdates.indexOf(xulTab) == -1) {
2030 this._delayedModUpdates.push(xulTab);
2031 }
2032 } else
2033 this._updateAppTabIcons(xulTab);
2034 },
2036 // ----------
2037 // Function: flushTabUpdates
2038 // Update apptab icons based on xulTabs which have been updated
2039 // while the TabView hasn't been visible
2040 flushAppTabUpdates: function GroupItems_flushAppTabUpdates() {
2041 let self = this;
2042 this._delayedModUpdates.forEach(function(xulTab) {
2043 self._updateAppTabIcons(xulTab);
2044 });
2045 this._delayedModUpdates = [];
2046 },
2048 // ----------
2049 // Function: _updateAppTabIcons
2050 // Update images of any apptab icons that point to passed in xultab
2051 _updateAppTabIcons: function GroupItems__updateAppTabIcons(xulTab) {
2052 if (!xulTab.pinned)
2053 return;
2055 this.getAppTabFavIconUrl(xulTab, function(iconUrl) {
2056 iQ(".appTabIcon").each(function GroupItems__updateAppTabIcons_forEach(icon) {
2057 let $icon = iQ(icon);
2058 if ($icon.data("xulTab") == xulTab && iconUrl != $icon.attr("src"))
2059 $icon.attr("src", iconUrl);
2060 });
2061 });
2062 },
2064 // ----------
2065 // Function: getAppTabFavIconUrl
2066 // Gets the fav icon url for app tab.
2067 getAppTabFavIconUrl: function GroupItems_getAppTabFavIconUrl(xulTab, callback) {
2068 FavIcons.getFavIconUrlForTab(xulTab, function GroupItems_getAppTabFavIconUrl_getFavIconUrlForTab(iconUrl) {
2069 callback(iconUrl || FavIcons.defaultFavicon);
2070 });
2071 },
2073 // ----------
2074 // Function: addAppTab
2075 // Adds the given xul:tab to the app tab tray in all groups
2076 addAppTab: function GroupItems_addAppTab(xulTab) {
2077 this.groupItems.forEach(function(groupItem) {
2078 groupItem.addAppTab(xulTab);
2079 });
2080 this.updateGroupCloseButtons();
2081 },
2083 // ----------
2084 // Function: removeAppTab
2085 // Removes the given xul:tab from the app tab tray in all groups
2086 removeAppTab: function GroupItems_removeAppTab(xulTab) {
2087 this.groupItems.forEach(function(groupItem) {
2088 groupItem.removeAppTab(xulTab);
2089 });
2090 this.updateGroupCloseButtons();
2091 },
2093 // ----------
2094 // Function: arrangeAppTab
2095 // Arranges the given xul:tab as an app tab from app tab tray in all groups
2096 arrangeAppTab: function GroupItems_arrangeAppTab(xulTab) {
2097 this.groupItems.forEach(function(groupItem) {
2098 groupItem.arrangeAppTab(xulTab);
2099 });
2100 },
2102 // ----------
2103 // Function: getNextID
2104 // Returns the next unused groupItem ID.
2105 getNextID: function GroupItems_getNextID() {
2106 var result = this.nextID;
2107 this.nextID++;
2108 this._save();
2109 return result;
2110 },
2112 // ----------
2113 // Function: saveAll
2114 // Saves GroupItems state, as well as the state of all of the groupItems.
2115 saveAll: function GroupItems_saveAll() {
2116 this._save();
2117 this.groupItems.forEach(function(groupItem) {
2118 groupItem.save();
2119 });
2120 },
2122 // ----------
2123 // Function: _save
2124 // Saves GroupItems state.
2125 _save: function GroupItems__save() {
2126 if (!this._inited) // too soon to save now
2127 return;
2129 let activeGroupId = this._activeGroupItem ? this._activeGroupItem.id : null;
2130 Storage.saveGroupItemsData(
2131 gWindow,
2132 { nextID: this.nextID, activeGroupId: activeGroupId,
2133 totalNumber: this.groupItems.length });
2134 },
2136 // ----------
2137 // Function: getBoundingBox
2138 // Given an array of DOM elements, returns a <Rect> with (roughly) the union of their locations.
2139 getBoundingBox: function GroupItems_getBoundingBox(els) {
2140 var bounds = [iQ(el).bounds() for each (el in els)];
2141 var left = Math.min.apply({},[ b.left for each (b in bounds) ]);
2142 var top = Math.min.apply({},[ b.top for each (b in bounds) ]);
2143 var right = Math.max.apply({},[ b.right for each (b in bounds) ]);
2144 var bottom = Math.max.apply({},[ b.bottom for each (b in bounds) ]);
2146 return new Rect(left, top, right-left, bottom-top);
2147 },
2149 // ----------
2150 // Function: reconstitute
2151 // Restores to stored state, creating groupItems as needed.
2152 reconstitute: function GroupItems_reconstitute(groupItemsData, groupItemData) {
2153 try {
2154 let activeGroupId;
2156 if (groupItemsData) {
2157 if (groupItemsData.nextID)
2158 this.nextID = Math.max(this.nextID, groupItemsData.nextID);
2159 if (groupItemsData.activeGroupId)
2160 activeGroupId = groupItemsData.activeGroupId;
2161 }
2163 if (groupItemData) {
2164 var toClose = this.groupItems.concat();
2165 for (var id in groupItemData) {
2166 let data = groupItemData[id];
2167 if (this.groupItemStorageSanity(data)) {
2168 let groupItem = this.groupItem(data.id);
2169 if (groupItem && !groupItem.hidden) {
2170 groupItem.userSize = data.userSize;
2171 groupItem.setTitle(data.title);
2172 groupItem.setBounds(data.bounds, true);
2174 let index = toClose.indexOf(groupItem);
2175 if (index != -1)
2176 toClose.splice(index, 1);
2177 } else {
2178 var options = {
2179 dontPush: true,
2180 immediately: true
2181 };
2183 new GroupItem([], Utils.extend({}, data, options));
2184 }
2185 }
2186 }
2188 toClose.forEach(function(groupItem) {
2189 // all tabs still existing in closed groups will be moved to new
2190 // groups. prepare them to be reconnected later.
2191 groupItem.getChildren().forEach(function (tabItem) {
2192 if (tabItem.parent.hidden)
2193 iQ(tabItem.container).show();
2195 tabItem._reconnected = false;
2197 // sanity check the tab's groupID
2198 let tabData = Storage.getTabData(tabItem.tab);
2200 if (tabData) {
2201 let parentGroup = GroupItems.groupItem(tabData.groupID);
2203 // the tab's group id could be invalid or point to a non-existing
2204 // group. correct it by assigning the active group id or the first
2205 // group of the just restored session.
2206 if (!parentGroup || -1 < toClose.indexOf(parentGroup)) {
2207 tabData.groupID = activeGroupId || Object.keys(groupItemData)[0];
2208 Storage.saveTab(tabItem.tab, tabData);
2209 }
2210 }
2211 });
2213 // this closes the group but not its children
2214 groupItem.close({immediately: true});
2215 });
2216 }
2218 // set active group item
2219 if (activeGroupId) {
2220 let activeGroupItem = this.groupItem(activeGroupId);
2221 if (activeGroupItem)
2222 UI.setActive(activeGroupItem);
2223 }
2225 this._inited = true;
2226 this._save(); // for nextID
2227 } catch(e) {
2228 Utils.log("error in recons: "+e);
2229 }
2230 },
2232 // ----------
2233 // Function: load
2234 // Loads the storage data for groups.
2235 // Returns true if there was global group data.
2236 load: function GroupItems_load() {
2237 let groupItemsData = Storage.readGroupItemsData(gWindow);
2238 let groupItemData = Storage.readGroupItemData(gWindow);
2239 this.reconstitute(groupItemsData, groupItemData);
2241 return (groupItemsData && !Utils.isEmptyObject(groupItemsData));
2242 },
2244 // ----------
2245 // Function: groupItemStorageSanity
2246 // Given persistent storage data for a groupItem, returns true if it appears to not be damaged.
2247 groupItemStorageSanity: function GroupItems_groupItemStorageSanity(groupItemData) {
2248 let sane = true;
2249 if (!groupItemData.bounds || !Utils.isRect(groupItemData.bounds)) {
2250 Utils.log('GroupItems.groupItemStorageSanity: bad bounds', groupItemData.bounds);
2251 sane = false;
2252 } else if ((groupItemData.userSize &&
2253 !Utils.isPoint(groupItemData.userSize)) ||
2254 !groupItemData.id) {
2255 sane = false;
2256 }
2258 return sane;
2259 },
2261 // ----------
2262 // Function: register
2263 // Adds the given <GroupItem> to the list of groupItems we're tracking.
2264 register: function GroupItems_register(groupItem) {
2265 Utils.assert(groupItem, 'groupItem');
2266 Utils.assert(this.groupItems.indexOf(groupItem) == -1, 'only register once per groupItem');
2267 this.groupItems.push(groupItem);
2268 UI.updateTabButton();
2269 },
2271 // ----------
2272 // Function: unregister
2273 // Removes the given <GroupItem> from the list of groupItems we're tracking.
2274 unregister: function GroupItems_unregister(groupItem) {
2275 var index = this.groupItems.indexOf(groupItem);
2276 if (index != -1)
2277 this.groupItems.splice(index, 1);
2279 if (groupItem == this._activeGroupItem)
2280 this._activeGroupItem = null;
2282 this._arrangesPending = this._arrangesPending.filter(function (pending) {
2283 return groupItem != pending.groupItem;
2284 });
2286 this._lastActiveList.remove(groupItem);
2287 UI.updateTabButton();
2288 },
2290 // ----------
2291 // Function: groupItem
2292 // Given some sort of identifier, returns the appropriate groupItem.
2293 // Currently only supports groupItem ids.
2294 groupItem: function GroupItems_groupItem(a) {
2295 if (!this.groupItems) {
2296 // uninit has been called
2297 return null;
2298 }
2299 var result = null;
2300 this.groupItems.forEach(function(candidate) {
2301 if (candidate.id == a)
2302 result = candidate;
2303 });
2305 return result;
2306 },
2308 // ----------
2309 // Function: removeAll
2310 // Removes all tabs from all groupItems (which automatically closes all unnamed groupItems).
2311 removeAll: function GroupItems_removeAll() {
2312 var toRemove = this.groupItems.concat();
2313 toRemove.forEach(function(groupItem) {
2314 groupItem.removeAll();
2315 });
2316 },
2318 // ----------
2319 // Function: newTab
2320 // Given a <TabItem>, files it in the appropriate groupItem.
2321 newTab: function GroupItems_newTab(tabItem, options) {
2322 let activeGroupItem = this.getActiveGroupItem();
2324 // 1. Active group
2325 // 2. First visible non-app tab (that's not the tab in question)
2326 // 3. First group
2327 // 4. At this point there should be no groups or tabs (except for app tabs and the
2328 // tab in question): make a new group
2330 if (activeGroupItem && !activeGroupItem.hidden) {
2331 activeGroupItem.add(tabItem, options);
2332 return;
2333 }
2335 let targetGroupItem;
2336 // find first non-app visible tab belongs a group, and add the new tabItem
2337 // to that group
2338 gBrowser.visibleTabs.some(function(tab) {
2339 if (!tab.pinned && tab != tabItem.tab) {
2340 if (tab._tabViewTabItem && tab._tabViewTabItem.parent &&
2341 !tab._tabViewTabItem.parent.hidden) {
2342 targetGroupItem = tab._tabViewTabItem.parent;
2343 }
2344 return true;
2345 }
2346 return false;
2347 });
2349 let visibleGroupItems;
2350 if (targetGroupItem) {
2351 // add the new tabItem to the first group item
2352 targetGroupItem.add(tabItem);
2353 UI.setActive(targetGroupItem);
2354 return;
2355 } else {
2356 // find the first visible group item
2357 visibleGroupItems = this.groupItems.filter(function(groupItem) {
2358 return (!groupItem.hidden);
2359 });
2360 if (visibleGroupItems.length > 0) {
2361 visibleGroupItems[0].add(tabItem);
2362 UI.setActive(visibleGroupItems[0]);
2363 return;
2364 }
2365 }
2367 // create new group for the new tabItem
2368 tabItem.setPosition(60, 60, true);
2369 let newGroupItemBounds = tabItem.getBounds();
2371 newGroupItemBounds.inset(-40,-40);
2372 let newGroupItem = new GroupItem([tabItem], { bounds: newGroupItemBounds });
2373 newGroupItem.snap();
2374 UI.setActive(newGroupItem);
2375 },
2377 // ----------
2378 // Function: getActiveGroupItem
2379 // Returns the active groupItem. Active means its tabs are
2380 // shown in the tab bar when not in the TabView interface.
2381 getActiveGroupItem: function GroupItems_getActiveGroupItem() {
2382 return this._activeGroupItem;
2383 },
2385 // ----------
2386 // Function: setActiveGroupItem
2387 // Sets the active groupItem, thereby showing only the relevant tabs and
2388 // setting the groupItem which will receive new tabs.
2389 //
2390 // Paramaters:
2391 // groupItem - the active <GroupItem>
2392 setActiveGroupItem: function GroupItems_setActiveGroupItem(groupItem) {
2393 Utils.assert(groupItem, "groupItem must be given");
2395 if (this._activeGroupItem)
2396 iQ(this._activeGroupItem.container).removeClass('activeGroupItem');
2398 iQ(groupItem.container).addClass('activeGroupItem');
2400 this._lastActiveList.update(groupItem);
2401 this._activeGroupItem = groupItem;
2402 this._save();
2403 },
2405 // ----------
2406 // Function: getLastActiveGroupItem
2407 // Gets last active group item.
2408 // Returns the <groupItem>. If nothing is found, return null.
2409 getLastActiveGroupItem: function GroupItem_getLastActiveGroupItem() {
2410 return this._lastActiveList.peek(function(groupItem) {
2411 return (groupItem && !groupItem.hidden && groupItem.getChildren().length > 0)
2412 });
2413 },
2415 // ----------
2416 // Function: _updateTabBar
2417 // Hides and shows tabs in the tab bar based on the active groupItem
2418 _updateTabBar: function GroupItems__updateTabBar() {
2419 if (!window.UI)
2420 return; // called too soon
2422 Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
2424 let tabItems = this._activeGroupItem._children;
2425 gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
2426 },
2428 // ----------
2429 // Function: updateActiveGroupItemAndTabBar
2430 // Sets active TabItem and GroupItem, and updates tab bar appropriately.
2431 // Parameters:
2432 // tabItem - the tab item
2433 // options - is passed to UI.setActive() directly
2434 updateActiveGroupItemAndTabBar:
2435 function GroupItems_updateActiveGroupItemAndTabBar(tabItem, options) {
2436 Utils.assertThrow(tabItem && tabItem.isATabItem, "tabItem must be a TabItem");
2438 UI.setActive(tabItem, options);
2439 this._updateTabBar();
2440 },
2442 // ----------
2443 // Function: getNextGroupItemTab
2444 // Paramaters:
2445 // reverse - the boolean indicates the direction to look for the next groupItem.
2446 // Returns the <tabItem>. If nothing is found, return null.
2447 getNextGroupItemTab: function GroupItems_getNextGroupItemTab(reverse) {
2448 var groupItems = Utils.copy(GroupItems.groupItems);
2449 var activeGroupItem = GroupItems.getActiveGroupItem();
2450 var tabItem = null;
2452 if (reverse)
2453 groupItems = groupItems.reverse();
2455 if (!activeGroupItem) {
2456 if (groupItems.length > 0) {
2457 groupItems.some(function(groupItem) {
2458 if (!groupItem.hidden) {
2459 // restore the last active tab in the group
2460 let activeTab = groupItem.getActiveTab();
2461 if (activeTab) {
2462 tabItem = activeTab;
2463 return true;
2464 }
2465 // if no tab is active, use the first one
2466 var child = groupItem.getChild(0);
2467 if (child) {
2468 tabItem = child;
2469 return true;
2470 }
2471 }
2472 return false;
2473 });
2474 }
2475 } else {
2476 var currentIndex;
2477 groupItems.some(function(groupItem, index) {
2478 if (!groupItem.hidden && groupItem == activeGroupItem) {
2479 currentIndex = index;
2480 return true;
2481 }
2482 return false;
2483 });
2484 var firstGroupItems = groupItems.slice(currentIndex + 1);
2485 firstGroupItems.some(function(groupItem) {
2486 if (!groupItem.hidden) {
2487 // restore the last active tab in the group
2488 let activeTab = groupItem.getActiveTab();
2489 if (activeTab) {
2490 tabItem = activeTab;
2491 return true;
2492 }
2493 // if no tab is active, use the first one
2494 var child = groupItem.getChild(0);
2495 if (child) {
2496 tabItem = child;
2497 return true;
2498 }
2499 }
2500 return false;
2501 });
2502 if (!tabItem) {
2503 var secondGroupItems = groupItems.slice(0, currentIndex);
2504 secondGroupItems.some(function(groupItem) {
2505 if (!groupItem.hidden) {
2506 // restore the last active tab in the group
2507 let activeTab = groupItem.getActiveTab();
2508 if (activeTab) {
2509 tabItem = activeTab;
2510 return true;
2511 }
2512 // if no tab is active, use the first one
2513 var child = groupItem.getChild(0);
2514 if (child) {
2515 tabItem = child;
2516 return true;
2517 }
2518 }
2519 return false;
2520 });
2521 }
2522 }
2523 return tabItem;
2524 },
2526 // ----------
2527 // Function: moveTabToGroupItem
2528 // Used for the right click menu in the tab strip; moves the given tab
2529 // into the given group. Does nothing if the tab is an app tab.
2530 // Paramaters:
2531 // tab - the <xul:tab>.
2532 // groupItemId - the <groupItem>'s id. If nothing, create a new <groupItem>.
2533 moveTabToGroupItem : function GroupItems_moveTabToGroupItem(tab, groupItemId) {
2534 if (tab.pinned)
2535 return;
2537 Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem");
2539 // given tab is already contained in target group
2540 if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
2541 return;
2543 let shouldUpdateTabBar = false;
2544 let shouldShowTabView = false;
2545 let groupItem;
2547 // switch to the appropriate tab first.
2548 if (tab.selected) {
2549 if (gBrowser.visibleTabs.length > 1) {
2550 gBrowser._blurTab(tab);
2551 shouldUpdateTabBar = true;
2552 } else {
2553 shouldShowTabView = true;
2554 }
2555 } else {
2556 shouldUpdateTabBar = true
2557 }
2559 // remove tab item from a groupItem
2560 if (tab._tabViewTabItem.parent)
2561 tab._tabViewTabItem.parent.remove(tab._tabViewTabItem);
2563 // add tab item to a groupItem
2564 if (groupItemId) {
2565 groupItem = GroupItems.groupItem(groupItemId);
2566 groupItem.add(tab._tabViewTabItem);
2567 groupItem.reorderTabsBasedOnTabItemOrder()
2568 } else {
2569 let pageBounds = Items.getPageBounds();
2570 pageBounds.inset(20, 20);
2572 let box = new Rect(pageBounds);
2573 box.width = 250;
2574 box.height = 200;
2576 new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
2577 }
2579 if (shouldUpdateTabBar)
2580 this._updateTabBar();
2581 else if (shouldShowTabView)
2582 UI.showTabView();
2583 },
2585 // ----------
2586 // Function: removeHiddenGroups
2587 // Removes all hidden groups' data and its browser tabs.
2588 removeHiddenGroups: function GroupItems_removeHiddenGroups() {
2589 if (this._removingHiddenGroups)
2590 return;
2591 this._removingHiddenGroups = true;
2593 let groupItems = this.groupItems.concat();
2594 groupItems.forEach(function(groupItem) {
2595 if (groupItem.hidden)
2596 groupItem.closeHidden();
2597 });
2599 this._removingHiddenGroups = false;
2600 },
2602 // ----------
2603 // Function: getUnclosableGroupItemId
2604 // If there's only one (non-hidden) group, and there are app tabs present,
2605 // returns that group.
2606 // Return the <GroupItem>'s Id
2607 getUnclosableGroupItemId: function GroupItems_getUnclosableGroupItemId() {
2608 let unclosableGroupItemId = null;
2610 if (gBrowser._numPinnedTabs > 0) {
2611 let hiddenGroupItems =
2612 this.groupItems.concat().filter(function(groupItem) {
2613 return !groupItem.hidden;
2614 });
2615 if (hiddenGroupItems.length == 1)
2616 unclosableGroupItemId = hiddenGroupItems[0].id;
2617 }
2619 return unclosableGroupItemId;
2620 },
2622 // ----------
2623 // Function: updateGroupCloseButtons
2624 // Updates group close buttons.
2625 updateGroupCloseButtons: function GroupItems_updateGroupCloseButtons() {
2626 let unclosableGroupItemId = this.getUnclosableGroupItemId();
2628 if (unclosableGroupItemId) {
2629 let groupItem = this.groupItem(unclosableGroupItemId);
2631 if (groupItem) {
2632 groupItem.$closeButton.hide();
2633 }
2634 } else {
2635 this.groupItems.forEach(function(groupItem) {
2636 groupItem.$closeButton.show();
2637 });
2638 }
2639 },
2641 // ----------
2642 // Function: calcValidSize
2643 // Basic measure rules. Assures that item is a minimum size.
2644 calcValidSize: function GroupItems_calcValidSize(size, options) {
2645 Utils.assert(Utils.isPoint(size), 'input is a Point');
2646 Utils.assert((size.x>0 || size.y>0) && (size.x!=0 && size.y!=0),
2647 "dimensions are valid:"+size.x+","+size.y);
2648 return new Point(
2649 Math.max(size.x, GroupItems.minGroupWidth),
2650 Math.max(size.y, GroupItems.minGroupHeight));
2651 },
2653 // ----------
2654 // Function: pauseAutoclose()
2655 // Temporarily disable the behavior that closes groups when they become
2656 // empty. This is used when entering private browsing, to avoid trashing the
2657 // user's groups while private browsing is shuffling things around.
2658 pauseAutoclose: function GroupItems_pauseAutoclose() {
2659 this._autoclosePaused = true;
2660 },
2662 // ----------
2663 // Function: unpauseAutoclose()
2664 // Re-enables the auto-close behavior.
2665 resumeAutoclose: function GroupItems_resumeAutoclose() {
2666 this._autoclosePaused = false;
2667 }
2668 };