michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // ********** michael@0: // Title: groupItems.js michael@0: michael@0: // ########## michael@0: // Class: GroupItem michael@0: // A single groupItem in the TabView window. Descended from . michael@0: // Note that it implements the interface. michael@0: // michael@0: // ---------- michael@0: // Constructor: GroupItem michael@0: // michael@0: // Parameters: michael@0: // listOfEls - an array of DOM elements for tabs to be added to this groupItem michael@0: // options - various options for this groupItem (see below). In addition, gets passed michael@0: // to along with the elements provided. michael@0: // michael@0: // Possible options: michael@0: // id - specifies the groupItem's id; otherwise automatically generated michael@0: // userSize - see ; default is null michael@0: // bounds - a ; otherwise based on the locations of the provided elements michael@0: // container - a DOM element to use as the container for this groupItem; otherwise will create michael@0: // title - the title for the groupItem; otherwise blank michael@0: // focusTitle - focus the title's input field after creation michael@0: // dontPush - true if this groupItem shouldn't push away or snap on creation; default is false michael@0: // immediately - true if we want all placement immediately, not with animation michael@0: function GroupItem(listOfEls, options) { michael@0: if (!options) michael@0: options = {}; michael@0: michael@0: this._inited = false; michael@0: this._uninited = false; michael@0: this._children = []; // an array of Items michael@0: this.isAGroupItem = true; michael@0: this.id = options.id || GroupItems.getNextID(); michael@0: this._isStacked = false; michael@0: this.expanded = null; michael@0: this.hidden = false; michael@0: this.fadeAwayUndoButtonDelay = 15000; michael@0: this.fadeAwayUndoButtonDuration = 300; michael@0: michael@0: this.keepProportional = false; michael@0: this._frozenItemSizeData = {}; michael@0: michael@0: this._onChildClose = this._onChildClose.bind(this); michael@0: michael@0: // Variable: _activeTab michael@0: // The for the groupItem's active tab. michael@0: this._activeTab = null; michael@0: michael@0: if (Utils.isPoint(options.userSize)) michael@0: this.userSize = new Point(options.userSize); michael@0: michael@0: var self = this; michael@0: michael@0: var rectToBe; michael@0: if (options.bounds) { michael@0: Utils.assert(Utils.isRect(options.bounds), "options.bounds must be a Rect"); michael@0: rectToBe = new Rect(options.bounds); michael@0: } michael@0: michael@0: if (!rectToBe) { michael@0: rectToBe = GroupItems.getBoundingBox(listOfEls); michael@0: rectToBe.inset(-42, -42); michael@0: } michael@0: michael@0: var $container = options.container; michael@0: let immediately = options.immediately || $container ? true : false; michael@0: if (!$container) { michael@0: $container = iQ('
') michael@0: .addClass('groupItem') michael@0: .css({position: 'absolute'}) michael@0: .css(rectToBe); michael@0: } michael@0: michael@0: this.bounds = $container.bounds(); michael@0: michael@0: this.isDragging = false; michael@0: $container michael@0: .css({zIndex: -100}) michael@0: .attr("data-id", this.id) michael@0: .appendTo("body"); michael@0: michael@0: // ___ Resizer michael@0: this.$resizer = iQ("
") michael@0: .addClass('resizer') michael@0: .appendTo($container) michael@0: .hide(); michael@0: michael@0: // ___ Titlebar michael@0: var html = michael@0: "
" + michael@0: "" + michael@0: "
" + michael@0: "
"; michael@0: michael@0: this.$titlebar = iQ('
') michael@0: .addClass('titlebar') michael@0: .html(html) michael@0: .appendTo($container); michael@0: michael@0: this.$closeButton = iQ('
') michael@0: .addClass('close') michael@0: .click(function() { michael@0: self.closeAll(); michael@0: }) michael@0: .attr("title", tabviewString("groupItem.closeGroup")) michael@0: .appendTo($container); michael@0: michael@0: // ___ Title michael@0: this.$titleContainer = iQ('.title-container', this.$titlebar); michael@0: this.$title = iQ('.name', this.$titlebar).attr('placeholder', this.defaultName); michael@0: this.$titleShield = iQ('.title-shield', this.$titlebar); michael@0: this.setTitle(options.title); michael@0: michael@0: var handleKeyPress = function (e) { michael@0: if (e.keyCode == KeyEvent.DOM_VK_ESCAPE || michael@0: e.keyCode == KeyEvent.DOM_VK_RETURN) { michael@0: (self.$title)[0].blur(); michael@0: self.$title michael@0: .addClass("transparentBorder") michael@0: .one("mouseout", function() { michael@0: self.$title.removeClass("transparentBorder"); michael@0: }); michael@0: e.stopPropagation(); michael@0: e.preventDefault(); michael@0: } michael@0: }; michael@0: michael@0: var handleKeyUp = function(e) { michael@0: // NOTE: When user commits or cancels IME composition, the last key michael@0: // event fires only a keyup event. Then, we shouldn't take any michael@0: // reactions but we should update our status. michael@0: self.save(); michael@0: }; michael@0: michael@0: this.$title michael@0: .blur(function() { michael@0: self._titleFocused = false; michael@0: self.$title[0].setSelectionRange(0, 0); michael@0: self.$titleShield.show(); michael@0: if (self.getTitle()) michael@0: gTabView.firstUseExperienced = true; michael@0: self.save(); michael@0: }) michael@0: .focus(function() { michael@0: self._unfreezeItemSize(); michael@0: if (!self._titleFocused) { michael@0: (self.$title)[0].select(); michael@0: self._titleFocused = true; michael@0: } michael@0: }) michael@0: .mousedown(function(e) { michael@0: e.stopPropagation(); michael@0: }) michael@0: .keypress(handleKeyPress) michael@0: .keyup(handleKeyUp) michael@0: .attr("title", tabviewString("groupItem.defaultName")); michael@0: michael@0: this.$titleShield michael@0: .mousedown(function(e) { michael@0: self.lastMouseDownTarget = (Utils.isLeftClick(e) ? e.target : null); michael@0: }) michael@0: .mouseup(function(e) { michael@0: var same = (e.target == self.lastMouseDownTarget); michael@0: self.lastMouseDownTarget = null; michael@0: if (!same) michael@0: return; michael@0: michael@0: if (!self.isDragging) michael@0: self.focusTitle(); michael@0: }) michael@0: .attr("title", tabviewString("groupItem.defaultName")); michael@0: michael@0: if (options.focusTitle) michael@0: this.focusTitle(); michael@0: michael@0: // ___ Stack Expander michael@0: this.$expander = iQ("
") michael@0: .addClass("stackExpander") michael@0: .appendTo($container) michael@0: .hide(); michael@0: michael@0: // ___ app tabs: create app tab tray and populate it michael@0: let appTabTrayContainer = iQ("
") michael@0: .addClass("appTabTrayContainer") michael@0: .appendTo($container); michael@0: this.$appTabTray = iQ("
") michael@0: .addClass("appTabTray") michael@0: .appendTo(appTabTrayContainer); michael@0: michael@0: let pinnedTabCount = gBrowser._numPinnedTabs; michael@0: AllTabs.tabs.forEach(function (xulTab, index) { michael@0: // only adjust tray when it's the last app tab. michael@0: if (xulTab.pinned) michael@0: this.addAppTab(xulTab, {dontAdjustTray: index + 1 < pinnedTabCount}); michael@0: }, this); michael@0: michael@0: // ___ Undo Close michael@0: this.$undoContainer = null; michael@0: this._undoButtonTimeoutId = null; michael@0: michael@0: // ___ Superclass initialization michael@0: this._init($container[0]); michael@0: michael@0: // ___ Children michael@0: // We explicitly set dontArrange=true to prevent the groupItem from michael@0: // re-arranging its children after a tabItem has been added. This saves us a michael@0: // group.arrange() call per child and therefore some tab.setBounds() calls. michael@0: options.dontArrange = true; michael@0: listOfEls.forEach(function (el) { michael@0: self.add(el, options); michael@0: }); michael@0: michael@0: // ___ Finish Up michael@0: this._addHandlers($container); michael@0: michael@0: this.setResizable(true, immediately); michael@0: michael@0: GroupItems.register(this); michael@0: michael@0: // ___ Position michael@0: this.setBounds(rectToBe, immediately); michael@0: if (options.dontPush) { michael@0: this.setZ(drag.zIndex); michael@0: drag.zIndex++; michael@0: } else { michael@0: // Calling snap will also trigger pushAway michael@0: this.snap(immediately); michael@0: } michael@0: michael@0: if (!options.immediately && listOfEls.length > 0) michael@0: $container.hide().fadeIn(); michael@0: michael@0: this._inited = true; michael@0: this.save(); michael@0: michael@0: GroupItems.updateGroupCloseButtons(); michael@0: }; michael@0: michael@0: // ---------- michael@0: GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [GroupItem id=id] for debug use michael@0: toString: function GroupItem_toString() { michael@0: return "[GroupItem id=" + this.id + "]"; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Variable: defaultName michael@0: // The prompt text for the title field. michael@0: defaultName: tabviewString('groupItem.defaultName'), michael@0: michael@0: // ----------- michael@0: // Function: setActiveTab michael@0: // Sets the active for this groupItem; can be null, but only michael@0: // if there are no children. michael@0: setActiveTab: function GroupItem_setActiveTab(tab) { michael@0: Utils.assertThrow((!tab && this._children.length == 0) || tab.isATabItem, michael@0: "tab must be null (if no children) or a TabItem"); michael@0: michael@0: this._activeTab = tab; michael@0: michael@0: if (this.isStacked()) michael@0: this.arrange({immediately: true}); michael@0: }, michael@0: michael@0: // ----------- michael@0: // Function: getActiveTab michael@0: // Gets the active for this groupItem; can be null, but only michael@0: // if there are no children. michael@0: getActiveTab: function GroupItem_getActiveTab() { michael@0: return this._activeTab; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getStorageData michael@0: // Returns all of the info worth storing about this groupItem. michael@0: getStorageData: function GroupItem_getStorageData() { michael@0: var data = { michael@0: bounds: this.getBounds(), michael@0: userSize: null, michael@0: title: this.getTitle(), michael@0: id: this.id michael@0: }; michael@0: michael@0: if (Utils.isPoint(this.userSize)) michael@0: data.userSize = new Point(this.userSize); michael@0: michael@0: return data; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isEmpty michael@0: // Returns true if the tab groupItem is empty and unnamed. michael@0: isEmpty: function GroupItem_isEmpty() { michael@0: return !this._children.length && !this.getTitle(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isStacked michael@0: // Returns true if this item is in a stacked groupItem. michael@0: isStacked: function GroupItem_isStacked() { michael@0: return this._isStacked; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isTopOfStack michael@0: // Returns true if the item is showing on top of this group's stack, michael@0: // determined by whether the tab is this group's topChild, or michael@0: // if it doesn't have one, its first child. michael@0: isTopOfStack: function GroupItem_isTopOfStack(item) { michael@0: return this.isStacked() && item == this.getTopChild(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: save michael@0: // Saves this groupItem to persistent storage. michael@0: save: function GroupItem_save() { michael@0: if (!this._inited || this._uninited) // too soon/late to save michael@0: return; michael@0: michael@0: var data = this.getStorageData(); michael@0: if (GroupItems.groupItemStorageSanity(data)) michael@0: Storage.saveGroupItem(gWindow, data); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: deleteData michael@0: // Deletes the groupItem in the persistent storage. michael@0: deleteData: function GroupItem_deleteData() { michael@0: this._uninited = true; michael@0: Storage.deleteGroupItem(gWindow, this.id); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getTitle michael@0: // Returns the title of this groupItem as a string. michael@0: getTitle: function GroupItem_getTitle() { michael@0: return this.$title ? this.$title.val() : ''; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setTitle michael@0: // Sets the title of this groupItem with the given string michael@0: setTitle: function GroupItem_setTitle(value) { michael@0: this.$title.val(value); michael@0: this.save(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: focusTitle michael@0: // Hide the title's shield and focus the underlying input field. michael@0: focusTitle: function GroupItem_focusTitle() { michael@0: this.$titleShield.hide(); michael@0: this.$title[0].focus(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: adjustAppTabTray michael@0: // Used to adjust the appTabTray size, to split the appTabIcons across michael@0: // multiple columns when needed - if the groupItem size is too small. michael@0: // michael@0: // Parameters: michael@0: // arrangeGroup - rearrange the groupItem if the number of appTab columns michael@0: // changes. If true, then this.arrange() is called, otherwise not. michael@0: adjustAppTabTray: function GroupItem_adjustAppTabTray(arrangeGroup) { michael@0: let icons = iQ(".appTabIcon", this.$appTabTray); michael@0: let container = iQ(this.$appTabTray[0].parentNode); michael@0: if (!icons.length) { michael@0: // There are no icons, so hide the appTabTray if needed. michael@0: if (parseInt(container.css("width")) != 0) { michael@0: this.$appTabTray.css("-moz-column-count", "auto"); michael@0: this.$appTabTray.css("height", 0); michael@0: container.css("width", 0); michael@0: container.css("height", 0); michael@0: michael@0: if (container.hasClass("appTabTrayContainerTruncated")) michael@0: container.removeClass("appTabTrayContainerTruncated"); michael@0: michael@0: if (arrangeGroup) michael@0: this.arrange(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let iconBounds = iQ(icons[0]).bounds(); michael@0: let boxBounds = this.getBounds(); michael@0: let contentHeight = boxBounds.height - michael@0: parseInt(container.css("top")) - michael@0: this.$resizer.height(); michael@0: let rows = Math.floor(contentHeight / iconBounds.height); michael@0: let columns = Math.ceil(icons.length / rows); michael@0: let columnsGap = parseInt(this.$appTabTray.css("-moz-column-gap")); michael@0: let iconWidth = iconBounds.width + columnsGap; michael@0: let maxColumns = Math.floor((boxBounds.width * 0.20) / iconWidth); michael@0: michael@0: Utils.assert(rows > 0 && columns > 0 && maxColumns > 0, michael@0: "make sure the calculated rows, columns and maxColumns are correct"); michael@0: michael@0: if (columns > maxColumns) michael@0: container.addClass("appTabTrayContainerTruncated"); michael@0: else if (container.hasClass("appTabTrayContainerTruncated")) michael@0: container.removeClass("appTabTrayContainerTruncated"); michael@0: michael@0: // Need to drop the -moz- prefix when Gecko makes it obsolete. michael@0: // See bug 629452. michael@0: if (parseInt(this.$appTabTray.css("-moz-column-count")) != columns) michael@0: this.$appTabTray.css("-moz-column-count", columns); michael@0: michael@0: if (parseInt(this.$appTabTray.css("height")) != contentHeight) { michael@0: this.$appTabTray.css("height", contentHeight + "px"); michael@0: container.css("height", contentHeight + "px"); michael@0: } michael@0: michael@0: let fullTrayWidth = iconWidth * columns - columnsGap; michael@0: if (parseInt(this.$appTabTray.css("width")) != fullTrayWidth) michael@0: this.$appTabTray.css("width", fullTrayWidth + "px"); michael@0: michael@0: let trayWidth = iconWidth * Math.min(columns, maxColumns) - columnsGap; michael@0: if (parseInt(container.css("width")) != trayWidth) { michael@0: container.css("width", trayWidth + "px"); michael@0: michael@0: // Rearrange the groupItem if the width changed. michael@0: if (arrangeGroup) michael@0: this.arrange(); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getContentBounds michael@0: // Returns a for the groupItem's content area (which doesn't include the title, etc). michael@0: // michael@0: // Parameters: michael@0: // options - an object with additional parameters, see below michael@0: // michael@0: // Possible options: michael@0: // stacked - true to get content bounds for stacked mode michael@0: getContentBounds: function GroupItem_getContentBounds(options) { michael@0: let box = this.getBounds(); michael@0: let titleHeight = this.$titlebar.height(); michael@0: box.top += titleHeight; michael@0: box.height -= titleHeight; michael@0: michael@0: let appTabTrayContainer = iQ(this.$appTabTray[0].parentNode); michael@0: let appTabTrayWidth = appTabTrayContainer.width(); michael@0: if (appTabTrayWidth) michael@0: appTabTrayWidth += parseInt(appTabTrayContainer.css(UI.rtl ? "left" : "right")); michael@0: michael@0: box.width -= appTabTrayWidth; michael@0: if (UI.rtl) { michael@0: box.left += appTabTrayWidth; michael@0: } michael@0: michael@0: // Make the computed bounds' "padding" and expand button margin actually be michael@0: // themeable --OR-- compute this from actual bounds. Bug 586546 michael@0: box.inset(6, 6); michael@0: michael@0: // make some room for the expand button in stacked mode michael@0: if (options && options.stacked) michael@0: box.height -= this.$expander.height() + 9; // the button height plus padding michael@0: michael@0: return box; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setBounds michael@0: // Sets the bounds with the given , animating unless "immediately" is false. michael@0: // michael@0: // Parameters: michael@0: // inRect - a giving the new bounds michael@0: // immediately - true if it should not animate; default false michael@0: // options - an object with additional parameters, see below michael@0: // michael@0: // Possible options: michael@0: // force - true to always update the DOM even if the bounds haven't changed; default false michael@0: setBounds: function GroupItem_setBounds(inRect, immediately, options) { michael@0: Utils.assert(Utils.isRect(inRect), 'GroupItem.setBounds: rect is not a real rectangle!'); michael@0: michael@0: // Validate and conform passed in size michael@0: let validSize = GroupItems.calcValidSize( michael@0: new Point(inRect.width, inRect.height)); michael@0: let rect = new Rect(inRect.left, inRect.top, validSize.x, validSize.y); michael@0: michael@0: if (!options) michael@0: options = {}; michael@0: michael@0: var titleHeight = this.$titlebar.height(); michael@0: michael@0: // ___ Determine what has changed michael@0: var css = {}; michael@0: var titlebarCSS = {}; michael@0: var contentCSS = {}; michael@0: michael@0: if (rect.left != this.bounds.left || options.force) michael@0: css.left = rect.left; michael@0: michael@0: if (rect.top != this.bounds.top || options.force) michael@0: css.top = rect.top; michael@0: michael@0: if (rect.width != this.bounds.width || options.force) { michael@0: css.width = rect.width; michael@0: titlebarCSS.width = rect.width; michael@0: contentCSS.width = rect.width; michael@0: } michael@0: michael@0: if (rect.height != this.bounds.height || options.force) { michael@0: css.height = rect.height; michael@0: contentCSS.height = rect.height - titleHeight; michael@0: } michael@0: michael@0: if (Utils.isEmptyObject(css)) michael@0: return; michael@0: michael@0: var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top); michael@0: this.bounds = new Rect(rect); michael@0: michael@0: // Make sure the AppTab icons fit the new groupItem size. michael@0: if (css.width || css.height) michael@0: this.adjustAppTabTray(); michael@0: michael@0: // ___ Deal with children michael@0: if (css.width || css.height) { michael@0: this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)}); michael@0: } else if (css.left || css.top) { michael@0: this._children.forEach(function(child) { michael@0: if (!child.getHidden()) { michael@0: var box = child.getBounds(); michael@0: child.setPosition(box.left + offset.x, box.top + offset.y, immediately); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // ___ Update our representation michael@0: if (immediately) { michael@0: iQ(this.container).css(css); michael@0: this.$titlebar.css(titlebarCSS); michael@0: } else { michael@0: TabItems.pausePainting(); michael@0: iQ(this.container).animate(css, { michael@0: duration: 350, michael@0: easing: "tabviewBounce", michael@0: complete: function() { michael@0: TabItems.resumePainting(); michael@0: } michael@0: }); michael@0: michael@0: this.$titlebar.animate(titlebarCSS, { michael@0: duration: 350 michael@0: }); michael@0: } michael@0: michael@0: UI.clearShouldResizeItems(); michael@0: this.setTrenches(rect); michael@0: this.save(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setZ michael@0: // Set the Z order for the groupItem's container, as well as its children. michael@0: setZ: function GroupItem_setZ(value) { michael@0: this.zIndex = value; michael@0: michael@0: iQ(this.container).css({zIndex: value}); michael@0: michael@0: var count = this._children.length; michael@0: if (count) { michael@0: var topZIndex = value + count + 1; michael@0: var zIndex = topZIndex; michael@0: var self = this; michael@0: this._children.forEach(function(child) { michael@0: if (child == self.getTopChild()) michael@0: child.setZ(topZIndex + 1); michael@0: else { michael@0: child.setZ(zIndex); michael@0: zIndex--; michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: close michael@0: // Closes the groupItem, removing (but not closing) all of its children. michael@0: // michael@0: // Parameters: michael@0: // options - An object with optional settings for this call. michael@0: // michael@0: // Options: michael@0: // immediately - (bool) if true, no animation will be used michael@0: close: function GroupItem_close(options) { michael@0: this.removeAll({dontClose: true}); michael@0: GroupItems.unregister(this); michael@0: michael@0: // remove unfreeze event handlers, if item size is frozen michael@0: this._unfreezeItemSize({dontArrange: true}); michael@0: michael@0: let self = this; michael@0: let destroyGroup = function () { michael@0: iQ(self.container).remove(); michael@0: if (self.$undoContainer) { michael@0: self.$undoContainer.remove(); michael@0: self.$undoContainer = null; michael@0: } michael@0: self.removeTrenches(); michael@0: Items.unsquish(); michael@0: self._sendToSubscribers("close"); michael@0: GroupItems.updateGroupCloseButtons(); michael@0: } michael@0: michael@0: if (this.hidden || (options && options.immediately)) { michael@0: destroyGroup(); michael@0: } else { michael@0: iQ(this.container).animate({ michael@0: opacity: 0, michael@0: "transform": "scale(.3)", michael@0: }, { michael@0: duration: 170, michael@0: complete: destroyGroup michael@0: }); michael@0: } michael@0: michael@0: this.deleteData(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: closeAll michael@0: // Closes the groupItem and all of its children. michael@0: closeAll: function GroupItem_closeAll() { michael@0: if (this._children.length > 0) { michael@0: this._unfreezeItemSize(); michael@0: this._children.forEach(function(child) { michael@0: iQ(child.container).hide(); michael@0: }); michael@0: michael@0: iQ(this.container).animate({ michael@0: opacity: 0, michael@0: "transform": "scale(.3)", michael@0: }, { michael@0: duration: 170, michael@0: complete: function() { michael@0: iQ(this).hide(); michael@0: } michael@0: }); michael@0: michael@0: this.droppable(false); michael@0: this.removeTrenches(); michael@0: this._createUndoButton(); michael@0: } else michael@0: this.close(); michael@0: michael@0: this._makeLastActiveGroupItemActive(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _makeClosestTabActive michael@0: // Make the closest tab external to this group active. michael@0: // Used when closing the group. michael@0: _makeClosestTabActive: function GroupItem__makeClosestTabActive() { michael@0: let closeCenter = this.getBounds().center(); michael@0: // Find closest tab to make active michael@0: let closestTabItem = UI.getClosestTab(closeCenter); michael@0: if (closestTabItem) michael@0: UI.setActive(closestTabItem); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _makeLastActiveGroupItemActive michael@0: // Makes the last active group item active. michael@0: _makeLastActiveGroupItemActive: function GroupItem__makeLastActiveGroupItemActive() { michael@0: let groupItem = GroupItems.getLastActiveGroupItem(); michael@0: if (groupItem) michael@0: UI.setActive(groupItem); michael@0: else michael@0: this._makeClosestTabActive(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: closeIfEmpty michael@0: // Closes the group if it's empty, is closable, and autoclose is enabled michael@0: // (see pauseAutoclose()). Returns true if the close occurred and false michael@0: // otherwise. michael@0: closeIfEmpty: function GroupItem_closeIfEmpty() { michael@0: if (this.isEmpty() && !UI._closedLastVisibleTab && michael@0: !GroupItems.getUnclosableGroupItemId() && !GroupItems._autoclosePaused) { michael@0: this.close(); michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _unhide michael@0: // Shows the hidden group. michael@0: // michael@0: // Parameters: michael@0: // options - various options (see below) michael@0: // michael@0: // Possible options: michael@0: // immediately - true when no animations should be used michael@0: _unhide: function GroupItem__unhide(options) { michael@0: this._cancelFadeAwayUndoButtonTimer(); michael@0: this.hidden = false; michael@0: this.$undoContainer.remove(); michael@0: this.$undoContainer = null; michael@0: this.droppable(true); michael@0: this.setTrenches(this.bounds); michael@0: michael@0: let self = this; michael@0: michael@0: let finalize = function () { michael@0: self._children.forEach(function(child) { michael@0: iQ(child.container).show(); michael@0: }); michael@0: michael@0: UI.setActive(self); michael@0: self._sendToSubscribers("groupShown"); michael@0: }; michael@0: michael@0: let $container = iQ(this.container).show(); michael@0: michael@0: if (!options || !options.immediately) { michael@0: $container.animate({ michael@0: "transform": "scale(1)", michael@0: "opacity": 1 michael@0: }, { michael@0: duration: 170, michael@0: complete: finalize michael@0: }); michael@0: } else { michael@0: $container.css({"transform": "none", opacity: 1}); michael@0: finalize(); michael@0: } michael@0: michael@0: GroupItems.updateGroupCloseButtons(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: closeHidden michael@0: // Removes the group item, its children and its container. michael@0: closeHidden: function GroupItem_closeHidden() { michael@0: let self = this; michael@0: michael@0: this._cancelFadeAwayUndoButtonTimer(); michael@0: michael@0: // When the last non-empty groupItem is closed and there are no michael@0: // pinned tabs then create a new group with a blank tab. michael@0: let remainingGroups = GroupItems.groupItems.filter(function (groupItem) { michael@0: return (groupItem != self && groupItem.getChildren().length); michael@0: }); michael@0: michael@0: let tab = null; michael@0: michael@0: if (!gBrowser._numPinnedTabs && !remainingGroups.length) { michael@0: let emptyGroups = GroupItems.groupItems.filter(function (groupItem) { michael@0: return (groupItem != self && !groupItem.getChildren().length); michael@0: }); michael@0: let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup()); michael@0: tab = group.newTab(null, {dontZoomIn: true}); michael@0: } michael@0: michael@0: let closed = this.destroy(); michael@0: michael@0: if (!tab) michael@0: return; michael@0: michael@0: if (closed) { michael@0: // Let's make the new tab the selected tab. michael@0: UI.goToTab(tab); michael@0: } else { michael@0: // Remove the new tab and group, if this group is no longer closed. michael@0: tab._tabViewTabItem.parent.destroy({immediately: true}); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: destroy michael@0: // Close all tabs linked to children (tabItems), removes all children and michael@0: // close the groupItem. michael@0: // michael@0: // Parameters: michael@0: // options - An object with optional settings for this call. michael@0: // michael@0: // Options: michael@0: // immediately - (bool) if true, no animation will be used michael@0: // michael@0: // Returns true if the groupItem has been closed, or false otherwise. A group michael@0: // could not have been closed due to a tab with an onUnload handler (that michael@0: // waits for user interaction). michael@0: destroy: function GroupItem_destroy(options) { michael@0: let self = this; michael@0: michael@0: // when "TabClose" event is fired, the browser tab is about to close and our michael@0: // item "close" event is fired. And then, the browser tab gets closed. michael@0: // In other words, the group "close" event is fired before all browser michael@0: // tabs in the group are closed. The below code would fire the group "close" michael@0: // event only after all browser tabs in that group are closed. michael@0: this._children.concat().forEach(function(child) { michael@0: child.removeSubscriber("close", self._onChildClose); michael@0: michael@0: if (child.close(true)) { michael@0: self.remove(child, { dontArrange: true }); michael@0: } else { michael@0: // child.removeSubscriber() must be called before child.close(), michael@0: // therefore we call child.addSubscriber() if the tab is not removed. michael@0: child.addSubscriber("close", self._onChildClose); michael@0: } michael@0: }); michael@0: michael@0: if (this._children.length) { michael@0: if (this.hidden) michael@0: this.$undoContainer.fadeOut(function() { self._unhide() }); michael@0: michael@0: return false; michael@0: } else { michael@0: this.close(options); michael@0: return true; michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _fadeAwayUndoButton michael@0: // Fades away the undo button michael@0: _fadeAwayUndoButton: function GroupItem__fadeAwayUndoButton() { michael@0: let self = this; michael@0: michael@0: if (this.$undoContainer) { michael@0: // if there is more than one group and other groups are not empty, michael@0: // fade away the undo button. michael@0: let shouldFadeAway = false; michael@0: michael@0: if (GroupItems.groupItems.length > 1) { michael@0: shouldFadeAway = michael@0: GroupItems.groupItems.some(function(groupItem) { michael@0: return (groupItem != self && groupItem.getChildren().length > 0); michael@0: }); michael@0: } michael@0: michael@0: if (shouldFadeAway) { michael@0: self.$undoContainer.animate({ michael@0: color: "transparent", michael@0: opacity: 0 michael@0: }, { michael@0: duration: this._fadeAwayUndoButtonDuration, michael@0: complete: function() { self.closeHidden(); } michael@0: }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _createUndoButton michael@0: // Makes the affordance for undo a close group action michael@0: _createUndoButton: function GroupItem__createUndoButton() { michael@0: let self = this; michael@0: this.$undoContainer = iQ("
") michael@0: .addClass("undo") michael@0: .attr("type", "button") michael@0: .attr("data-group-id", this.id) michael@0: .appendTo("body"); michael@0: iQ("") michael@0: .text(tabviewString("groupItem.undoCloseGroup")) michael@0: .appendTo(this.$undoContainer); michael@0: let undoClose = iQ("") michael@0: .addClass("close") michael@0: .attr("title", tabviewString("groupItem.discardClosedGroup")) michael@0: .appendTo(this.$undoContainer); michael@0: michael@0: this.$undoContainer.css({ michael@0: left: this.bounds.left + this.bounds.width/2 - iQ(self.$undoContainer).width()/2, michael@0: top: this.bounds.top + this.bounds.height/2 - iQ(self.$undoContainer).height()/2, michael@0: "transform": "scale(.1)", michael@0: opacity: 0 michael@0: }); michael@0: this.hidden = true; michael@0: michael@0: // hide group item and show undo container. michael@0: setTimeout(function() { michael@0: self.$undoContainer.animate({ michael@0: "transform": "scale(1)", michael@0: "opacity": 1 michael@0: }, { michael@0: easing: "tabviewBounce", michael@0: duration: 170, michael@0: complete: function() { michael@0: self._sendToSubscribers("groupHidden"); michael@0: } michael@0: }); michael@0: }, 50); michael@0: michael@0: // add click handlers michael@0: this.$undoContainer.click(function(e) { michael@0: // don't do anything if the close button is clicked. michael@0: if (e.target == undoClose[0]) michael@0: return; michael@0: michael@0: self.$undoContainer.fadeOut(function() { self._unhide(); }); michael@0: }); michael@0: michael@0: undoClose.click(function() { michael@0: self.$undoContainer.fadeOut(function() { self.closeHidden(); }); michael@0: }); michael@0: michael@0: this.setupFadeAwayUndoButtonTimer(); michael@0: // Cancel the fadeaway if you move the mouse over the undo michael@0: // button, and restart the countdown once you move out of it. michael@0: this.$undoContainer.mouseover(function() { michael@0: self._cancelFadeAwayUndoButtonTimer(); michael@0: }); michael@0: this.$undoContainer.mouseout(function() { michael@0: self.setupFadeAwayUndoButtonTimer(); michael@0: }); michael@0: michael@0: GroupItems.updateGroupCloseButtons(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Sets up fade away undo button timeout. michael@0: setupFadeAwayUndoButtonTimer: function GroupItem_setupFadeAwayUndoButtonTimer() { michael@0: let self = this; michael@0: michael@0: if (!this._undoButtonTimeoutId) { michael@0: this._undoButtonTimeoutId = setTimeout(function() { michael@0: self._fadeAwayUndoButton(); michael@0: }, this.fadeAwayUndoButtonDelay); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Cancels the fade away undo button timeout. michael@0: _cancelFadeAwayUndoButtonTimer: function GroupItem__cancelFadeAwayUndoButtonTimer() { michael@0: clearTimeout(this._undoButtonTimeoutId); michael@0: this._undoButtonTimeoutId = null; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: add michael@0: // Adds an item to the groupItem. michael@0: // Parameters: michael@0: // michael@0: // a - The item to add. Can be an , a DOM element or an iQ object. michael@0: // The latter two must refer to the container of an . michael@0: // options - An object with optional settings for this call. michael@0: // michael@0: // Options: michael@0: // michael@0: // index - (int) if set, add this tab at this index michael@0: // immediately - (bool) if true, no animation will be used michael@0: // dontArrange - (bool) if true, will not trigger an arrange on the group michael@0: add: function GroupItem_add(a, options) { michael@0: try { michael@0: var item; michael@0: var $el; michael@0: if (a.isAnItem) { michael@0: item = a; michael@0: $el = iQ(a.container); michael@0: } else { michael@0: $el = iQ(a); michael@0: item = Items.item($el); michael@0: } michael@0: michael@0: // safeguard to remove the item from its previous group michael@0: if (item.parent && item.parent !== this) michael@0: item.parent.remove(item); michael@0: michael@0: item.removeTrenches(); michael@0: michael@0: if (!options) michael@0: options = {}; michael@0: michael@0: var self = this; michael@0: michael@0: var wasAlreadyInThisGroupItem = false; michael@0: var oldIndex = this._children.indexOf(item); michael@0: if (oldIndex != -1) { michael@0: this._children.splice(oldIndex, 1); michael@0: wasAlreadyInThisGroupItem = true; michael@0: } michael@0: michael@0: // Insert the tab into the right position. michael@0: var index = ("index" in options) ? options.index : this._children.length; michael@0: this._children.splice(index, 0, item); michael@0: michael@0: item.setZ(this.getZ() + 1); michael@0: michael@0: if (!wasAlreadyInThisGroupItem) { michael@0: item.droppable(false); michael@0: item.groupItemData = {}; michael@0: michael@0: item.addSubscriber("close", this._onChildClose); michael@0: item.setParent(this); michael@0: $el.attr("data-group-id", this.id); michael@0: michael@0: if (typeof item.setResizable == 'function') michael@0: item.setResizable(false, options.immediately); michael@0: michael@0: if (item == UI.getActiveTab() || !this._activeTab) michael@0: this.setActiveTab(item); michael@0: michael@0: // if it matches the selected tab or no active tab and the browser michael@0: // tab is hidden, the active group item would be set. michael@0: if (item.tab.selected || michael@0: (!GroupItems.getActiveGroupItem() && !item.tab.hidden)) michael@0: UI.setActive(this); michael@0: } michael@0: michael@0: if (!options.dontArrange) michael@0: this.arrange({animate: !options.immediately}); michael@0: michael@0: this._unfreezeItemSize({dontArrange: true}); michael@0: this._sendToSubscribers("childAdded", { item: item }); michael@0: michael@0: UI.setReorderTabsOnHide(this); michael@0: } catch(e) { michael@0: Utils.log('GroupItem.add error', e); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _onChildClose michael@0: // Handles "close" events from the group's children. michael@0: // michael@0: // Parameters: michael@0: // tabItem - The tabItem that is closed. michael@0: _onChildClose: function GroupItem__onChildClose(tabItem) { michael@0: let count = this._children.length; michael@0: let dontArrange = tabItem.closedManually && michael@0: (this.expanded || !this.shouldStack(count)); michael@0: let dontClose = !tabItem.closedManually && gBrowser._numPinnedTabs > 0; michael@0: this.remove(tabItem, {dontArrange: dontArrange, dontClose: dontClose}); michael@0: michael@0: if (dontArrange) michael@0: this._freezeItemSize(count); michael@0: michael@0: if (this._children.length > 0 && this._activeTab && tabItem.closedManually) michael@0: UI.setActive(this); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: remove michael@0: // Removes an item from the groupItem. michael@0: // Parameters: michael@0: // michael@0: // a - The item to remove. Can be an , a DOM element or an iQ object. michael@0: // The latter two must refer to the container of an . michael@0: // options - An optional object with settings for this call. See below. michael@0: // michael@0: // Possible options: michael@0: // dontArrange - don't rearrange the remaining items michael@0: // dontClose - don't close the group even if it normally would michael@0: // immediately - don't animate michael@0: remove: function GroupItem_remove(a, options) { michael@0: try { michael@0: let $el; michael@0: let item; michael@0: michael@0: if (a.isAnItem) { michael@0: item = a; michael@0: $el = iQ(item.container); michael@0: } else { michael@0: $el = iQ(a); michael@0: item = Items.item($el); michael@0: } michael@0: michael@0: if (!options) michael@0: options = {}; michael@0: michael@0: let index = this._children.indexOf(item); michael@0: if (index != -1) michael@0: this._children.splice(index, 1); michael@0: michael@0: if (item == this._activeTab || !this._activeTab) { michael@0: if (this._children.length > 0) michael@0: this._activeTab = this._children[0]; michael@0: else michael@0: this._activeTab = null; michael@0: } michael@0: michael@0: $el[0].removeAttribute("data-group-id"); michael@0: item.setParent(null); michael@0: item.removeClass("stacked"); michael@0: item.isStacked = false; michael@0: item.setHidden(false); michael@0: item.removeClass("stack-trayed"); michael@0: item.setRotation(0); michael@0: michael@0: // Force tabItem resize if it's dragged out of a stacked groupItem. michael@0: // The tabItems's title will be visible and that's why we need to michael@0: // recalculate its height. michael@0: if (item.isDragging && this.isStacked()) michael@0: item.setBounds(item.getBounds(), true, {force: true}); michael@0: michael@0: item.droppable(true); michael@0: item.removeSubscriber("close", this._onChildClose); michael@0: michael@0: if (typeof item.setResizable == 'function') michael@0: item.setResizable(true, options.immediately); michael@0: michael@0: // if a blank tab is selected while restoring a tab the blank tab gets michael@0: // removed. we need to keep the group alive for the restored tab. michael@0: if (item.isRemovedAfterRestore) michael@0: options.dontClose = true; michael@0: michael@0: let closed = options.dontClose ? false : this.closeIfEmpty(); michael@0: if (closed || michael@0: (this._children.length == 0 && !gBrowser._numPinnedTabs && michael@0: !item.isDragging)) { michael@0: this._makeLastActiveGroupItemActive(); michael@0: } else if (!options.dontArrange) { michael@0: this.arrange({animate: !options.immediately}); michael@0: this._unfreezeItemSize({dontArrange: true}); michael@0: } michael@0: michael@0: this._sendToSubscribers("childRemoved", { item: item }); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: removeAll michael@0: // Removes all of the groupItem's children. michael@0: // The optional "options" param is passed to each remove call. michael@0: removeAll: function GroupItem_removeAll(options) { michael@0: let self = this; michael@0: let newOptions = {dontArrange: true}; michael@0: if (options) michael@0: Utils.extend(newOptions, options); michael@0: michael@0: let toRemove = this._children.concat(); michael@0: toRemove.forEach(function(child) { michael@0: self.remove(child, newOptions); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Adds the given xul:tab as an app tab in this group's apptab tray michael@0: // michael@0: // Parameters: michael@0: // xulTab - the xul:tab. michael@0: // options - change how the app tab is added. michael@0: // michael@0: // Options: michael@0: // position - the position of the app tab should be added to. michael@0: // dontAdjustTray - (boolean) if true, do not adjust the tray. michael@0: addAppTab: function GroupItem_addAppTab(xulTab, options) { michael@0: GroupItems.getAppTabFavIconUrl(xulTab, function(iconUrl) { michael@0: // The tab might have been removed or unpinned while waiting. michael@0: if (!Utils.isValidXULTab(xulTab) || !xulTab.pinned) michael@0: return; michael@0: michael@0: let self = this; michael@0: let $appTab = iQ("") michael@0: .addClass("appTabIcon") michael@0: .attr("src", iconUrl) michael@0: .data("xulTab", xulTab) michael@0: .mousedown(function GroupItem_addAppTab_onAppTabMousedown(event) { michael@0: // stop mousedown propagation to disable group dragging on app tabs michael@0: event.stopPropagation(); michael@0: }) michael@0: .click(function GroupItem_addAppTab_onAppTabClick(event) { michael@0: if (!Utils.isLeftClick(event)) michael@0: return; michael@0: michael@0: UI.setActive(self, { dontSetActiveTabInGroup: true }); michael@0: UI.goToTab(iQ(this).data("xulTab")); michael@0: }); michael@0: michael@0: if (options && "position" in options) { michael@0: let children = this.$appTabTray[0].childNodes; michael@0: michael@0: if (options.position >= children.length) michael@0: $appTab.appendTo(this.$appTabTray); michael@0: else michael@0: this.$appTabTray[0].insertBefore($appTab[0], children[options.position]); michael@0: } else { michael@0: $appTab.appendTo(this.$appTabTray); michael@0: } michael@0: if (!options || !options.dontAdjustTray) michael@0: this.adjustAppTabTray(true); michael@0: michael@0: this._sendToSubscribers("appTabIconAdded", { item: $appTab }); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Removes the given xul:tab as an app tab in this group's apptab tray michael@0: removeAppTab: function GroupItem_removeAppTab(xulTab) { michael@0: // remove the icon michael@0: iQ(".appTabIcon", this.$appTabTray).each(function(icon) { michael@0: let $icon = iQ(icon); michael@0: if ($icon.data("xulTab") != xulTab) michael@0: return true; michael@0: michael@0: $icon.remove(); michael@0: return false; michael@0: }); michael@0: michael@0: // adjust the tray michael@0: this.adjustAppTabTray(true); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Arranges the given xul:tab as an app tab in the group's apptab tray michael@0: arrangeAppTab: function GroupItem_arrangeAppTab(xulTab) { michael@0: let self = this; michael@0: michael@0: let elements = iQ(".appTabIcon", this.$appTabTray); michael@0: let length = elements.length; michael@0: michael@0: elements.each(function(icon) { michael@0: let $icon = iQ(icon); michael@0: if ($icon.data("xulTab") != xulTab) michael@0: return true; michael@0: michael@0: let targetIndex = xulTab._tPos; michael@0: michael@0: $icon.remove({ preserveEventHandlers: true }); michael@0: if (targetIndex < (length - 1)) michael@0: self.$appTabTray[0].insertBefore( michael@0: icon, michael@0: iQ(".appTabIcon:nth-child(" + (targetIndex + 1) + ")", self.$appTabTray)[0]); michael@0: else michael@0: $icon.appendTo(self.$appTabTray); michael@0: return false; michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: hideExpandControl michael@0: // Hide the control which expands a stacked groupItem into a quick-look view. michael@0: hideExpandControl: function GroupItem_hideExpandControl() { michael@0: this.$expander.hide(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: showExpandControl michael@0: // Show the control which expands a stacked groupItem into a quick-look view. michael@0: showExpandControl: function GroupItem_showExpandControl() { michael@0: let parentBB = this.getBounds(); michael@0: let childBB = this.getChild(0).getBounds(); michael@0: this.$expander michael@0: .show() michael@0: .css({ michael@0: left: parentBB.width/2 - this.$expander.width()/2 michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: shouldStack michael@0: // Returns true if the groupItem, given "count", should stack (instead of michael@0: // grid). michael@0: shouldStack: function GroupItem_shouldStack(count) { michael@0: let bb = this.getContentBounds(); michael@0: let options = { michael@0: return: 'widthAndColumns', michael@0: count: count || this._children.length, michael@0: hideTitle: false michael@0: }; michael@0: let arrObj = Items.arrange(this._children, bb, options); michael@0: michael@0: let shouldStack = arrObj.childWidth < TabItems.minTabWidth * 1.35; michael@0: this._columns = shouldStack ? null : arrObj.columns; michael@0: michael@0: return shouldStack; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _freezeItemSize michael@0: // Freezes current item size (when removing a child). michael@0: // michael@0: // Parameters: michael@0: // itemCount - the number of children before the last one was removed michael@0: _freezeItemSize: function GroupItem__freezeItemSize(itemCount) { michael@0: let data = this._frozenItemSizeData; michael@0: michael@0: if (!data.lastItemCount) { michael@0: let self = this; michael@0: data.lastItemCount = itemCount; michael@0: michael@0: // unfreeze item size when tabview is hidden michael@0: data.onTabViewHidden = function () self._unfreezeItemSize(); michael@0: window.addEventListener('tabviewhidden', data.onTabViewHidden, false); michael@0: michael@0: // we don't need to observe mouse movement when expanded because the michael@0: // tray is closed when we leave it and collapse causes unfreezing michael@0: if (!self.expanded) { michael@0: // unfreeze item size when cursor is moved out of group bounds michael@0: data.onMouseMove = function (e) { michael@0: let cursor = new Point(e.pageX, e.pageY); michael@0: if (!self.bounds.contains(cursor)) michael@0: self._unfreezeItemSize(); michael@0: } michael@0: iQ(window).mousemove(data.onMouseMove); michael@0: } michael@0: } michael@0: michael@0: this.arrange({animate: true, count: data.lastItemCount}); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _unfreezeItemSize michael@0: // Unfreezes and updates item size. michael@0: // michael@0: // Parameters: michael@0: // options - various options (see below) michael@0: // michael@0: // Possible options: michael@0: // dontArrange - do not arrange items when unfreezing michael@0: _unfreezeItemSize: function GroupItem__unfreezeItemSize(options) { michael@0: let data = this._frozenItemSizeData; michael@0: if (!data.lastItemCount) michael@0: return; michael@0: michael@0: if (!options || !options.dontArrange) michael@0: this.arrange({animate: true}); michael@0: michael@0: // unbind event listeners michael@0: window.removeEventListener('tabviewhidden', data.onTabViewHidden, false); michael@0: if (data.onMouseMove) michael@0: iQ(window).unbind('mousemove', data.onMouseMove); michael@0: michael@0: // reset freeze status michael@0: this._frozenItemSizeData = {}; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: arrange michael@0: // Lays out all of the children. michael@0: // michael@0: // Parameters: michael@0: // options - passed to or <_stackArrange>, except those below michael@0: // michael@0: // Options: michael@0: // addTab - (boolean) if true, we add one to the child count michael@0: // oldDropIndex - if set, we will only set any bounds if the dropIndex has michael@0: // changed michael@0: // dropPos - () a position where a tab is currently positioned, above michael@0: // this group. michael@0: // animate - (boolean) if true, movement of children will be animated. michael@0: // michael@0: // Returns: michael@0: // dropIndex - an index value for where an item would be dropped, if michael@0: // options.dropPos is given. michael@0: arrange: function GroupItem_arrange(options) { michael@0: if (!options) michael@0: options = {}; michael@0: michael@0: let childrenToArrange = []; michael@0: this._children.forEach(function(child) { michael@0: if (child.isDragging) michael@0: options.addTab = true; michael@0: else michael@0: childrenToArrange.push(child); michael@0: }); michael@0: michael@0: if (GroupItems._arrangePaused) { michael@0: GroupItems.pushArrange(this, options); michael@0: return false; michael@0: } michael@0: michael@0: let shouldStack = this.shouldStack(childrenToArrange.length + (options.addTab ? 1 : 0)); michael@0: let shouldStackArrange = (shouldStack && !this.expanded); michael@0: let box; michael@0: michael@0: // if we should stack and we're not expanded michael@0: if (shouldStackArrange) { michael@0: this.showExpandControl(); michael@0: box = this.getContentBounds({stacked: true}); michael@0: this._stackArrange(childrenToArrange, box, options); michael@0: return false; michael@0: } else { michael@0: this.hideExpandControl(); michael@0: box = this.getContentBounds(); michael@0: // a dropIndex is returned michael@0: return this._gridArrange(childrenToArrange, box, options); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _stackArrange michael@0: // Arranges the children in a stack. michael@0: // michael@0: // Parameters: michael@0: // childrenToArrange - array of children michael@0: // bb - to arrange within michael@0: // options - see below michael@0: // michael@0: // Possible "options" properties: michael@0: // animate - whether to animate; default: true. michael@0: _stackArrange: function GroupItem__stackArrange(childrenToArrange, bb, options) { michael@0: if (!options) michael@0: options = {}; michael@0: var animate = "animate" in options ? options.animate : true; michael@0: michael@0: var count = childrenToArrange.length; michael@0: if (!count) michael@0: return; michael@0: michael@0: let itemAspect = TabItems.tabHeight / TabItems.tabWidth; michael@0: let zIndex = this.getZ() + count + 1; michael@0: let maxRotation = 35; // degress michael@0: let scale = 0.7; michael@0: let newTabsPad = 10; michael@0: let bbAspect = bb.height / bb.width; michael@0: let numInPile = 6; michael@0: let angleDelta = 3.5; // degrees michael@0: michael@0: // compute size of the entire stack, modulo rotation. michael@0: let size; michael@0: if (bbAspect > itemAspect) { // Tall, thin groupItem michael@0: size = TabItems.calcValidSize(new Point(bb.width * scale, -1), michael@0: {hideTitle:true}); michael@0: } else { // Short, wide groupItem michael@0: size = TabItems.calcValidSize(new Point(-1, bb.height * scale), michael@0: {hideTitle:true}); michael@0: } michael@0: michael@0: // x is the left margin that the stack will have, within the content area (bb) michael@0: // y is the vertical margin michael@0: var x = (bb.width - size.x) / 2; michael@0: var y = Math.min(size.x, (bb.height - size.y) / 2); michael@0: var box = new Rect(bb.left + x, bb.top + y, size.x, size.y); michael@0: michael@0: var self = this; michael@0: var children = []; michael@0: michael@0: // ensure topChild is the first item in childrenToArrange michael@0: let topChild = this.getTopChild(); michael@0: let topChildPos = childrenToArrange.indexOf(topChild); michael@0: if (topChildPos > 0) { michael@0: childrenToArrange.splice(topChildPos, 1); michael@0: childrenToArrange.unshift(topChild); michael@0: } michael@0: michael@0: childrenToArrange.forEach(function GroupItem__stackArrange_order(child) { michael@0: // Children are still considered stacked even if they're hidden later. michael@0: child.addClass("stacked"); michael@0: child.isStacked = true; michael@0: if (numInPile-- > 0) { michael@0: children.push(child); michael@0: } else { michael@0: child.setHidden(true); michael@0: } michael@0: }); michael@0: michael@0: self._isStacked = true; michael@0: michael@0: let angleAccum = 0; michael@0: children.forEach(function GroupItem__stackArrange_apply(child, index) { michael@0: child.setZ(zIndex); michael@0: zIndex--; michael@0: michael@0: // Force a recalculation of height because we've changed how the title michael@0: // is shown. michael@0: child.setBounds(box, !animate || child.getHidden(), {force:true}); michael@0: child.setRotation((UI.rtl ? -1 : 1) * angleAccum); michael@0: child.setHidden(false); michael@0: angleAccum += angleDelta; michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _gridArrange michael@0: // Arranges the children into a grid. michael@0: // michael@0: // Parameters: michael@0: // childrenToArrange - array of children michael@0: // box - to arrange within michael@0: // options - see below michael@0: // michael@0: // Possible "options" properties: michael@0: // animate - whether to animate; default: true. michael@0: // z - (int) a z-index to assign the children michael@0: // columns - the number of columns to use in the layout, if known in advance michael@0: // michael@0: // Returns: michael@0: // dropIndex - (int) the index at which a dragged item (if there is one) should be added michael@0: // if it is dropped. Otherwise (boolean) false. michael@0: _gridArrange: function GroupItem__gridArrange(childrenToArrange, box, options) { michael@0: let arrangeOptions; michael@0: if (this.expanded) { michael@0: // if we're expanded, we actually want to use the expanded tray's bounds. michael@0: box = new Rect(this.expanded.bounds); michael@0: box.inset(8, 8); michael@0: arrangeOptions = Utils.extend({}, options, {z: 99999}); michael@0: } else { michael@0: this._isStacked = false; michael@0: arrangeOptions = Utils.extend({}, options, { michael@0: columns: this._columns michael@0: }); michael@0: michael@0: childrenToArrange.forEach(function(child) { michael@0: child.removeClass("stacked"); michael@0: child.isStacked = false; michael@0: child.setHidden(false); michael@0: }); michael@0: } michael@0: michael@0: if (!childrenToArrange.length) michael@0: return false; michael@0: michael@0: // Items.arrange will determine where/how the child items should be michael@0: // placed, but will *not* actually move them for us. This is our job. michael@0: let result = Items.arrange(childrenToArrange, box, arrangeOptions); michael@0: let {dropIndex, rects, columns} = result; michael@0: if ("oldDropIndex" in options && options.oldDropIndex === dropIndex) michael@0: return dropIndex; michael@0: michael@0: this._columns = columns; michael@0: let index = 0; michael@0: let self = this; michael@0: childrenToArrange.forEach(function GroupItem_arrange_children_each(child, i) { michael@0: // If dropIndex spacing is active and this is a child after index, michael@0: // bump it up one so we actually use the correct rect michael@0: // (and skip one for the dropPos) michael@0: if (self._dropSpaceActive && index === dropIndex) michael@0: index++; michael@0: child.setBounds(rects[index], !options.animate); michael@0: child.setRotation(0); michael@0: if (arrangeOptions.z) michael@0: child.setZ(arrangeOptions.z); michael@0: index++; michael@0: }); michael@0: michael@0: return dropIndex; michael@0: }, michael@0: michael@0: expand: function GroupItem_expand() { michael@0: var self = this; michael@0: // ___ we're stacked, and command is held down so expand michael@0: UI.setActive(this.getTopChild()); michael@0: michael@0: var startBounds = this.getChild(0).getBounds(); michael@0: var $tray = iQ("
").css({ michael@0: top: startBounds.top, michael@0: left: startBounds.left, michael@0: width: startBounds.width, michael@0: height: startBounds.height, michael@0: position: "absolute", michael@0: zIndex: 99998 michael@0: }).appendTo("body"); michael@0: $tray[0].id = "expandedTray"; michael@0: michael@0: var w = 180; michael@0: var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1; michael@0: var padding = 20; michael@0: var col = Math.ceil(Math.sqrt(this._children.length)); michael@0: var row = Math.ceil(this._children.length/col); michael@0: michael@0: var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1)); michael@0: var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1)); michael@0: michael@0: var pos = {left: startBounds.left, top: startBounds.top}; michael@0: pos.left -= overlayWidth / 3; michael@0: pos.top -= overlayHeight / 3; michael@0: michael@0: if (pos.top < 0) michael@0: pos.top = 20; michael@0: if (pos.left < 0) michael@0: pos.left = 20; michael@0: if (pos.top + overlayHeight > window.innerHeight) michael@0: pos.top = window.innerHeight - overlayHeight - 20; michael@0: if (pos.left + overlayWidth > window.innerWidth) michael@0: pos.left = window.innerWidth - overlayWidth - 20; michael@0: michael@0: $tray michael@0: .animate({ michael@0: width: overlayWidth, michael@0: height: overlayHeight, michael@0: top: pos.top, michael@0: left: pos.left michael@0: }, { michael@0: duration: 200, michael@0: easing: "tabviewBounce", michael@0: complete: function GroupItem_expand_animate_complete() { michael@0: self._sendToSubscribers("expanded"); michael@0: } michael@0: }) michael@0: .addClass("overlay"); michael@0: michael@0: this._children.forEach(function(child) { michael@0: child.addClass("stack-trayed"); michael@0: child.setHidden(false); michael@0: }); michael@0: michael@0: var $shield = iQ('
') michael@0: .addClass('shield') michael@0: .css({ michael@0: zIndex: 99997 michael@0: }) michael@0: .appendTo('body') michael@0: .click(function() { // just in case michael@0: self.collapse(); michael@0: }); michael@0: michael@0: // There is a race-condition here. If there is michael@0: // a mouse-move while the shield is coming up michael@0: // it will collapse, which we don't want. Thus, michael@0: // we wait a little bit before adding this event michael@0: // handler. michael@0: setTimeout(function() { michael@0: $shield.mouseover(function() { michael@0: self.collapse(); michael@0: }); michael@0: }, 200); michael@0: michael@0: this.expanded = { michael@0: $tray: $tray, michael@0: $shield: $shield, michael@0: bounds: new Rect(pos.left, pos.top, overlayWidth, overlayHeight) michael@0: }; michael@0: michael@0: this.arrange(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: collapse michael@0: // Collapses the groupItem from the expanded "tray" mode. michael@0: collapse: function GroupItem_collapse() { michael@0: if (this.expanded) { michael@0: var z = this.getZ(); michael@0: var box = this.getBounds(); michael@0: let self = this; michael@0: this.expanded.$tray michael@0: .css({ michael@0: zIndex: z + 1 michael@0: }) michael@0: .animate({ michael@0: width: box.width, michael@0: height: box.height, michael@0: top: box.top, michael@0: left: box.left, michael@0: opacity: 0 michael@0: }, { michael@0: duration: 350, michael@0: easing: "tabviewBounce", michael@0: complete: function GroupItem_collapse_animate_complete() { michael@0: iQ(this).remove(); michael@0: self._sendToSubscribers("collapsed"); michael@0: } michael@0: }); michael@0: michael@0: this.expanded.$shield.remove(); michael@0: this.expanded = null; michael@0: michael@0: this._children.forEach(function(child) { michael@0: child.removeClass("stack-trayed"); michael@0: }); michael@0: michael@0: this.arrange({z: z + 2}); michael@0: this._unfreezeItemSize({dontArrange: true}); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _addHandlers michael@0: // Helper routine for the constructor; adds various event handlers to the container. michael@0: _addHandlers: function GroupItem__addHandlers(container) { michael@0: let self = this; michael@0: let lastMouseDownTarget; michael@0: michael@0: container.mousedown(function(e) { michael@0: let target = e.target; michael@0: // only set the last mouse down target if it is a left click, not on the michael@0: // close button, not on the expand button, not on the title bar and its michael@0: // elements michael@0: if (Utils.isLeftClick(e) && michael@0: self.$closeButton[0] != target && michael@0: self.$titlebar[0] != target && michael@0: self.$expander[0] != target && michael@0: !self.$titlebar.contains(target) && michael@0: !self.$appTabTray.contains(target)) { michael@0: lastMouseDownTarget = target; michael@0: } else { michael@0: lastMouseDownTarget = null; michael@0: } michael@0: }); michael@0: container.mouseup(function(e) { michael@0: let same = (e.target == lastMouseDownTarget); michael@0: lastMouseDownTarget = null; michael@0: michael@0: if (same && !self.isDragging) { michael@0: if (gBrowser.selectedTab.pinned && michael@0: UI.getActiveTab() != self.getActiveTab() && michael@0: self.getChildren().length > 0) { michael@0: UI.setActive(self, { dontSetActiveTabInGroup: true }); michael@0: UI.goToTab(gBrowser.selectedTab); michael@0: } else { michael@0: let tabItem = self.getTopChild(); michael@0: if (tabItem) michael@0: tabItem.zoomIn(); michael@0: else michael@0: self.newTab(); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: let dropIndex = false; michael@0: let dropSpaceTimer = null; michael@0: michael@0: // When the _dropSpaceActive flag is turned on on a group, and a tab is michael@0: // dragged on top, a space will open up. michael@0: this._dropSpaceActive = false; michael@0: michael@0: this.dropOptions.over = function GroupItem_dropOptions_over(event) { michael@0: iQ(this.container).addClass("acceptsDrop"); michael@0: }; michael@0: this.dropOptions.move = function GroupItem_dropOptions_move(event) { michael@0: let oldDropIndex = dropIndex; michael@0: let dropPos = drag.info.item.getBounds().center(); michael@0: let options = {dropPos: dropPos, michael@0: addTab: self._dropSpaceActive && drag.info.item.parent != self, michael@0: oldDropIndex: oldDropIndex}; michael@0: let newDropIndex = self.arrange(options); michael@0: // If this is a new drop index, start a timer! michael@0: if (newDropIndex !== oldDropIndex) { michael@0: dropIndex = newDropIndex; michael@0: if (this._dropSpaceActive) michael@0: return; michael@0: michael@0: if (dropSpaceTimer) { michael@0: clearTimeout(dropSpaceTimer); michael@0: dropSpaceTimer = null; michael@0: } michael@0: michael@0: dropSpaceTimer = setTimeout(function GroupItem_arrange_evaluateDropSpace() { michael@0: // Note that dropIndex's scope is GroupItem__addHandlers, but michael@0: // newDropIndex's scope is GroupItem_dropOptions_move. Thus, michael@0: // dropIndex may change with other movement events before we come michael@0: // back and check this. If it's still the same dropIndex, activate michael@0: // drop space display! michael@0: if (dropIndex === newDropIndex) { michael@0: self._dropSpaceActive = true; michael@0: dropIndex = self.arrange({dropPos: dropPos, michael@0: addTab: drag.info.item.parent != self, michael@0: animate: true}); michael@0: } michael@0: dropSpaceTimer = null; michael@0: }, 250); michael@0: } michael@0: }; michael@0: this.dropOptions.drop = function GroupItem_dropOptions_drop(event) { michael@0: iQ(this.container).removeClass("acceptsDrop"); michael@0: let options = {}; michael@0: if (this._dropSpaceActive) michael@0: this._dropSpaceActive = false; michael@0: michael@0: if (dropSpaceTimer) { michael@0: clearTimeout(dropSpaceTimer); michael@0: dropSpaceTimer = null; michael@0: // If we drop this item before the timed rearrange was executed, michael@0: // we won't have an accurate dropIndex value. Get that now. michael@0: let dropPos = drag.info.item.getBounds().center(); michael@0: dropIndex = self.arrange({dropPos: dropPos, michael@0: addTab: drag.info.item.parent != self, michael@0: animate: true}); michael@0: } michael@0: michael@0: if (dropIndex !== false) michael@0: options = {index: dropIndex}; michael@0: this.add(drag.info.$el, options); michael@0: UI.setActive(this); michael@0: dropIndex = false; michael@0: }; michael@0: this.dropOptions.out = function GroupItem_dropOptions_out(event) { michael@0: dropIndex = false; michael@0: if (this._dropSpaceActive) michael@0: this._dropSpaceActive = false; michael@0: michael@0: if (dropSpaceTimer) { michael@0: clearTimeout(dropSpaceTimer); michael@0: dropSpaceTimer = null; michael@0: } michael@0: self.arrange(); michael@0: var groupItem = drag.info.item.parent; michael@0: if (groupItem) michael@0: groupItem.remove(drag.info.$el, {dontClose: true}); michael@0: iQ(this.container).removeClass("acceptsDrop"); michael@0: } michael@0: michael@0: this.draggable(); michael@0: this.droppable(true); michael@0: michael@0: this.$expander.click(function() { michael@0: self.expand(); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setResizable michael@0: // Sets whether the groupItem is resizable and updates the UI accordingly. michael@0: setResizable: function GroupItem_setResizable(value, immediately) { michael@0: var self = this; michael@0: michael@0: this.resizeOptions.minWidth = GroupItems.minGroupWidth; michael@0: this.resizeOptions.minHeight = GroupItems.minGroupHeight; michael@0: michael@0: let start = this.resizeOptions.start; michael@0: this.resizeOptions.start = function (event) { michael@0: start.call(self, event); michael@0: self._unfreezeItemSize(); michael@0: } michael@0: michael@0: if (value) { michael@0: immediately ? this.$resizer.show() : this.$resizer.fadeIn(); michael@0: this.resizable(true); michael@0: } else { michael@0: immediately ? this.$resizer.hide() : this.$resizer.fadeOut(); michael@0: this.resizable(false); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: newTab michael@0: // Creates a new tab within this groupItem. michael@0: // Parameters: michael@0: // url - the new tab should open this url as well michael@0: // options - the options object michael@0: // dontZoomIn - set to true to not zoom into the newly created tab michael@0: // closedLastTab - boolean indicates the last tab has just been closed michael@0: newTab: function GroupItem_newTab(url, options) { michael@0: if (options && options.closedLastTab) michael@0: UI.closedLastTabInTabView = true; michael@0: michael@0: UI.setActive(this, { dontSetActiveTabInGroup: true }); michael@0: michael@0: let dontZoomIn = !!(options && options.dontZoomIn); michael@0: return gBrowser.loadOneTab(url || gWindow.BROWSER_NEW_TAB_URL, { inBackground: dontZoomIn }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: reorderTabItemsBasedOnTabOrder michael@0: // Reorders the tabs in a groupItem based on the arrangment of the tabs michael@0: // shown in the tab bar. It does it by sorting the children michael@0: // of the groupItem by the positions of their respective tabs in the michael@0: // tab bar. michael@0: reorderTabItemsBasedOnTabOrder: function GroupItem_reorderTabItemsBasedOnTabOrder() { michael@0: this._children.sort(function(a,b) a.tab._tPos - b.tab._tPos); michael@0: michael@0: this.arrange({animate: false}); michael@0: // this.arrange calls this.save for us michael@0: }, michael@0: michael@0: // Function: reorderTabsBasedOnTabItemOrder michael@0: // Reorders the tabs in the tab bar based on the arrangment of the tabs michael@0: // shown in the groupItem. michael@0: reorderTabsBasedOnTabItemOrder: function GroupItem_reorderTabsBasedOnTabItemOrder() { michael@0: let indices; michael@0: let tabs = this._children.map(function (tabItem) tabItem.tab); michael@0: michael@0: tabs.forEach(function (tab, index) { michael@0: if (!indices) michael@0: indices = tabs.map(function (tab) tab._tPos); michael@0: michael@0: let start = index ? indices[index - 1] + 1 : 0; michael@0: let end = index + 1 < indices.length ? indices[index + 1] - 1 : Infinity; michael@0: let targetRange = new Range(start, end); michael@0: michael@0: if (!targetRange.contains(tab._tPos)) { michael@0: gBrowser.moveTabTo(tab, start); michael@0: indices = null; michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getTopChild michael@0: // Gets the that should be displayed on top when in stack mode. michael@0: getTopChild: function GroupItem_getTopChild() { michael@0: if (!this.getChildren().length) { michael@0: return null; michael@0: } michael@0: michael@0: return this.getActiveTab() || this.getChild(0); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getChild michael@0: // Returns the nth child tab or null if index is out of range. michael@0: // michael@0: // Parameters: michael@0: // index - the index of the child tab to return, use negative michael@0: // numbers to index from the end (-1 is the last child) michael@0: getChild: function GroupItem_getChild(index) { michael@0: if (index < 0) michael@0: index = this._children.length + index; michael@0: if (index >= this._children.length || index < 0) michael@0: return null; michael@0: return this._children[index]; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getChildren michael@0: // Returns all children. michael@0: getChildren: function GroupItem_getChildren() { michael@0: return this._children; michael@0: } michael@0: }); michael@0: michael@0: // ########## michael@0: // Class: GroupItems michael@0: // Singleton for managing all s. michael@0: let GroupItems = { michael@0: groupItems: [], michael@0: nextID: 1, michael@0: _inited: false, michael@0: _activeGroupItem: null, michael@0: _cleanupFunctions: [], michael@0: _arrangePaused: false, michael@0: _arrangesPending: [], michael@0: _removingHiddenGroups: false, michael@0: _delayedModUpdates: [], michael@0: _autoclosePaused: false, michael@0: minGroupHeight: 110, michael@0: minGroupWidth: 125, michael@0: _lastActiveList: null, michael@0: michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [GroupItems] for debug use michael@0: toString: function GroupItems_toString() { michael@0: return "[GroupItems count=" + this.groupItems.length + "]"; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: init michael@0: init: function GroupItems_init() { michael@0: let self = this; michael@0: michael@0: // setup attr modified handler, and prepare for its uninit michael@0: function handleAttrModified(event) { michael@0: self._handleAttrModified(event.target); michael@0: } michael@0: michael@0: // make sure any closed tabs are removed from the delay update list michael@0: function handleClose(event) { michael@0: let idx = self._delayedModUpdates.indexOf(event.target); michael@0: if (idx != -1) michael@0: self._delayedModUpdates.splice(idx, 1); michael@0: } michael@0: michael@0: this._lastActiveList = new MRUList(); michael@0: michael@0: AllTabs.register("attrModified", handleAttrModified); michael@0: AllTabs.register("close", handleClose); michael@0: this._cleanupFunctions.push(function() { michael@0: AllTabs.unregister("attrModified", handleAttrModified); michael@0: AllTabs.unregister("close", handleClose); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: uninit michael@0: uninit: function GroupItems_uninit() { michael@0: // call our cleanup functions michael@0: this._cleanupFunctions.forEach(function(func) { michael@0: func(); michael@0: }); michael@0: michael@0: this._cleanupFunctions = []; michael@0: michael@0: // additional clean up michael@0: this.groupItems = null; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: newGroup michael@0: // Creates a new empty group. michael@0: newGroup: function GroupItems_newGroup() { michael@0: let bounds = new Rect(20, 20, 250, 200); michael@0: return new GroupItem([], {bounds: bounds, immediately: true}); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: pauseArrange michael@0: // Bypass arrange() calls and collect for resolution in michael@0: // resumeArrange() michael@0: pauseArrange: function GroupItems_pauseArrange() { michael@0: Utils.assert(this._arrangePaused == false, michael@0: "pauseArrange has been called while already paused"); michael@0: Utils.assert(this._arrangesPending.length == 0, michael@0: "There are bypassed arrange() calls that haven't been resolved"); michael@0: this._arrangePaused = true; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: pushArrange michael@0: // Push an arrange() call and its arguments onto an array michael@0: // to be resolved in resumeArrange() michael@0: pushArrange: function GroupItems_pushArrange(groupItem, options) { michael@0: Utils.assert(this._arrangePaused, michael@0: "Ensure pushArrange() called while arrange()s aren't paused"); michael@0: let i; michael@0: for (i = 0; i < this._arrangesPending.length; i++) michael@0: if (this._arrangesPending[i].groupItem === groupItem) michael@0: break; michael@0: let arrangeInfo = { michael@0: groupItem: groupItem, michael@0: options: options michael@0: }; michael@0: if (i < this._arrangesPending.length) michael@0: this._arrangesPending[i] = arrangeInfo; michael@0: else michael@0: this._arrangesPending.push(arrangeInfo); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: resumeArrange michael@0: // Resolve bypassed and collected arrange() calls michael@0: resumeArrange: function GroupItems_resumeArrange() { michael@0: this._arrangePaused = false; michael@0: for (let i = 0; i < this._arrangesPending.length; i++) { michael@0: let g = this._arrangesPending[i]; michael@0: g.groupItem.arrange(g.options); michael@0: } michael@0: this._arrangesPending = []; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _handleAttrModified michael@0: // watch for icon changes on app tabs michael@0: _handleAttrModified: function GroupItems__handleAttrModified(xulTab) { michael@0: if (!UI.isTabViewVisible()) { michael@0: if (this._delayedModUpdates.indexOf(xulTab) == -1) { michael@0: this._delayedModUpdates.push(xulTab); michael@0: } michael@0: } else michael@0: this._updateAppTabIcons(xulTab); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: flushTabUpdates michael@0: // Update apptab icons based on xulTabs which have been updated michael@0: // while the TabView hasn't been visible michael@0: flushAppTabUpdates: function GroupItems_flushAppTabUpdates() { michael@0: let self = this; michael@0: this._delayedModUpdates.forEach(function(xulTab) { michael@0: self._updateAppTabIcons(xulTab); michael@0: }); michael@0: this._delayedModUpdates = []; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _updateAppTabIcons michael@0: // Update images of any apptab icons that point to passed in xultab michael@0: _updateAppTabIcons: function GroupItems__updateAppTabIcons(xulTab) { michael@0: if (!xulTab.pinned) michael@0: return; michael@0: michael@0: this.getAppTabFavIconUrl(xulTab, function(iconUrl) { michael@0: iQ(".appTabIcon").each(function GroupItems__updateAppTabIcons_forEach(icon) { michael@0: let $icon = iQ(icon); michael@0: if ($icon.data("xulTab") == xulTab && iconUrl != $icon.attr("src")) michael@0: $icon.attr("src", iconUrl); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getAppTabFavIconUrl michael@0: // Gets the fav icon url for app tab. michael@0: getAppTabFavIconUrl: function GroupItems_getAppTabFavIconUrl(xulTab, callback) { michael@0: FavIcons.getFavIconUrlForTab(xulTab, function GroupItems_getAppTabFavIconUrl_getFavIconUrlForTab(iconUrl) { michael@0: callback(iconUrl || FavIcons.defaultFavicon); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: addAppTab michael@0: // Adds the given xul:tab to the app tab tray in all groups michael@0: addAppTab: function GroupItems_addAppTab(xulTab) { michael@0: this.groupItems.forEach(function(groupItem) { michael@0: groupItem.addAppTab(xulTab); michael@0: }); michael@0: this.updateGroupCloseButtons(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: removeAppTab michael@0: // Removes the given xul:tab from the app tab tray in all groups michael@0: removeAppTab: function GroupItems_removeAppTab(xulTab) { michael@0: this.groupItems.forEach(function(groupItem) { michael@0: groupItem.removeAppTab(xulTab); michael@0: }); michael@0: this.updateGroupCloseButtons(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: arrangeAppTab michael@0: // Arranges the given xul:tab as an app tab from app tab tray in all groups michael@0: arrangeAppTab: function GroupItems_arrangeAppTab(xulTab) { michael@0: this.groupItems.forEach(function(groupItem) { michael@0: groupItem.arrangeAppTab(xulTab); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getNextID michael@0: // Returns the next unused groupItem ID. michael@0: getNextID: function GroupItems_getNextID() { michael@0: var result = this.nextID; michael@0: this.nextID++; michael@0: this._save(); michael@0: return result; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: saveAll michael@0: // Saves GroupItems state, as well as the state of all of the groupItems. michael@0: saveAll: function GroupItems_saveAll() { michael@0: this._save(); michael@0: this.groupItems.forEach(function(groupItem) { michael@0: groupItem.save(); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _save michael@0: // Saves GroupItems state. michael@0: _save: function GroupItems__save() { michael@0: if (!this._inited) // too soon to save now michael@0: return; michael@0: michael@0: let activeGroupId = this._activeGroupItem ? this._activeGroupItem.id : null; michael@0: Storage.saveGroupItemsData( michael@0: gWindow, michael@0: { nextID: this.nextID, activeGroupId: activeGroupId, michael@0: totalNumber: this.groupItems.length }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getBoundingBox michael@0: // Given an array of DOM elements, returns a with (roughly) the union of their locations. michael@0: getBoundingBox: function GroupItems_getBoundingBox(els) { michael@0: var bounds = [iQ(el).bounds() for each (el in els)]; michael@0: var left = Math.min.apply({},[ b.left for each (b in bounds) ]); michael@0: var top = Math.min.apply({},[ b.top for each (b in bounds) ]); michael@0: var right = Math.max.apply({},[ b.right for each (b in bounds) ]); michael@0: var bottom = Math.max.apply({},[ b.bottom for each (b in bounds) ]); michael@0: michael@0: return new Rect(left, top, right-left, bottom-top); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: reconstitute michael@0: // Restores to stored state, creating groupItems as needed. michael@0: reconstitute: function GroupItems_reconstitute(groupItemsData, groupItemData) { michael@0: try { michael@0: let activeGroupId; michael@0: michael@0: if (groupItemsData) { michael@0: if (groupItemsData.nextID) michael@0: this.nextID = Math.max(this.nextID, groupItemsData.nextID); michael@0: if (groupItemsData.activeGroupId) michael@0: activeGroupId = groupItemsData.activeGroupId; michael@0: } michael@0: michael@0: if (groupItemData) { michael@0: var toClose = this.groupItems.concat(); michael@0: for (var id in groupItemData) { michael@0: let data = groupItemData[id]; michael@0: if (this.groupItemStorageSanity(data)) { michael@0: let groupItem = this.groupItem(data.id); michael@0: if (groupItem && !groupItem.hidden) { michael@0: groupItem.userSize = data.userSize; michael@0: groupItem.setTitle(data.title); michael@0: groupItem.setBounds(data.bounds, true); michael@0: michael@0: let index = toClose.indexOf(groupItem); michael@0: if (index != -1) michael@0: toClose.splice(index, 1); michael@0: } else { michael@0: var options = { michael@0: dontPush: true, michael@0: immediately: true michael@0: }; michael@0: michael@0: new GroupItem([], Utils.extend({}, data, options)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: toClose.forEach(function(groupItem) { michael@0: // all tabs still existing in closed groups will be moved to new michael@0: // groups. prepare them to be reconnected later. michael@0: groupItem.getChildren().forEach(function (tabItem) { michael@0: if (tabItem.parent.hidden) michael@0: iQ(tabItem.container).show(); michael@0: michael@0: tabItem._reconnected = false; michael@0: michael@0: // sanity check the tab's groupID michael@0: let tabData = Storage.getTabData(tabItem.tab); michael@0: michael@0: if (tabData) { michael@0: let parentGroup = GroupItems.groupItem(tabData.groupID); michael@0: michael@0: // the tab's group id could be invalid or point to a non-existing michael@0: // group. correct it by assigning the active group id or the first michael@0: // group of the just restored session. michael@0: if (!parentGroup || -1 < toClose.indexOf(parentGroup)) { michael@0: tabData.groupID = activeGroupId || Object.keys(groupItemData)[0]; michael@0: Storage.saveTab(tabItem.tab, tabData); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // this closes the group but not its children michael@0: groupItem.close({immediately: true}); michael@0: }); michael@0: } michael@0: michael@0: // set active group item michael@0: if (activeGroupId) { michael@0: let activeGroupItem = this.groupItem(activeGroupId); michael@0: if (activeGroupItem) michael@0: UI.setActive(activeGroupItem); michael@0: } michael@0: michael@0: this._inited = true; michael@0: this._save(); // for nextID michael@0: } catch(e) { michael@0: Utils.log("error in recons: "+e); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: load michael@0: // Loads the storage data for groups. michael@0: // Returns true if there was global group data. michael@0: load: function GroupItems_load() { michael@0: let groupItemsData = Storage.readGroupItemsData(gWindow); michael@0: let groupItemData = Storage.readGroupItemData(gWindow); michael@0: this.reconstitute(groupItemsData, groupItemData); michael@0: michael@0: return (groupItemsData && !Utils.isEmptyObject(groupItemsData)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: groupItemStorageSanity michael@0: // Given persistent storage data for a groupItem, returns true if it appears to not be damaged. michael@0: groupItemStorageSanity: function GroupItems_groupItemStorageSanity(groupItemData) { michael@0: let sane = true; michael@0: if (!groupItemData.bounds || !Utils.isRect(groupItemData.bounds)) { michael@0: Utils.log('GroupItems.groupItemStorageSanity: bad bounds', groupItemData.bounds); michael@0: sane = false; michael@0: } else if ((groupItemData.userSize && michael@0: !Utils.isPoint(groupItemData.userSize)) || michael@0: !groupItemData.id) { michael@0: sane = false; michael@0: } michael@0: michael@0: return sane; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: register michael@0: // Adds the given to the list of groupItems we're tracking. michael@0: register: function GroupItems_register(groupItem) { michael@0: Utils.assert(groupItem, 'groupItem'); michael@0: Utils.assert(this.groupItems.indexOf(groupItem) == -1, 'only register once per groupItem'); michael@0: this.groupItems.push(groupItem); michael@0: UI.updateTabButton(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: unregister michael@0: // Removes the given from the list of groupItems we're tracking. michael@0: unregister: function GroupItems_unregister(groupItem) { michael@0: var index = this.groupItems.indexOf(groupItem); michael@0: if (index != -1) michael@0: this.groupItems.splice(index, 1); michael@0: michael@0: if (groupItem == this._activeGroupItem) michael@0: this._activeGroupItem = null; michael@0: michael@0: this._arrangesPending = this._arrangesPending.filter(function (pending) { michael@0: return groupItem != pending.groupItem; michael@0: }); michael@0: michael@0: this._lastActiveList.remove(groupItem); michael@0: UI.updateTabButton(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: groupItem michael@0: // Given some sort of identifier, returns the appropriate groupItem. michael@0: // Currently only supports groupItem ids. michael@0: groupItem: function GroupItems_groupItem(a) { michael@0: if (!this.groupItems) { michael@0: // uninit has been called michael@0: return null; michael@0: } michael@0: var result = null; michael@0: this.groupItems.forEach(function(candidate) { michael@0: if (candidate.id == a) michael@0: result = candidate; michael@0: }); michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: removeAll michael@0: // Removes all tabs from all groupItems (which automatically closes all unnamed groupItems). michael@0: removeAll: function GroupItems_removeAll() { michael@0: var toRemove = this.groupItems.concat(); michael@0: toRemove.forEach(function(groupItem) { michael@0: groupItem.removeAll(); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: newTab michael@0: // Given a , files it in the appropriate groupItem. michael@0: newTab: function GroupItems_newTab(tabItem, options) { michael@0: let activeGroupItem = this.getActiveGroupItem(); michael@0: michael@0: // 1. Active group michael@0: // 2. First visible non-app tab (that's not the tab in question) michael@0: // 3. First group michael@0: // 4. At this point there should be no groups or tabs (except for app tabs and the michael@0: // tab in question): make a new group michael@0: michael@0: if (activeGroupItem && !activeGroupItem.hidden) { michael@0: activeGroupItem.add(tabItem, options); michael@0: return; michael@0: } michael@0: michael@0: let targetGroupItem; michael@0: // find first non-app visible tab belongs a group, and add the new tabItem michael@0: // to that group michael@0: gBrowser.visibleTabs.some(function(tab) { michael@0: if (!tab.pinned && tab != tabItem.tab) { michael@0: if (tab._tabViewTabItem && tab._tabViewTabItem.parent && michael@0: !tab._tabViewTabItem.parent.hidden) { michael@0: targetGroupItem = tab._tabViewTabItem.parent; michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: michael@0: let visibleGroupItems; michael@0: if (targetGroupItem) { michael@0: // add the new tabItem to the first group item michael@0: targetGroupItem.add(tabItem); michael@0: UI.setActive(targetGroupItem); michael@0: return; michael@0: } else { michael@0: // find the first visible group item michael@0: visibleGroupItems = this.groupItems.filter(function(groupItem) { michael@0: return (!groupItem.hidden); michael@0: }); michael@0: if (visibleGroupItems.length > 0) { michael@0: visibleGroupItems[0].add(tabItem); michael@0: UI.setActive(visibleGroupItems[0]); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // create new group for the new tabItem michael@0: tabItem.setPosition(60, 60, true); michael@0: let newGroupItemBounds = tabItem.getBounds(); michael@0: michael@0: newGroupItemBounds.inset(-40,-40); michael@0: let newGroupItem = new GroupItem([tabItem], { bounds: newGroupItemBounds }); michael@0: newGroupItem.snap(); michael@0: UI.setActive(newGroupItem); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getActiveGroupItem michael@0: // Returns the active groupItem. Active means its tabs are michael@0: // shown in the tab bar when not in the TabView interface. michael@0: getActiveGroupItem: function GroupItems_getActiveGroupItem() { michael@0: return this._activeGroupItem; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setActiveGroupItem michael@0: // Sets the active groupItem, thereby showing only the relevant tabs and michael@0: // setting the groupItem which will receive new tabs. michael@0: // michael@0: // Paramaters: michael@0: // groupItem - the active michael@0: setActiveGroupItem: function GroupItems_setActiveGroupItem(groupItem) { michael@0: Utils.assert(groupItem, "groupItem must be given"); michael@0: michael@0: if (this._activeGroupItem) michael@0: iQ(this._activeGroupItem.container).removeClass('activeGroupItem'); michael@0: michael@0: iQ(groupItem.container).addClass('activeGroupItem'); michael@0: michael@0: this._lastActiveList.update(groupItem); michael@0: this._activeGroupItem = groupItem; michael@0: this._save(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getLastActiveGroupItem michael@0: // Gets last active group item. michael@0: // Returns the . If nothing is found, return null. michael@0: getLastActiveGroupItem: function GroupItem_getLastActiveGroupItem() { michael@0: return this._lastActiveList.peek(function(groupItem) { michael@0: return (groupItem && !groupItem.hidden && groupItem.getChildren().length > 0) michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _updateTabBar michael@0: // Hides and shows tabs in the tab bar based on the active groupItem michael@0: _updateTabBar: function GroupItems__updateTabBar() { michael@0: if (!window.UI) michael@0: return; // called too soon michael@0: michael@0: Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!"); michael@0: michael@0: let tabItems = this._activeGroupItem._children; michael@0: gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: updateActiveGroupItemAndTabBar michael@0: // Sets active TabItem and GroupItem, and updates tab bar appropriately. michael@0: // Parameters: michael@0: // tabItem - the tab item michael@0: // options - is passed to UI.setActive() directly michael@0: updateActiveGroupItemAndTabBar: michael@0: function GroupItems_updateActiveGroupItemAndTabBar(tabItem, options) { michael@0: Utils.assertThrow(tabItem && tabItem.isATabItem, "tabItem must be a TabItem"); michael@0: michael@0: UI.setActive(tabItem, options); michael@0: this._updateTabBar(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getNextGroupItemTab michael@0: // Paramaters: michael@0: // reverse - the boolean indicates the direction to look for the next groupItem. michael@0: // Returns the . If nothing is found, return null. michael@0: getNextGroupItemTab: function GroupItems_getNextGroupItemTab(reverse) { michael@0: var groupItems = Utils.copy(GroupItems.groupItems); michael@0: var activeGroupItem = GroupItems.getActiveGroupItem(); michael@0: var tabItem = null; michael@0: michael@0: if (reverse) michael@0: groupItems = groupItems.reverse(); michael@0: michael@0: if (!activeGroupItem) { michael@0: if (groupItems.length > 0) { michael@0: groupItems.some(function(groupItem) { michael@0: if (!groupItem.hidden) { michael@0: // restore the last active tab in the group michael@0: let activeTab = groupItem.getActiveTab(); michael@0: if (activeTab) { michael@0: tabItem = activeTab; michael@0: return true; michael@0: } michael@0: // if no tab is active, use the first one michael@0: var child = groupItem.getChild(0); michael@0: if (child) { michael@0: tabItem = child; michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }); michael@0: } michael@0: } else { michael@0: var currentIndex; michael@0: groupItems.some(function(groupItem, index) { michael@0: if (!groupItem.hidden && groupItem == activeGroupItem) { michael@0: currentIndex = index; michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: var firstGroupItems = groupItems.slice(currentIndex + 1); michael@0: firstGroupItems.some(function(groupItem) { michael@0: if (!groupItem.hidden) { michael@0: // restore the last active tab in the group michael@0: let activeTab = groupItem.getActiveTab(); michael@0: if (activeTab) { michael@0: tabItem = activeTab; michael@0: return true; michael@0: } michael@0: // if no tab is active, use the first one michael@0: var child = groupItem.getChild(0); michael@0: if (child) { michael@0: tabItem = child; michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }); michael@0: if (!tabItem) { michael@0: var secondGroupItems = groupItems.slice(0, currentIndex); michael@0: secondGroupItems.some(function(groupItem) { michael@0: if (!groupItem.hidden) { michael@0: // restore the last active tab in the group michael@0: let activeTab = groupItem.getActiveTab(); michael@0: if (activeTab) { michael@0: tabItem = activeTab; michael@0: return true; michael@0: } michael@0: // if no tab is active, use the first one michael@0: var child = groupItem.getChild(0); michael@0: if (child) { michael@0: tabItem = child; michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }); michael@0: } michael@0: } michael@0: return tabItem; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: moveTabToGroupItem michael@0: // Used for the right click menu in the tab strip; moves the given tab michael@0: // into the given group. Does nothing if the tab is an app tab. michael@0: // Paramaters: michael@0: // tab - the . michael@0: // groupItemId - the 's id. If nothing, create a new . michael@0: moveTabToGroupItem : function GroupItems_moveTabToGroupItem(tab, groupItemId) { michael@0: if (tab.pinned) michael@0: return; michael@0: michael@0: Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem"); michael@0: michael@0: // given tab is already contained in target group michael@0: if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId) michael@0: return; michael@0: michael@0: let shouldUpdateTabBar = false; michael@0: let shouldShowTabView = false; michael@0: let groupItem; michael@0: michael@0: // switch to the appropriate tab first. michael@0: if (tab.selected) { michael@0: if (gBrowser.visibleTabs.length > 1) { michael@0: gBrowser._blurTab(tab); michael@0: shouldUpdateTabBar = true; michael@0: } else { michael@0: shouldShowTabView = true; michael@0: } michael@0: } else { michael@0: shouldUpdateTabBar = true michael@0: } michael@0: michael@0: // remove tab item from a groupItem michael@0: if (tab._tabViewTabItem.parent) michael@0: tab._tabViewTabItem.parent.remove(tab._tabViewTabItem); michael@0: michael@0: // add tab item to a groupItem michael@0: if (groupItemId) { michael@0: groupItem = GroupItems.groupItem(groupItemId); michael@0: groupItem.add(tab._tabViewTabItem); michael@0: groupItem.reorderTabsBasedOnTabItemOrder() michael@0: } else { michael@0: let pageBounds = Items.getPageBounds(); michael@0: pageBounds.inset(20, 20); michael@0: michael@0: let box = new Rect(pageBounds); michael@0: box.width = 250; michael@0: box.height = 200; michael@0: michael@0: new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true }); michael@0: } michael@0: michael@0: if (shouldUpdateTabBar) michael@0: this._updateTabBar(); michael@0: else if (shouldShowTabView) michael@0: UI.showTabView(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: removeHiddenGroups michael@0: // Removes all hidden groups' data and its browser tabs. michael@0: removeHiddenGroups: function GroupItems_removeHiddenGroups() { michael@0: if (this._removingHiddenGroups) michael@0: return; michael@0: this._removingHiddenGroups = true; michael@0: michael@0: let groupItems = this.groupItems.concat(); michael@0: groupItems.forEach(function(groupItem) { michael@0: if (groupItem.hidden) michael@0: groupItem.closeHidden(); michael@0: }); michael@0: michael@0: this._removingHiddenGroups = false; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getUnclosableGroupItemId michael@0: // If there's only one (non-hidden) group, and there are app tabs present, michael@0: // returns that group. michael@0: // Return the 's Id michael@0: getUnclosableGroupItemId: function GroupItems_getUnclosableGroupItemId() { michael@0: let unclosableGroupItemId = null; michael@0: michael@0: if (gBrowser._numPinnedTabs > 0) { michael@0: let hiddenGroupItems = michael@0: this.groupItems.concat().filter(function(groupItem) { michael@0: return !groupItem.hidden; michael@0: }); michael@0: if (hiddenGroupItems.length == 1) michael@0: unclosableGroupItemId = hiddenGroupItems[0].id; michael@0: } michael@0: michael@0: return unclosableGroupItemId; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: updateGroupCloseButtons michael@0: // Updates group close buttons. michael@0: updateGroupCloseButtons: function GroupItems_updateGroupCloseButtons() { michael@0: let unclosableGroupItemId = this.getUnclosableGroupItemId(); michael@0: michael@0: if (unclosableGroupItemId) { michael@0: let groupItem = this.groupItem(unclosableGroupItemId); michael@0: michael@0: if (groupItem) { michael@0: groupItem.$closeButton.hide(); michael@0: } michael@0: } else { michael@0: this.groupItems.forEach(function(groupItem) { michael@0: groupItem.$closeButton.show(); michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: calcValidSize michael@0: // Basic measure rules. Assures that item is a minimum size. michael@0: calcValidSize: function GroupItems_calcValidSize(size, options) { michael@0: Utils.assert(Utils.isPoint(size), 'input is a Point'); michael@0: Utils.assert((size.x>0 || size.y>0) && (size.x!=0 && size.y!=0), michael@0: "dimensions are valid:"+size.x+","+size.y); michael@0: return new Point( michael@0: Math.max(size.x, GroupItems.minGroupWidth), michael@0: Math.max(size.y, GroupItems.minGroupHeight)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: pauseAutoclose() michael@0: // Temporarily disable the behavior that closes groups when they become michael@0: // empty. This is used when entering private browsing, to avoid trashing the michael@0: // user's groups while private browsing is shuffling things around. michael@0: pauseAutoclose: function GroupItems_pauseAutoclose() { michael@0: this._autoclosePaused = true; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: unpauseAutoclose() michael@0: // Re-enables the auto-close behavior. michael@0: resumeAutoclose: function GroupItems_resumeAutoclose() { michael@0: this._autoclosePaused = false; michael@0: } michael@0: };