|
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: ui.js |
|
7 |
|
8 let Keys = { meta: false }; |
|
9 |
|
10 // ########## |
|
11 // Class: UI |
|
12 // Singleton top-level UI manager. |
|
13 let UI = { |
|
14 // Variable: _frameInitialized |
|
15 // True if the Tab View UI frame has been initialized. |
|
16 _frameInitialized: false, |
|
17 |
|
18 // Variable: _pageBounds |
|
19 // Stores the page bounds. |
|
20 _pageBounds: null, |
|
21 |
|
22 // Variable: _closedLastVisibleTab |
|
23 // If true, the last visible tab has just been closed in the tab strip. |
|
24 _closedLastVisibleTab: false, |
|
25 |
|
26 // Variable: _closedSelectedTabInTabView |
|
27 // If true, a select tab has just been closed in TabView. |
|
28 _closedSelectedTabInTabView: false, |
|
29 |
|
30 // Variable: restoredClosedTab |
|
31 // If true, a closed tab has just been restored. |
|
32 restoredClosedTab: false, |
|
33 |
|
34 // Variable: _isChangingVisibility |
|
35 // Tracks whether we're currently in the process of showing/hiding the tabview. |
|
36 _isChangingVisibility: false, |
|
37 |
|
38 // Variable: _reorderTabItemsOnShow |
|
39 // Keeps track of the <GroupItem>s which their tab items' tabs have been moved |
|
40 // and re-orders the tab items when switching to TabView. |
|
41 _reorderTabItemsOnShow: [], |
|
42 |
|
43 // Variable: _reorderTabsOnHide |
|
44 // Keeps track of the <GroupItem>s which their tab items have been moved in |
|
45 // TabView UI and re-orders the tabs when switcing back to main browser. |
|
46 _reorderTabsOnHide: [], |
|
47 |
|
48 // Variable: _currentTab |
|
49 // Keeps track of which xul:tab we are currently on. |
|
50 // Used to facilitate zooming down from a previous tab. |
|
51 _currentTab: null, |
|
52 |
|
53 // Variable: _eventListeners |
|
54 // Keeps track of event listeners added to the AllTabs object. |
|
55 _eventListeners: {}, |
|
56 |
|
57 // Variable: _cleanupFunctions |
|
58 // An array of functions to be called at uninit time |
|
59 _cleanupFunctions: [], |
|
60 |
|
61 // Constant: _maxInteractiveWait |
|
62 // If the UI is in the middle of an operation, this is the max amount of |
|
63 // milliseconds to wait between input events before we no longer consider |
|
64 // the operation interactive. |
|
65 _maxInteractiveWait: 250, |
|
66 |
|
67 // Variable: _storageBusy |
|
68 // Tells whether the storage is currently busy or not. |
|
69 _storageBusy: false, |
|
70 |
|
71 // Variable: isDOMWindowClosing |
|
72 // Tells wether the parent window is about to close |
|
73 isDOMWindowClosing: false, |
|
74 |
|
75 // Variable: _browserKeys |
|
76 // Used to keep track of allowed browser keys. |
|
77 _browserKeys: null, |
|
78 |
|
79 // Variable: _browserKeysWithShift |
|
80 // Used to keep track of allowed browser keys with Shift key combination. |
|
81 _browserKeysWithShift: null, |
|
82 |
|
83 // Variable: ignoreKeypressForSearch |
|
84 // Used to prevent keypress being handled after quitting search mode. |
|
85 ignoreKeypressForSearch: false, |
|
86 |
|
87 // Variable: _lastOpenedTab |
|
88 // Used to keep track of the last opened tab. |
|
89 _lastOpenedTab: null, |
|
90 |
|
91 // Variable: _originalSmoothScroll |
|
92 // Used to keep track of the tab strip smooth scroll value. |
|
93 _originalSmoothScroll: null, |
|
94 |
|
95 // ---------- |
|
96 // Function: toString |
|
97 // Prints [UI] for debug use |
|
98 toString: function UI_toString() { |
|
99 return "[UI]"; |
|
100 }, |
|
101 |
|
102 // ---------- |
|
103 // Function: init |
|
104 // Must be called after the object is created. |
|
105 init: function UI_init() { |
|
106 try { |
|
107 let self = this; |
|
108 |
|
109 // initialize the direction of the page |
|
110 this._initPageDirection(); |
|
111 |
|
112 // ___ storage |
|
113 Storage.init(); |
|
114 |
|
115 if (Storage.readWindowBusyState(gWindow)) |
|
116 this.storageBusy(); |
|
117 |
|
118 let data = Storage.readUIData(gWindow); |
|
119 this._storageSanity(data); |
|
120 this._pageBounds = data.pageBounds; |
|
121 |
|
122 // ___ search |
|
123 Search.init(); |
|
124 |
|
125 Telemetry.init(); |
|
126 |
|
127 // ___ currentTab |
|
128 this._currentTab = gBrowser.selectedTab; |
|
129 |
|
130 // ___ exit button |
|
131 iQ("#exit-button").click(function() { |
|
132 self.exit(); |
|
133 self.blurAll(); |
|
134 }) |
|
135 .attr("title", tabviewString("button.exitTabGroups")); |
|
136 |
|
137 // When you click on the background/empty part of TabView, |
|
138 // we create a new groupItem. |
|
139 iQ(gTabViewFrame.contentDocument).mousedown(function(e) { |
|
140 if (iQ(":focus").length > 0) { |
|
141 iQ(":focus").each(function(element) { |
|
142 // don't fire blur event if the same input element is clicked. |
|
143 if (e.target != element && element.nodeName == "INPUT") |
|
144 element.blur(); |
|
145 }); |
|
146 } |
|
147 if (e.originalTarget.id == "content" && |
|
148 Utils.isLeftClick(e) && |
|
149 e.detail == 1) { |
|
150 self._createGroupItemOnDrag(e); |
|
151 } |
|
152 }); |
|
153 |
|
154 iQ(gTabViewFrame.contentDocument).dblclick(function(e) { |
|
155 if (e.originalTarget.id != "content") |
|
156 return; |
|
157 |
|
158 // Create a group with one tab on double click |
|
159 let box = |
|
160 new Rect(e.clientX - Math.floor(TabItems.tabWidth/2), |
|
161 e.clientY - Math.floor(TabItems.tabHeight/2), |
|
162 TabItems.tabWidth, TabItems.tabHeight); |
|
163 box.inset(-30, -30); |
|
164 |
|
165 let opts = {immediately: true, bounds: box}; |
|
166 let groupItem = new GroupItem([], opts); |
|
167 groupItem.newTab(); |
|
168 |
|
169 gTabView.firstUseExperienced = true; |
|
170 }); |
|
171 |
|
172 iQ(window).bind("unload", function() { |
|
173 self.uninit(); |
|
174 }); |
|
175 |
|
176 // ___ setup DOMWillOpenModalDialog message handler |
|
177 let mm = gWindow.messageManager; |
|
178 let callback = this._onDOMWillOpenModalDialog.bind(this); |
|
179 mm.addMessageListener("Panorama:DOMWillOpenModalDialog", callback); |
|
180 |
|
181 this._cleanupFunctions.push(function () { |
|
182 mm.removeMessageListener("Panorama:DOMWillOpenModalDialog", callback); |
|
183 }); |
|
184 |
|
185 // ___ setup key handlers |
|
186 this._setTabViewFrameKeyHandlers(); |
|
187 |
|
188 // ___ add tab action handlers |
|
189 this._addTabActionHandlers(); |
|
190 |
|
191 // ___ groups |
|
192 GroupItems.init(); |
|
193 GroupItems.pauseArrange(); |
|
194 let hasGroupItemsData = GroupItems.load(); |
|
195 |
|
196 // ___ tabs |
|
197 TabItems.init(); |
|
198 TabItems.pausePainting(); |
|
199 |
|
200 // ___ favicons |
|
201 FavIcons.init(); |
|
202 |
|
203 if (!hasGroupItemsData) |
|
204 this.reset(); |
|
205 |
|
206 // ___ resizing |
|
207 if (this._pageBounds) |
|
208 this._resize(true); |
|
209 else |
|
210 this._pageBounds = Items.getPageBounds(); |
|
211 |
|
212 iQ(window).resize(function() { |
|
213 self._resize(); |
|
214 }); |
|
215 |
|
216 // ___ setup event listener to save canvas images |
|
217 let onWindowClosing = function () { |
|
218 gWindow.removeEventListener("SSWindowClosing", onWindowClosing, false); |
|
219 |
|
220 // XXX bug #635975 - don't unlink the tab if the dom window is closing. |
|
221 self.isDOMWindowClosing = true; |
|
222 |
|
223 if (self.isTabViewVisible()) |
|
224 GroupItems.removeHiddenGroups(); |
|
225 |
|
226 TabItems.saveAll(); |
|
227 |
|
228 self._save(); |
|
229 }; |
|
230 |
|
231 gWindow.addEventListener("SSWindowClosing", onWindowClosing); |
|
232 this._cleanupFunctions.push(function () { |
|
233 gWindow.removeEventListener("SSWindowClosing", onWindowClosing); |
|
234 }); |
|
235 |
|
236 // ___ load frame script |
|
237 let frameScript = "chrome://browser/content/tabview-content.js"; |
|
238 gWindow.messageManager.loadFrameScript(frameScript, true); |
|
239 |
|
240 // ___ Done |
|
241 this._frameInitialized = true; |
|
242 this._save(); |
|
243 |
|
244 // fire an iframe initialized event so everyone knows tab view is |
|
245 // initialized. |
|
246 let event = document.createEvent("Events"); |
|
247 event.initEvent("tabviewframeinitialized", true, false); |
|
248 dispatchEvent(event); |
|
249 } catch(e) { |
|
250 Utils.log(e); |
|
251 } finally { |
|
252 GroupItems.resumeArrange(); |
|
253 } |
|
254 }, |
|
255 |
|
256 // Function: uninit |
|
257 // Should be called when window is unloaded. |
|
258 uninit: function UI_uninit() { |
|
259 // call our cleanup functions |
|
260 this._cleanupFunctions.forEach(function(func) { |
|
261 func(); |
|
262 }); |
|
263 this._cleanupFunctions = []; |
|
264 |
|
265 // additional clean up |
|
266 TabItems.uninit(); |
|
267 GroupItems.uninit(); |
|
268 FavIcons.uninit(); |
|
269 Storage.uninit(); |
|
270 Telemetry.uninit(); |
|
271 |
|
272 this._removeTabActionHandlers(); |
|
273 this._currentTab = null; |
|
274 this._pageBounds = null; |
|
275 this._reorderTabItemsOnShow = null; |
|
276 this._reorderTabsOnHide = null; |
|
277 this._frameInitialized = false; |
|
278 }, |
|
279 |
|
280 // Property: rtl |
|
281 // Returns true if we are in RTL mode, false otherwise |
|
282 rtl: false, |
|
283 |
|
284 // Function: reset |
|
285 // Resets the Panorama view to have just one group with all tabs |
|
286 reset: function UI_reset() { |
|
287 let padding = Trenches.defaultRadius; |
|
288 let welcomeWidth = 300; |
|
289 let pageBounds = Items.getPageBounds(); |
|
290 pageBounds.inset(padding, padding); |
|
291 |
|
292 let $actions = iQ("#actions"); |
|
293 if ($actions) { |
|
294 pageBounds.width -= $actions.width(); |
|
295 if (UI.rtl) |
|
296 pageBounds.left += $actions.width() - padding; |
|
297 } |
|
298 |
|
299 // ___ make a fresh groupItem |
|
300 let box = new Rect(pageBounds); |
|
301 box.width = Math.min(box.width * 0.667, |
|
302 pageBounds.width - (welcomeWidth + padding)); |
|
303 box.height = box.height * 0.667; |
|
304 if (UI.rtl) { |
|
305 box.left = pageBounds.left + welcomeWidth + 2 * padding; |
|
306 } |
|
307 |
|
308 GroupItems.groupItems.forEach(function(group) { |
|
309 group.close(); |
|
310 }); |
|
311 |
|
312 let options = { |
|
313 bounds: box, |
|
314 immediately: true |
|
315 }; |
|
316 let groupItem = new GroupItem([], options); |
|
317 let items = TabItems.getItems(); |
|
318 items.forEach(function(item) { |
|
319 if (item.parent) |
|
320 item.parent.remove(item); |
|
321 groupItem.add(item, {immediately: true}); |
|
322 }); |
|
323 this.setActive(groupItem); |
|
324 }, |
|
325 |
|
326 // ---------- |
|
327 // Function: blurAll |
|
328 // Blurs any currently focused element |
|
329 blurAll: function UI_blurAll() { |
|
330 iQ(":focus").each(function(element) { |
|
331 element.blur(); |
|
332 }); |
|
333 }, |
|
334 |
|
335 // ---------- |
|
336 // Function: isIdle |
|
337 // Returns true if the last interaction was long enough ago to consider the |
|
338 // UI idle. Used to determine whether interactivity would be sacrificed if |
|
339 // the CPU was to become busy. |
|
340 // |
|
341 isIdle: function UI_isIdle() { |
|
342 let time = Date.now(); |
|
343 let maxEvent = Math.max(drag.lastMoveTime, resize.lastMoveTime); |
|
344 return (time - maxEvent) > this._maxInteractiveWait; |
|
345 }, |
|
346 |
|
347 // ---------- |
|
348 // Function: getActiveTab |
|
349 // Returns the currently active tab as a <TabItem> |
|
350 getActiveTab: function UI_getActiveTab() { |
|
351 return this._activeTab; |
|
352 }, |
|
353 |
|
354 // ---------- |
|
355 // Function: _setActiveTab |
|
356 // Sets the currently active tab. The idea of a focused tab is useful |
|
357 // for keyboard navigation and returning to the last zoomed-in tab. |
|
358 // Hitting return/esc brings you to the focused tab, and using the |
|
359 // arrow keys lets you navigate between open tabs. |
|
360 // |
|
361 // Parameters: |
|
362 // - Takes a <TabItem> |
|
363 _setActiveTab: function UI__setActiveTab(tabItem) { |
|
364 if (tabItem == this._activeTab) |
|
365 return; |
|
366 |
|
367 if (this._activeTab) { |
|
368 this._activeTab.makeDeactive(); |
|
369 this._activeTab.removeSubscriber("close", this._onActiveTabClosed); |
|
370 } |
|
371 |
|
372 this._activeTab = tabItem; |
|
373 |
|
374 if (this._activeTab) { |
|
375 this._activeTab.addSubscriber("close", this._onActiveTabClosed); |
|
376 this._activeTab.makeActive(); |
|
377 } |
|
378 }, |
|
379 |
|
380 // ---------- |
|
381 // Function: _onActiveTabClosed |
|
382 // Handles when the currently active tab gets closed. |
|
383 // |
|
384 // Parameters: |
|
385 // - the <TabItem> that is closed |
|
386 _onActiveTabClosed: function UI__onActiveTabClosed(tabItem){ |
|
387 if (UI._activeTab == tabItem) |
|
388 UI._setActiveTab(null); |
|
389 }, |
|
390 |
|
391 // ---------- |
|
392 // Function: setActive |
|
393 // Sets the active tab item or group item |
|
394 // Parameters: |
|
395 // |
|
396 // options |
|
397 // dontSetActiveTabInGroup bool for not setting active tab in group |
|
398 setActive: function UI_setActive(item, options) { |
|
399 Utils.assert(item, "item must be given"); |
|
400 |
|
401 if (item.isATabItem) { |
|
402 if (item.parent) |
|
403 GroupItems.setActiveGroupItem(item.parent); |
|
404 if (!options || !options.dontSetActiveTabInGroup) |
|
405 this._setActiveTab(item); |
|
406 } else { |
|
407 GroupItems.setActiveGroupItem(item); |
|
408 if (!options || !options.dontSetActiveTabInGroup) { |
|
409 let activeTab = item.getActiveTab(); |
|
410 if (activeTab) |
|
411 this._setActiveTab(activeTab); |
|
412 } |
|
413 } |
|
414 }, |
|
415 |
|
416 // ---------- |
|
417 // Function: clearActiveTab |
|
418 // Sets the active tab to 'null'. |
|
419 clearActiveTab: function UI_clearActiveTab() { |
|
420 this._setActiveTab(null); |
|
421 }, |
|
422 |
|
423 // ---------- |
|
424 // Function: isTabViewVisible |
|
425 // Returns true if the TabView UI is currently shown. |
|
426 isTabViewVisible: function UI_isTabViewVisible() { |
|
427 return gTabViewDeck.selectedPanel == gTabViewFrame; |
|
428 }, |
|
429 |
|
430 // --------- |
|
431 // Function: _initPageDirection |
|
432 // Initializes the page base direction |
|
433 _initPageDirection: function UI__initPageDirection() { |
|
434 let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
|
435 getService(Ci.nsIXULChromeRegistry); |
|
436 let dir = chromeReg.isLocaleRTL("global"); |
|
437 document.documentElement.setAttribute("dir", dir ? "rtl" : "ltr"); |
|
438 this.rtl = dir; |
|
439 }, |
|
440 |
|
441 // ---------- |
|
442 // Function: showTabView |
|
443 // Shows TabView and hides the main browser UI. |
|
444 // Parameters: |
|
445 // zoomOut - true for zoom out animation, false for nothing. |
|
446 showTabView: function UI_showTabView(zoomOut) { |
|
447 if (this.isTabViewVisible() || this._isChangingVisibility) |
|
448 return; |
|
449 |
|
450 this._isChangingVisibility = true; |
|
451 |
|
452 // store tab strip smooth scroll value and disable it. |
|
453 let tabStrip = gBrowser.tabContainer.mTabstrip; |
|
454 this._originalSmoothScroll = tabStrip.smoothScroll; |
|
455 tabStrip.smoothScroll = false; |
|
456 |
|
457 // initialize the direction of the page |
|
458 this._initPageDirection(); |
|
459 |
|
460 var self = this; |
|
461 var currentTab = this._currentTab; |
|
462 |
|
463 this._reorderTabItemsOnShow.forEach(function(groupItem) { |
|
464 groupItem.reorderTabItemsBasedOnTabOrder(); |
|
465 }); |
|
466 this._reorderTabItemsOnShow = []; |
|
467 |
|
468 #ifdef XP_WIN |
|
469 // Restore the full height when showing TabView |
|
470 gTabViewFrame.style.marginTop = ""; |
|
471 #endif |
|
472 gTabViewDeck.selectedPanel = gTabViewFrame; |
|
473 gWindow.TabsInTitlebar.allowedBy("tabview-open", false); |
|
474 gTabViewFrame.contentWindow.focus(); |
|
475 |
|
476 gBrowser.updateTitlebar(); |
|
477 #ifdef XP_MACOSX |
|
478 this.setTitlebarColors(true); |
|
479 #endif |
|
480 let event = document.createEvent("Events"); |
|
481 event.initEvent("tabviewshown", true, false); |
|
482 |
|
483 Storage.saveVisibilityData(gWindow, "true"); |
|
484 |
|
485 if (zoomOut && currentTab && currentTab._tabViewTabItem) { |
|
486 let item = currentTab._tabViewTabItem; |
|
487 // If there was a previous currentTab we want to animate |
|
488 // its thumbnail (canvas) for the zoom out. |
|
489 // Note that we start the animation on the chrome thread. |
|
490 |
|
491 // Zoom out! |
|
492 item.zoomOut(function() { |
|
493 if (!currentTab._tabViewTabItem) // if the tab's been destroyed |
|
494 item = null; |
|
495 |
|
496 self.setActive(item); |
|
497 |
|
498 self._resize(true); |
|
499 self._isChangingVisibility = false; |
|
500 dispatchEvent(event); |
|
501 |
|
502 // Flush pending updates |
|
503 GroupItems.flushAppTabUpdates(); |
|
504 |
|
505 TabItems.resumePainting(); |
|
506 }); |
|
507 } else { |
|
508 if (!currentTab || !currentTab._tabViewTabItem) |
|
509 self.clearActiveTab(); |
|
510 self._isChangingVisibility = false; |
|
511 dispatchEvent(event); |
|
512 |
|
513 // Flush pending updates |
|
514 GroupItems.flushAppTabUpdates(); |
|
515 |
|
516 TabItems.resumePainting(); |
|
517 } |
|
518 |
|
519 if (gTabView.firstUseExperienced) |
|
520 gTabView.enableSessionRestore(); |
|
521 }, |
|
522 |
|
523 // ---------- |
|
524 // Function: hideTabView |
|
525 // Hides TabView and shows the main browser UI. |
|
526 hideTabView: function UI_hideTabView() { |
|
527 if (!this.isTabViewVisible() || this._isChangingVisibility) |
|
528 return; |
|
529 |
|
530 // another tab might be select if user decides to stay on a page when |
|
531 // a onclose confirmation prompts. |
|
532 GroupItems.removeHiddenGroups(); |
|
533 |
|
534 // We need to set this after removing the hidden groups because doing so |
|
535 // might show prompts which will cause us to be called again, and we'd get |
|
536 // stuck if we prevent re-entrancy before doing that. |
|
537 this._isChangingVisibility = true; |
|
538 |
|
539 TabItems.pausePainting(); |
|
540 |
|
541 this._reorderTabsOnHide.forEach(function(groupItem) { |
|
542 groupItem.reorderTabsBasedOnTabItemOrder(); |
|
543 }); |
|
544 this._reorderTabsOnHide = []; |
|
545 |
|
546 #ifdef XP_WIN |
|
547 // Push the top of TabView frame to behind the tabbrowser, so glass can show |
|
548 // XXX bug 586679: avoid shrinking the iframe and squishing iframe contents |
|
549 // as well as avoiding the flash of black as we animate out |
|
550 gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px"; |
|
551 #endif |
|
552 gTabViewDeck.selectedPanel = gBrowserPanel; |
|
553 gWindow.TabsInTitlebar.allowedBy("tabview-open", true); |
|
554 gBrowser.selectedBrowser.focus(); |
|
555 |
|
556 gBrowser.updateTitlebar(); |
|
557 gBrowser.tabContainer.mTabstrip.smoothScroll = this._originalSmoothScroll; |
|
558 #ifdef XP_MACOSX |
|
559 this.setTitlebarColors(false); |
|
560 #endif |
|
561 Storage.saveVisibilityData(gWindow, "false"); |
|
562 |
|
563 this._isChangingVisibility = false; |
|
564 |
|
565 let event = document.createEvent("Events"); |
|
566 event.initEvent("tabviewhidden", true, false); |
|
567 dispatchEvent(event); |
|
568 }, |
|
569 |
|
570 #ifdef XP_MACOSX |
|
571 // ---------- |
|
572 // Function: setTitlebarColors |
|
573 // Used on the Mac to make the title bar match the gradient in the rest of the |
|
574 // TabView UI. |
|
575 // |
|
576 // Parameters: |
|
577 // colors - (bool or object) true for the special TabView color, false for |
|
578 // the normal color, and an object with "active" and "inactive" |
|
579 // properties to specify directly. |
|
580 setTitlebarColors: function UI_setTitlebarColors(colors) { |
|
581 // Mac Only |
|
582 var mainWindow = gWindow.document.getElementById("main-window"); |
|
583 if (colors === true) { |
|
584 mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4"); |
|
585 mainWindow.setAttribute("inactivetitlebarcolor", "#EDEDED"); |
|
586 } else if (colors && "active" in colors && "inactive" in colors) { |
|
587 mainWindow.setAttribute("activetitlebarcolor", colors.active); |
|
588 mainWindow.setAttribute("inactivetitlebarcolor", colors.inactive); |
|
589 } else { |
|
590 mainWindow.removeAttribute("activetitlebarcolor"); |
|
591 mainWindow.removeAttribute("inactivetitlebarcolor"); |
|
592 } |
|
593 }, |
|
594 #endif |
|
595 |
|
596 // ---------- |
|
597 // Function: storageBusy |
|
598 // Pauses the storage activity that conflicts with sessionstore updates. |
|
599 // Calls can be nested. |
|
600 storageBusy: function UI_storageBusy() { |
|
601 if (this._storageBusy) |
|
602 return; |
|
603 |
|
604 this._storageBusy = true; |
|
605 |
|
606 TabItems.pauseReconnecting(); |
|
607 GroupItems.pauseAutoclose(); |
|
608 }, |
|
609 |
|
610 // ---------- |
|
611 // Function: storageReady |
|
612 // Resumes the activity paused by storageBusy, and updates for any new group |
|
613 // information in sessionstore. Calls can be nested. |
|
614 storageReady: function UI_storageReady() { |
|
615 if (!this._storageBusy) |
|
616 return; |
|
617 |
|
618 this._storageBusy = false; |
|
619 |
|
620 let hasGroupItemsData = GroupItems.load(); |
|
621 if (!hasGroupItemsData) |
|
622 this.reset(); |
|
623 |
|
624 TabItems.resumeReconnecting(); |
|
625 GroupItems._updateTabBar(); |
|
626 GroupItems.resumeAutoclose(); |
|
627 }, |
|
628 |
|
629 // ---------- |
|
630 // Function: _addTabActionHandlers |
|
631 // Adds handlers to handle tab actions. |
|
632 _addTabActionHandlers: function UI__addTabActionHandlers() { |
|
633 var self = this; |
|
634 |
|
635 // session restore events |
|
636 function handleSSWindowStateBusy() { |
|
637 self.storageBusy(); |
|
638 } |
|
639 |
|
640 function handleSSWindowStateReady() { |
|
641 self.storageReady(); |
|
642 } |
|
643 |
|
644 gWindow.addEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false); |
|
645 gWindow.addEventListener("SSWindowStateReady", handleSSWindowStateReady, false); |
|
646 |
|
647 this._cleanupFunctions.push(function() { |
|
648 gWindow.removeEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false); |
|
649 gWindow.removeEventListener("SSWindowStateReady", handleSSWindowStateReady, false); |
|
650 }); |
|
651 |
|
652 // TabOpen |
|
653 this._eventListeners.open = function (event) { |
|
654 let tab = event.target; |
|
655 |
|
656 // if it's an app tab, add it to all the group items |
|
657 if (tab.pinned) |
|
658 GroupItems.addAppTab(tab); |
|
659 else if (self.isTabViewVisible() && !self._storageBusyCount) |
|
660 self._lastOpenedTab = tab; |
|
661 }; |
|
662 |
|
663 // TabClose |
|
664 this._eventListeners.close = function (event) { |
|
665 let tab = event.target; |
|
666 |
|
667 // if it's an app tab, remove it from all the group items |
|
668 if (tab.pinned) |
|
669 GroupItems.removeAppTab(tab); |
|
670 |
|
671 if (self.isTabViewVisible()) { |
|
672 // just closed the selected tab in the TabView interface. |
|
673 if (self._currentTab == tab) |
|
674 self._closedSelectedTabInTabView = true; |
|
675 } else { |
|
676 // If we're currently in the process of session store update, |
|
677 // we don't want to go to the Tab View UI. |
|
678 if (self._storageBusy) |
|
679 return; |
|
680 |
|
681 // if not closing the last tab |
|
682 if (gBrowser.tabs.length > 1) { |
|
683 // Don't return to TabView if there are any app tabs |
|
684 for (let a = 0; a < gBrowser._numPinnedTabs; a++) { |
|
685 if (Utils.isValidXULTab(gBrowser.tabs[a])) |
|
686 return; |
|
687 } |
|
688 |
|
689 let groupItem = GroupItems.getActiveGroupItem(); |
|
690 |
|
691 // 1) Only go back to the TabView tab when there you close the last |
|
692 // tab of a groupItem. |
|
693 let closingLastOfGroup = (groupItem && |
|
694 groupItem._children.length == 1 && |
|
695 groupItem._children[0].tab == tab); |
|
696 |
|
697 // 2) When a blank tab is active while restoring a closed tab the |
|
698 // blank tab gets removed. The active group is not closed as this is |
|
699 // where the restored tab goes. So do not show the TabView. |
|
700 let tabItem = tab && tab._tabViewTabItem; |
|
701 let closingBlankTabAfterRestore = |
|
702 (tabItem && tabItem.isRemovedAfterRestore); |
|
703 |
|
704 if (closingLastOfGroup && !closingBlankTabAfterRestore) { |
|
705 // for the tab focus event to pick up. |
|
706 self._closedLastVisibleTab = true; |
|
707 self.showTabView(); |
|
708 } |
|
709 } |
|
710 } |
|
711 }; |
|
712 |
|
713 // TabMove |
|
714 this._eventListeners.move = function (event) { |
|
715 let tab = event.target; |
|
716 |
|
717 if (GroupItems.groupItems.length > 0) { |
|
718 if (tab.pinned) { |
|
719 if (gBrowser._numPinnedTabs > 1) |
|
720 GroupItems.arrangeAppTab(tab); |
|
721 } else { |
|
722 let activeGroupItem = GroupItems.getActiveGroupItem(); |
|
723 if (activeGroupItem) |
|
724 self.setReorderTabItemsOnShow(activeGroupItem); |
|
725 } |
|
726 } |
|
727 }; |
|
728 |
|
729 // TabSelect |
|
730 this._eventListeners.select = function (event) { |
|
731 self.onTabSelect(event.target); |
|
732 }; |
|
733 |
|
734 // TabPinned |
|
735 this._eventListeners.pinned = function (event) { |
|
736 let tab = event.target; |
|
737 |
|
738 TabItems.handleTabPin(tab); |
|
739 GroupItems.addAppTab(tab); |
|
740 }; |
|
741 |
|
742 // TabUnpinned |
|
743 this._eventListeners.unpinned = function (event) { |
|
744 let tab = event.target; |
|
745 |
|
746 TabItems.handleTabUnpin(tab); |
|
747 GroupItems.removeAppTab(tab); |
|
748 |
|
749 let groupItem = tab._tabViewTabItem.parent; |
|
750 if (groupItem) |
|
751 self.setReorderTabItemsOnShow(groupItem); |
|
752 }; |
|
753 |
|
754 // Actually register the above handlers |
|
755 for (let name in this._eventListeners) |
|
756 AllTabs.register(name, this._eventListeners[name]); |
|
757 }, |
|
758 |
|
759 // ---------- |
|
760 // Function: _removeTabActionHandlers |
|
761 // Removes handlers to handle tab actions. |
|
762 _removeTabActionHandlers: function UI__removeTabActionHandlers() { |
|
763 for (let name in this._eventListeners) |
|
764 AllTabs.unregister(name, this._eventListeners[name]); |
|
765 }, |
|
766 |
|
767 // ---------- |
|
768 // Function: goToTab |
|
769 // Selects the given xul:tab in the browser. |
|
770 goToTab: function UI_goToTab(xulTab) { |
|
771 // If it's not focused, the onFocus listener would handle it. |
|
772 if (xulTab.selected) |
|
773 this.onTabSelect(xulTab); |
|
774 else |
|
775 gBrowser.selectedTab = xulTab; |
|
776 }, |
|
777 |
|
778 // ---------- |
|
779 // Function: onTabSelect |
|
780 // Called when the user switches from one tab to another outside of the TabView UI. |
|
781 onTabSelect: function UI_onTabSelect(tab) { |
|
782 this._currentTab = tab; |
|
783 |
|
784 if (this.isTabViewVisible()) { |
|
785 // We want to zoom in if: |
|
786 // 1) we didn't just restore a tab via Ctrl+Shift+T |
|
787 // 2) the currently selected tab is the last created tab and has a tabItem |
|
788 if (!this.restoredClosedTab && |
|
789 this._lastOpenedTab == tab && tab._tabViewTabItem) { |
|
790 tab._tabViewTabItem.zoomIn(true); |
|
791 this._lastOpenedTab = null; |
|
792 return; |
|
793 } |
|
794 if (this._closedLastVisibleTab || |
|
795 (this._closedSelectedTabInTabView && !this.closedLastTabInTabView) || |
|
796 this.restoredClosedTab) { |
|
797 if (this.restoredClosedTab) { |
|
798 // when the tab view UI is being displayed, update the thumb for the |
|
799 // restored closed tab after the page load |
|
800 tab.linkedBrowser.addEventListener("load", function onLoad(event) { |
|
801 tab.linkedBrowser.removeEventListener("load", onLoad, true); |
|
802 TabItems._update(tab); |
|
803 }, true); |
|
804 } |
|
805 this._closedLastVisibleTab = false; |
|
806 this._closedSelectedTabInTabView = false; |
|
807 this.closedLastTabInTabView = false; |
|
808 this.restoredClosedTab = false; |
|
809 return; |
|
810 } |
|
811 } |
|
812 // reset these vars, just in case. |
|
813 this._closedLastVisibleTab = false; |
|
814 this._closedSelectedTabInTabView = false; |
|
815 this.closedLastTabInTabView = false; |
|
816 this.restoredClosedTab = false; |
|
817 this._lastOpenedTab = null; |
|
818 |
|
819 // if TabView is visible but we didn't just close the last tab or |
|
820 // selected tab, show chrome. |
|
821 if (this.isTabViewVisible()) { |
|
822 // Unhide the group of the tab the user is activating. |
|
823 if (tab && tab._tabViewTabItem && tab._tabViewTabItem.parent && |
|
824 tab._tabViewTabItem.parent.hidden) |
|
825 tab._tabViewTabItem.parent._unhide({immediately: true}); |
|
826 |
|
827 this.hideTabView(); |
|
828 } |
|
829 |
|
830 // another tab might be selected when hideTabView() is invoked so a |
|
831 // validation is needed. |
|
832 if (this._currentTab != tab) |
|
833 return; |
|
834 |
|
835 let newItem = null; |
|
836 // update the tab bar for the new tab's group |
|
837 if (tab && tab._tabViewTabItem) { |
|
838 if (!TabItems.reconnectingPaused()) { |
|
839 newItem = tab._tabViewTabItem; |
|
840 GroupItems.updateActiveGroupItemAndTabBar(newItem); |
|
841 } |
|
842 } else { |
|
843 // No tabItem; must be an app tab. Base the tab bar on the current group. |
|
844 // If no current group, figure it out based on what's already in the tab |
|
845 // bar. |
|
846 if (!GroupItems.getActiveGroupItem()) { |
|
847 for (let a = 0; a < gBrowser.tabs.length; a++) { |
|
848 let theTab = gBrowser.tabs[a]; |
|
849 if (!theTab.pinned) { |
|
850 let tabItem = theTab._tabViewTabItem; |
|
851 this.setActive(tabItem.parent); |
|
852 break; |
|
853 } |
|
854 } |
|
855 } |
|
856 |
|
857 if (GroupItems.getActiveGroupItem()) |
|
858 GroupItems._updateTabBar(); |
|
859 } |
|
860 }, |
|
861 |
|
862 // ---------- |
|
863 // Function: _onDOMWillOpenModalDialog |
|
864 // Called when a web page is about to show a modal dialog. |
|
865 _onDOMWillOpenModalDialog: function UI__onDOMWillOpenModalDialog(cx) { |
|
866 if (!this.isTabViewVisible()) |
|
867 return; |
|
868 |
|
869 let index = gBrowser.browsers.indexOf(cx.target); |
|
870 if (index == -1) |
|
871 return; |
|
872 |
|
873 let tab = gBrowser.tabs[index]; |
|
874 |
|
875 // When TabView is visible, we need to call onTabSelect to make sure that |
|
876 // TabView is hidden and that the correct group is activated. When a modal |
|
877 // dialog is shown for currently selected tab the onTabSelect event handler |
|
878 // is not called, so we need to do it. |
|
879 if (tab.selected && this._currentTab == tab) |
|
880 this.onTabSelect(tab); |
|
881 }, |
|
882 |
|
883 // ---------- |
|
884 // Function: setReorderTabsOnHide |
|
885 // Sets the groupItem which the tab items' tabs should be re-ordered when |
|
886 // switching to the main browser UI. |
|
887 // Parameters: |
|
888 // groupItem - the groupItem which would be used for re-ordering tabs. |
|
889 setReorderTabsOnHide: function UI_setReorderTabsOnHide(groupItem) { |
|
890 if (this.isTabViewVisible()) { |
|
891 var index = this._reorderTabsOnHide.indexOf(groupItem); |
|
892 if (index == -1) |
|
893 this._reorderTabsOnHide.push(groupItem); |
|
894 } |
|
895 }, |
|
896 |
|
897 // ---------- |
|
898 // Function: setReorderTabItemsOnShow |
|
899 // Sets the groupItem which the tab items should be re-ordered when |
|
900 // switching to the tab view UI. |
|
901 // Parameters: |
|
902 // groupItem - the groupItem which would be used for re-ordering tab items. |
|
903 setReorderTabItemsOnShow: function UI_setReorderTabItemsOnShow(groupItem) { |
|
904 if (!this.isTabViewVisible()) { |
|
905 var index = this._reorderTabItemsOnShow.indexOf(groupItem); |
|
906 if (index == -1) |
|
907 this._reorderTabItemsOnShow.push(groupItem); |
|
908 } |
|
909 }, |
|
910 |
|
911 // ---------- |
|
912 updateTabButton: function UI_updateTabButton() { |
|
913 let exitButton = document.getElementById("exit-button"); |
|
914 let numberOfGroups = GroupItems.groupItems.length; |
|
915 |
|
916 exitButton.setAttribute("groups", numberOfGroups); |
|
917 gTabView.updateGroupNumberBroadcaster(numberOfGroups); |
|
918 }, |
|
919 |
|
920 // ---------- |
|
921 // Function: getClosestTab |
|
922 // Convenience function to get the next tab closest to the entered position |
|
923 getClosestTab: function UI_getClosestTab(tabCenter) { |
|
924 let cl = null; |
|
925 let clDist; |
|
926 TabItems.getItems().forEach(function (item) { |
|
927 if (!item.parent || item.parent.hidden) |
|
928 return; |
|
929 let testDist = tabCenter.distance(item.bounds.center()); |
|
930 if (cl==null || testDist < clDist) { |
|
931 cl = item; |
|
932 clDist = testDist; |
|
933 } |
|
934 }); |
|
935 return cl; |
|
936 }, |
|
937 |
|
938 // ---------- |
|
939 // Function: _setupBrowserKeys |
|
940 // Sets up the allowed browser keys using key elements. |
|
941 _setupBrowserKeys: function UI__setupKeyWhiteList() { |
|
942 let keys = {}; |
|
943 |
|
944 [ |
|
945 #ifdef XP_UNIX |
|
946 "quitApplication", |
|
947 #else |
|
948 "redo", |
|
949 #endif |
|
950 #ifdef XP_MACOSX |
|
951 "preferencesCmdMac", "minimizeWindow", "hideThisAppCmdMac", |
|
952 #endif |
|
953 "newNavigator", "newNavigatorTab", "undo", "cut", "copy", "paste", |
|
954 "selectAll", "find" |
|
955 ].forEach(function(key) { |
|
956 let element = gWindow.document.getElementById("key_" + key); |
|
957 let code = element.getAttribute("key").toLocaleLowerCase().charCodeAt(0); |
|
958 keys[code] = key; |
|
959 }); |
|
960 this._browserKeys = keys; |
|
961 |
|
962 keys = {}; |
|
963 // The lower case letters are passed to processBrowserKeys() even with shift |
|
964 // key when stimulating a key press using EventUtils.synthesizeKey() so need |
|
965 // to handle both upper and lower cases here. |
|
966 [ |
|
967 #ifdef XP_UNIX |
|
968 "redo", |
|
969 #endif |
|
970 #ifdef XP_MACOSX |
|
971 "fullScreen", |
|
972 #endif |
|
973 "closeWindow", "tabview", "undoCloseTab", "undoCloseWindow" |
|
974 ].forEach(function(key) { |
|
975 let element = gWindow.document.getElementById("key_" + key); |
|
976 let code = element.getAttribute("key").toLocaleLowerCase().charCodeAt(0); |
|
977 keys[code] = key; |
|
978 }); |
|
979 this._browserKeysWithShift = keys; |
|
980 }, |
|
981 |
|
982 // ---------- |
|
983 // Function: _setTabViewFrameKeyHandlers |
|
984 // Sets up the key handlers for navigating between tabs within the TabView UI. |
|
985 _setTabViewFrameKeyHandlers: function UI__setTabViewFrameKeyHandlers() { |
|
986 let self = this; |
|
987 |
|
988 this._setupBrowserKeys(); |
|
989 |
|
990 iQ(window).keyup(function(event) { |
|
991 if (!event.metaKey) |
|
992 Keys.meta = false; |
|
993 }); |
|
994 |
|
995 iQ(window).keypress(function(event) { |
|
996 if (event.metaKey) |
|
997 Keys.meta = true; |
|
998 |
|
999 function processBrowserKeys(evt) { |
|
1000 // let any keys with alt to pass through |
|
1001 if (evt.altKey) |
|
1002 return; |
|
1003 |
|
1004 #ifdef XP_MACOSX |
|
1005 if (evt.metaKey) { |
|
1006 #else |
|
1007 if (evt.ctrlKey) { |
|
1008 #endif |
|
1009 let preventDefault = true; |
|
1010 if (evt.shiftKey) { |
|
1011 // when a user presses ctrl+shift+key, upper case letter charCode |
|
1012 // is passed to processBrowserKeys() so converting back to lower |
|
1013 // case charCode before doing the check |
|
1014 let lowercaseCharCode = |
|
1015 String.fromCharCode(evt.charCode).toLocaleLowerCase().charCodeAt(0); |
|
1016 if (lowercaseCharCode in self._browserKeysWithShift) { |
|
1017 let key = self._browserKeysWithShift[lowercaseCharCode]; |
|
1018 if (key == "tabview") |
|
1019 self.exit(); |
|
1020 else |
|
1021 preventDefault = false; |
|
1022 } |
|
1023 } else { |
|
1024 if (evt.charCode in self._browserKeys) { |
|
1025 let key = self._browserKeys[evt.charCode]; |
|
1026 if (key == "find") |
|
1027 self.enableSearch(); |
|
1028 else |
|
1029 preventDefault = false; |
|
1030 } |
|
1031 } |
|
1032 if (preventDefault) { |
|
1033 evt.stopPropagation(); |
|
1034 evt.preventDefault(); |
|
1035 } |
|
1036 } |
|
1037 } |
|
1038 if ((iQ(":focus").length > 0 && iQ(":focus")[0].nodeName == "INPUT") || |
|
1039 Search.isEnabled() || self.ignoreKeypressForSearch) { |
|
1040 self.ignoreKeypressForSearch = false; |
|
1041 processBrowserKeys(event); |
|
1042 return; |
|
1043 } |
|
1044 |
|
1045 function getClosestTabBy(norm) { |
|
1046 if (!self.getActiveTab()) |
|
1047 return null; |
|
1048 |
|
1049 let activeTab = self.getActiveTab(); |
|
1050 let activeTabGroup = activeTab.parent; |
|
1051 let myCenter = activeTab.bounds.center(); |
|
1052 let match; |
|
1053 |
|
1054 TabItems.getItems().forEach(function (item) { |
|
1055 if (!item.parent.hidden && |
|
1056 (!activeTabGroup.expanded || activeTabGroup.id == item.parent.id)) { |
|
1057 let itemCenter = item.bounds.center(); |
|
1058 |
|
1059 if (norm(itemCenter, myCenter)) { |
|
1060 let itemDist = myCenter.distance(itemCenter); |
|
1061 if (!match || match[0] > itemDist) |
|
1062 match = [itemDist, item]; |
|
1063 } |
|
1064 } |
|
1065 }); |
|
1066 |
|
1067 return match && match[1]; |
|
1068 } |
|
1069 |
|
1070 let preventDefault = true; |
|
1071 let activeTab; |
|
1072 let activeGroupItem; |
|
1073 let norm = null; |
|
1074 switch (event.keyCode) { |
|
1075 case KeyEvent.DOM_VK_RIGHT: |
|
1076 norm = function(a, me){return a.x > me.x}; |
|
1077 break; |
|
1078 case KeyEvent.DOM_VK_LEFT: |
|
1079 norm = function(a, me){return a.x < me.x}; |
|
1080 break; |
|
1081 case KeyEvent.DOM_VK_DOWN: |
|
1082 norm = function(a, me){return a.y > me.y}; |
|
1083 break; |
|
1084 case KeyEvent.DOM_VK_UP: |
|
1085 norm = function(a, me){return a.y < me.y} |
|
1086 break; |
|
1087 } |
|
1088 |
|
1089 if (norm != null) { |
|
1090 let nextTab = getClosestTabBy(norm); |
|
1091 if (nextTab) { |
|
1092 if (nextTab.isStacked && !nextTab.parent.expanded) |
|
1093 nextTab = nextTab.parent.getChild(0); |
|
1094 self.setActive(nextTab); |
|
1095 } |
|
1096 } else { |
|
1097 switch(event.keyCode) { |
|
1098 case KeyEvent.DOM_VK_ESCAPE: |
|
1099 activeGroupItem = GroupItems.getActiveGroupItem(); |
|
1100 if (activeGroupItem && activeGroupItem.expanded) |
|
1101 activeGroupItem.collapse(); |
|
1102 else |
|
1103 self.exit(); |
|
1104 break; |
|
1105 case KeyEvent.DOM_VK_RETURN: |
|
1106 activeGroupItem = GroupItems.getActiveGroupItem(); |
|
1107 if (activeGroupItem) { |
|
1108 activeTab = self.getActiveTab(); |
|
1109 |
|
1110 if (!activeTab || activeTab.parent != activeGroupItem) |
|
1111 activeTab = activeGroupItem.getActiveTab(); |
|
1112 |
|
1113 if (activeTab) |
|
1114 activeTab.zoomIn(); |
|
1115 else |
|
1116 activeGroupItem.newTab(); |
|
1117 } |
|
1118 break; |
|
1119 case KeyEvent.DOM_VK_TAB: |
|
1120 // tab/shift + tab to go to the next tab. |
|
1121 activeTab = self.getActiveTab(); |
|
1122 if (activeTab) { |
|
1123 let tabItems = (activeTab.parent ? activeTab.parent.getChildren() : |
|
1124 [activeTab]); |
|
1125 let length = tabItems.length; |
|
1126 let currentIndex = tabItems.indexOf(activeTab); |
|
1127 |
|
1128 if (length > 1) { |
|
1129 let newIndex; |
|
1130 if (event.shiftKey) { |
|
1131 if (currentIndex == 0) |
|
1132 newIndex = (length - 1); |
|
1133 else |
|
1134 newIndex = (currentIndex - 1); |
|
1135 } else { |
|
1136 if (currentIndex == (length - 1)) |
|
1137 newIndex = 0; |
|
1138 else |
|
1139 newIndex = (currentIndex + 1); |
|
1140 } |
|
1141 self.setActive(tabItems[newIndex]); |
|
1142 } |
|
1143 } |
|
1144 break; |
|
1145 default: |
|
1146 processBrowserKeys(event); |
|
1147 preventDefault = false; |
|
1148 } |
|
1149 if (preventDefault) { |
|
1150 event.stopPropagation(); |
|
1151 event.preventDefault(); |
|
1152 } |
|
1153 } |
|
1154 }); |
|
1155 }, |
|
1156 |
|
1157 // ---------- |
|
1158 // Function: enableSearch |
|
1159 // Enables the search feature. |
|
1160 enableSearch: function UI_enableSearch() { |
|
1161 if (!Search.isEnabled()) { |
|
1162 Search.ensureShown(); |
|
1163 Search.switchToInMode(); |
|
1164 } |
|
1165 }, |
|
1166 |
|
1167 // ---------- |
|
1168 // Function: _createGroupItemOnDrag |
|
1169 // Called in response to a mousedown in empty space in the TabView UI; |
|
1170 // creates a new groupItem based on the user's drag. |
|
1171 _createGroupItemOnDrag: function UI__createGroupItemOnDrag(e) { |
|
1172 const minSize = 60; |
|
1173 const minMinSize = 15; |
|
1174 |
|
1175 let lastActiveGroupItem = GroupItems.getActiveGroupItem(); |
|
1176 |
|
1177 var startPos = { x: e.clientX, y: e.clientY }; |
|
1178 var phantom = iQ("<div>") |
|
1179 .addClass("groupItem phantom activeGroupItem dragRegion") |
|
1180 .css({ |
|
1181 position: "absolute", |
|
1182 zIndex: -1, |
|
1183 cursor: "default" |
|
1184 }) |
|
1185 .appendTo("body"); |
|
1186 |
|
1187 var item = { // a faux-Item |
|
1188 container: phantom, |
|
1189 isAFauxItem: true, |
|
1190 bounds: {}, |
|
1191 getBounds: function FauxItem_getBounds() { |
|
1192 return this.container.bounds(); |
|
1193 }, |
|
1194 setBounds: function FauxItem_setBounds(bounds) { |
|
1195 this.container.css(bounds); |
|
1196 }, |
|
1197 setZ: function FauxItem_setZ(z) { |
|
1198 // don't set a z-index because we want to force it to be low. |
|
1199 }, |
|
1200 setOpacity: function FauxItem_setOpacity(opacity) { |
|
1201 this.container.css("opacity", opacity); |
|
1202 }, |
|
1203 // we don't need to pushAway the phantom item at the end, because |
|
1204 // when we create a new GroupItem, it'll do the actual pushAway. |
|
1205 pushAway: function () {}, |
|
1206 }; |
|
1207 item.setBounds(new Rect(startPos.y, startPos.x, 0, 0)); |
|
1208 |
|
1209 var dragOutInfo = new Drag(item, e); |
|
1210 |
|
1211 function updateSize(e) { |
|
1212 var box = new Rect(); |
|
1213 box.left = Math.min(startPos.x, e.clientX); |
|
1214 box.right = Math.max(startPos.x, e.clientX); |
|
1215 box.top = Math.min(startPos.y, e.clientY); |
|
1216 box.bottom = Math.max(startPos.y, e.clientY); |
|
1217 item.setBounds(box); |
|
1218 |
|
1219 // compute the stationaryCorner |
|
1220 var stationaryCorner = ""; |
|
1221 |
|
1222 if (startPos.y == box.top) |
|
1223 stationaryCorner += "top"; |
|
1224 else |
|
1225 stationaryCorner += "bottom"; |
|
1226 |
|
1227 if (startPos.x == box.left) |
|
1228 stationaryCorner += "left"; |
|
1229 else |
|
1230 stationaryCorner += "right"; |
|
1231 |
|
1232 dragOutInfo.snap(stationaryCorner, false, false); // null for ui, which we don't use anyway. |
|
1233 |
|
1234 box = item.getBounds(); |
|
1235 if (box.width > minMinSize && box.height > minMinSize && |
|
1236 (box.width > minSize || box.height > minSize)) |
|
1237 item.setOpacity(1); |
|
1238 else |
|
1239 item.setOpacity(0.7); |
|
1240 |
|
1241 e.preventDefault(); |
|
1242 } |
|
1243 |
|
1244 let self = this; |
|
1245 function collapse() { |
|
1246 let center = phantom.bounds().center(); |
|
1247 phantom.animate({ |
|
1248 width: 0, |
|
1249 height: 0, |
|
1250 top: center.y, |
|
1251 left: center.x |
|
1252 }, { |
|
1253 duration: 300, |
|
1254 complete: function() { |
|
1255 phantom.remove(); |
|
1256 } |
|
1257 }); |
|
1258 self.setActive(lastActiveGroupItem); |
|
1259 } |
|
1260 |
|
1261 function finalize(e) { |
|
1262 iQ(window).unbind("mousemove", updateSize); |
|
1263 item.container.removeClass("dragRegion"); |
|
1264 dragOutInfo.stop(); |
|
1265 let box = item.getBounds(); |
|
1266 if (box.width > minMinSize && box.height > minMinSize && |
|
1267 (box.width > minSize || box.height > minSize)) { |
|
1268 let opts = {bounds: item.getBounds(), focusTitle: true}; |
|
1269 let groupItem = new GroupItem([], opts); |
|
1270 self.setActive(groupItem); |
|
1271 phantom.remove(); |
|
1272 dragOutInfo = null; |
|
1273 gTabView.firstUseExperienced = true; |
|
1274 } else { |
|
1275 collapse(); |
|
1276 } |
|
1277 } |
|
1278 |
|
1279 iQ(window).mousemove(updateSize) |
|
1280 iQ(gWindow).one("mouseup", finalize); |
|
1281 e.preventDefault(); |
|
1282 return false; |
|
1283 }, |
|
1284 |
|
1285 // ---------- |
|
1286 // Function: _resize |
|
1287 // Update the TabView UI contents in response to a window size change. |
|
1288 // Won't do anything if it doesn't deem the resize necessary. |
|
1289 // Parameters: |
|
1290 // force - true to update even when "unnecessary"; default false |
|
1291 _resize: function UI__resize(force) { |
|
1292 if (!this._pageBounds) |
|
1293 return; |
|
1294 |
|
1295 // Here are reasons why we *won't* resize: |
|
1296 // 1. Panorama isn't visible (in which case we will resize when we do display) |
|
1297 // 2. the screen dimensions haven't changed |
|
1298 // 3. everything on the screen fits and nothing feels cramped |
|
1299 if (!force && !this.isTabViewVisible()) |
|
1300 return; |
|
1301 |
|
1302 let oldPageBounds = new Rect(this._pageBounds); |
|
1303 let newPageBounds = Items.getPageBounds(); |
|
1304 if (newPageBounds.equals(oldPageBounds)) |
|
1305 return; |
|
1306 |
|
1307 if (!this.shouldResizeItems()) |
|
1308 return; |
|
1309 |
|
1310 var items = Items.getTopLevelItems(); |
|
1311 |
|
1312 // compute itemBounds: the union of all the top-level items' bounds. |
|
1313 var itemBounds = new Rect(this._pageBounds); |
|
1314 // We start with pageBounds so that we respect the empty space the user |
|
1315 // has left on the page. |
|
1316 itemBounds.width = 1; |
|
1317 itemBounds.height = 1; |
|
1318 items.forEach(function(item) { |
|
1319 var bounds = item.getBounds(); |
|
1320 itemBounds = (itemBounds ? itemBounds.union(bounds) : new Rect(bounds)); |
|
1321 }); |
|
1322 |
|
1323 if (newPageBounds.width < this._pageBounds.width && |
|
1324 newPageBounds.width > itemBounds.width) |
|
1325 newPageBounds.width = this._pageBounds.width; |
|
1326 |
|
1327 if (newPageBounds.height < this._pageBounds.height && |
|
1328 newPageBounds.height > itemBounds.height) |
|
1329 newPageBounds.height = this._pageBounds.height; |
|
1330 |
|
1331 var wScale; |
|
1332 var hScale; |
|
1333 if (Math.abs(newPageBounds.width - this._pageBounds.width) |
|
1334 > Math.abs(newPageBounds.height - this._pageBounds.height)) { |
|
1335 wScale = newPageBounds.width / this._pageBounds.width; |
|
1336 hScale = newPageBounds.height / itemBounds.height; |
|
1337 } else { |
|
1338 wScale = newPageBounds.width / itemBounds.width; |
|
1339 hScale = newPageBounds.height / this._pageBounds.height; |
|
1340 } |
|
1341 |
|
1342 var scale = Math.min(hScale, wScale); |
|
1343 var self = this; |
|
1344 var pairs = []; |
|
1345 items.forEach(function(item) { |
|
1346 var bounds = item.getBounds(); |
|
1347 bounds.left += (UI.rtl ? -1 : 1) * (newPageBounds.left - self._pageBounds.left); |
|
1348 bounds.left *= scale; |
|
1349 bounds.width *= scale; |
|
1350 |
|
1351 bounds.top += newPageBounds.top - self._pageBounds.top; |
|
1352 bounds.top *= scale; |
|
1353 bounds.height *= scale; |
|
1354 |
|
1355 pairs.push({ |
|
1356 item: item, |
|
1357 bounds: bounds |
|
1358 }); |
|
1359 }); |
|
1360 |
|
1361 Items.unsquish(pairs); |
|
1362 |
|
1363 pairs.forEach(function(pair) { |
|
1364 pair.item.setBounds(pair.bounds, true); |
|
1365 pair.item.snap(); |
|
1366 }); |
|
1367 |
|
1368 this._pageBounds = Items.getPageBounds(); |
|
1369 this._save(); |
|
1370 }, |
|
1371 |
|
1372 // ---------- |
|
1373 // Function: shouldResizeItems |
|
1374 // Returns whether we should resize the items on the screen, based on whether |
|
1375 // the top-level items fit in the screen or not and whether they feel |
|
1376 // "cramped" or not. |
|
1377 // These computations may be done using cached values. The cache can be |
|
1378 // cleared with UI.clearShouldResizeItems(). |
|
1379 shouldResizeItems: function UI_shouldResizeItems() { |
|
1380 let newPageBounds = Items.getPageBounds(); |
|
1381 |
|
1382 // If we don't have cached cached values... |
|
1383 if (this._minimalRect === undefined || this._feelsCramped === undefined) { |
|
1384 |
|
1385 // Loop through every top-level Item for two operations: |
|
1386 // 1. check if it is feeling "cramped" due to squishing (a technical term), |
|
1387 // 2. union its bounds with the minimalRect |
|
1388 let feelsCramped = false; |
|
1389 let minimalRect = new Rect(0, 0, 1, 1); |
|
1390 |
|
1391 Items.getTopLevelItems() |
|
1392 .forEach(function UI_shouldResizeItems_checkItem(item) { |
|
1393 let bounds = new Rect(item.getBounds()); |
|
1394 feelsCramped = feelsCramped || (item.userSize && |
|
1395 (item.userSize.x > bounds.width || item.userSize.y > bounds.height)); |
|
1396 bounds.inset(-Trenches.defaultRadius, -Trenches.defaultRadius); |
|
1397 minimalRect = minimalRect.union(bounds); |
|
1398 }); |
|
1399 |
|
1400 // ensure the minimalRect extends to, but not beyond, the origin |
|
1401 minimalRect.left = 0; |
|
1402 minimalRect.top = 0; |
|
1403 |
|
1404 this._minimalRect = minimalRect; |
|
1405 this._feelsCramped = feelsCramped; |
|
1406 } |
|
1407 |
|
1408 return this._minimalRect.width > newPageBounds.width || |
|
1409 this._minimalRect.height > newPageBounds.height || |
|
1410 this._feelsCramped; |
|
1411 }, |
|
1412 |
|
1413 // ---------- |
|
1414 // Function: clearShouldResizeItems |
|
1415 // Clear the cache of whether we should resize the items on the Panorama |
|
1416 // screen, forcing a recomputation on the next UI.shouldResizeItems() |
|
1417 // call. |
|
1418 clearShouldResizeItems: function UI_clearShouldResizeItems() { |
|
1419 delete this._minimalRect; |
|
1420 delete this._feelsCramped; |
|
1421 }, |
|
1422 |
|
1423 // ---------- |
|
1424 // Function: exit |
|
1425 // Exits TabView UI. |
|
1426 exit: function UI_exit() { |
|
1427 let self = this; |
|
1428 let zoomedIn = false; |
|
1429 |
|
1430 if (Search.isEnabled()) { |
|
1431 let matcher = Search.createSearchTabMatcher(); |
|
1432 let matches = matcher.matched(); |
|
1433 |
|
1434 if (matches.length > 0) { |
|
1435 matches[0].zoomIn(); |
|
1436 zoomedIn = true; |
|
1437 } |
|
1438 Search.hide(); |
|
1439 } |
|
1440 |
|
1441 if (!zoomedIn) { |
|
1442 let unhiddenGroups = GroupItems.groupItems.filter(function(groupItem) { |
|
1443 return (!groupItem.hidden && groupItem.getChildren().length > 0); |
|
1444 }); |
|
1445 // no pinned tabs and no visible groups: open a new group. open a blank |
|
1446 // tab and return |
|
1447 if (!unhiddenGroups.length) { |
|
1448 let emptyGroups = GroupItems.groupItems.filter(function (groupItem) { |
|
1449 return (!groupItem.hidden && !groupItem.getChildren().length); |
|
1450 }); |
|
1451 let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup()); |
|
1452 if (!gBrowser._numPinnedTabs) { |
|
1453 group.newTab(null, { closedLastTab: true }); |
|
1454 return; |
|
1455 } |
|
1456 } |
|
1457 |
|
1458 // If there's an active TabItem, zoom into it. If not (for instance when the |
|
1459 // selected tab is an app tab), just go there. |
|
1460 let activeTabItem = this.getActiveTab(); |
|
1461 if (!activeTabItem) { |
|
1462 let tabItem = gBrowser.selectedTab._tabViewTabItem; |
|
1463 if (tabItem) { |
|
1464 if (!tabItem.parent || !tabItem.parent.hidden) { |
|
1465 activeTabItem = tabItem; |
|
1466 } else { // set active tab item if there is at least one unhidden group |
|
1467 if (unhiddenGroups.length > 0) |
|
1468 activeTabItem = unhiddenGroups[0].getActiveTab(); |
|
1469 } |
|
1470 } |
|
1471 } |
|
1472 |
|
1473 if (activeTabItem) { |
|
1474 activeTabItem.zoomIn(); |
|
1475 } else { |
|
1476 if (gBrowser._numPinnedTabs > 0) { |
|
1477 if (gBrowser.selectedTab.pinned) { |
|
1478 self.goToTab(gBrowser.selectedTab); |
|
1479 } else { |
|
1480 Array.some(gBrowser.tabs, function(tab) { |
|
1481 if (tab.pinned) { |
|
1482 self.goToTab(tab); |
|
1483 return true; |
|
1484 } |
|
1485 return false |
|
1486 }); |
|
1487 } |
|
1488 } |
|
1489 } |
|
1490 } |
|
1491 }, |
|
1492 |
|
1493 // ---------- |
|
1494 // Function: storageSanity |
|
1495 // Given storage data for this object, returns true if it looks valid. |
|
1496 _storageSanity: function UI__storageSanity(data) { |
|
1497 if (Utils.isEmptyObject(data)) |
|
1498 return true; |
|
1499 |
|
1500 if (!Utils.isRect(data.pageBounds)) { |
|
1501 Utils.log("UI.storageSanity: bad pageBounds", data.pageBounds); |
|
1502 data.pageBounds = null; |
|
1503 return false; |
|
1504 } |
|
1505 |
|
1506 return true; |
|
1507 }, |
|
1508 |
|
1509 // ---------- |
|
1510 // Function: _save |
|
1511 // Saves the data for this object to persistent storage |
|
1512 _save: function UI__save() { |
|
1513 if (!this._frameInitialized) |
|
1514 return; |
|
1515 |
|
1516 var data = { |
|
1517 pageBounds: this._pageBounds |
|
1518 }; |
|
1519 |
|
1520 if (this._storageSanity(data)) |
|
1521 Storage.saveUIData(gWindow, data); |
|
1522 }, |
|
1523 |
|
1524 // ---------- |
|
1525 // Function: _saveAll |
|
1526 // Saves all data associated with TabView. |
|
1527 _saveAll: function UI__saveAll() { |
|
1528 this._save(); |
|
1529 GroupItems.saveAll(); |
|
1530 TabItems.saveAll(); |
|
1531 }, |
|
1532 |
|
1533 // ---------- |
|
1534 // Function: notifySessionRestoreEnabled |
|
1535 // Notify the user that session restore has been automatically enabled |
|
1536 // by showing a banner that expects no user interaction. It fades out after |
|
1537 // some seconds. |
|
1538 notifySessionRestoreEnabled: function UI_notifySessionRestoreEnabled() { |
|
1539 let brandBundle = gWindow.document.getElementById("bundle_brand"); |
|
1540 let brandShortName = brandBundle.getString("brandShortName"); |
|
1541 let notificationText = tabviewBundle.formatStringFromName( |
|
1542 "tabview.notification.sessionStore", [brandShortName], 1); |
|
1543 |
|
1544 let banner = iQ("<div>") |
|
1545 .text(notificationText) |
|
1546 .addClass("banner") |
|
1547 .appendTo("body"); |
|
1548 |
|
1549 let onFadeOut = function () { |
|
1550 banner.remove(); |
|
1551 }; |
|
1552 |
|
1553 let onFadeIn = function () { |
|
1554 setTimeout(function () { |
|
1555 banner.animate({opacity: 0}, {duration: 1500, complete: onFadeOut}); |
|
1556 }, 5000); |
|
1557 }; |
|
1558 |
|
1559 banner.animate({opacity: 0.7}, {duration: 1500, complete: onFadeIn}); |
|
1560 } |
|
1561 }; |
|
1562 |
|
1563 // ---------- |
|
1564 UI.init(); |