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