browser/components/tabview/groupitems.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 };

mercurial