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