michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // ********** michael@0: // Title: ui.js michael@0: michael@0: let Keys = { meta: false }; michael@0: michael@0: // ########## michael@0: // Class: UI michael@0: // Singleton top-level UI manager. michael@0: let UI = { michael@0: // Variable: _frameInitialized michael@0: // True if the Tab View UI frame has been initialized. michael@0: _frameInitialized: false, michael@0: michael@0: // Variable: _pageBounds michael@0: // Stores the page bounds. michael@0: _pageBounds: null, michael@0: michael@0: // Variable: _closedLastVisibleTab michael@0: // If true, the last visible tab has just been closed in the tab strip. michael@0: _closedLastVisibleTab: false, michael@0: michael@0: // Variable: _closedSelectedTabInTabView michael@0: // If true, a select tab has just been closed in TabView. michael@0: _closedSelectedTabInTabView: false, michael@0: michael@0: // Variable: restoredClosedTab michael@0: // If true, a closed tab has just been restored. michael@0: restoredClosedTab: false, michael@0: michael@0: // Variable: _isChangingVisibility michael@0: // Tracks whether we're currently in the process of showing/hiding the tabview. michael@0: _isChangingVisibility: false, michael@0: michael@0: // Variable: _reorderTabItemsOnShow michael@0: // Keeps track of the s which their tab items' tabs have been moved michael@0: // and re-orders the tab items when switching to TabView. michael@0: _reorderTabItemsOnShow: [], michael@0: michael@0: // Variable: _reorderTabsOnHide michael@0: // Keeps track of the s which their tab items have been moved in michael@0: // TabView UI and re-orders the tabs when switcing back to main browser. michael@0: _reorderTabsOnHide: [], michael@0: michael@0: // Variable: _currentTab michael@0: // Keeps track of which xul:tab we are currently on. michael@0: // Used to facilitate zooming down from a previous tab. michael@0: _currentTab: null, michael@0: michael@0: // Variable: _eventListeners michael@0: // Keeps track of event listeners added to the AllTabs object. michael@0: _eventListeners: {}, michael@0: michael@0: // Variable: _cleanupFunctions michael@0: // An array of functions to be called at uninit time michael@0: _cleanupFunctions: [], michael@0: michael@0: // Constant: _maxInteractiveWait michael@0: // If the UI is in the middle of an operation, this is the max amount of michael@0: // milliseconds to wait between input events before we no longer consider michael@0: // the operation interactive. michael@0: _maxInteractiveWait: 250, michael@0: michael@0: // Variable: _storageBusy michael@0: // Tells whether the storage is currently busy or not. michael@0: _storageBusy: false, michael@0: michael@0: // Variable: isDOMWindowClosing michael@0: // Tells wether the parent window is about to close michael@0: isDOMWindowClosing: false, michael@0: michael@0: // Variable: _browserKeys michael@0: // Used to keep track of allowed browser keys. michael@0: _browserKeys: null, michael@0: michael@0: // Variable: _browserKeysWithShift michael@0: // Used to keep track of allowed browser keys with Shift key combination. michael@0: _browserKeysWithShift: null, michael@0: michael@0: // Variable: ignoreKeypressForSearch michael@0: // Used to prevent keypress being handled after quitting search mode. michael@0: ignoreKeypressForSearch: false, michael@0: michael@0: // Variable: _lastOpenedTab michael@0: // Used to keep track of the last opened tab. michael@0: _lastOpenedTab: null, michael@0: michael@0: // Variable: _originalSmoothScroll michael@0: // Used to keep track of the tab strip smooth scroll value. michael@0: _originalSmoothScroll: null, michael@0: michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [UI] for debug use michael@0: toString: function UI_toString() { michael@0: return "[UI]"; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: init michael@0: // Must be called after the object is created. michael@0: init: function UI_init() { michael@0: try { michael@0: let self = this; michael@0: michael@0: // initialize the direction of the page michael@0: this._initPageDirection(); michael@0: michael@0: // ___ storage michael@0: Storage.init(); michael@0: michael@0: if (Storage.readWindowBusyState(gWindow)) michael@0: this.storageBusy(); michael@0: michael@0: let data = Storage.readUIData(gWindow); michael@0: this._storageSanity(data); michael@0: this._pageBounds = data.pageBounds; michael@0: michael@0: // ___ search michael@0: Search.init(); michael@0: michael@0: Telemetry.init(); michael@0: michael@0: // ___ currentTab michael@0: this._currentTab = gBrowser.selectedTab; michael@0: michael@0: // ___ exit button michael@0: iQ("#exit-button").click(function() { michael@0: self.exit(); michael@0: self.blurAll(); michael@0: }) michael@0: .attr("title", tabviewString("button.exitTabGroups")); michael@0: michael@0: // When you click on the background/empty part of TabView, michael@0: // we create a new groupItem. michael@0: iQ(gTabViewFrame.contentDocument).mousedown(function(e) { michael@0: if (iQ(":focus").length > 0) { michael@0: iQ(":focus").each(function(element) { michael@0: // don't fire blur event if the same input element is clicked. michael@0: if (e.target != element && element.nodeName == "INPUT") michael@0: element.blur(); michael@0: }); michael@0: } michael@0: if (e.originalTarget.id == "content" && michael@0: Utils.isLeftClick(e) && michael@0: e.detail == 1) { michael@0: self._createGroupItemOnDrag(e); michael@0: } michael@0: }); michael@0: michael@0: iQ(gTabViewFrame.contentDocument).dblclick(function(e) { michael@0: if (e.originalTarget.id != "content") michael@0: return; michael@0: michael@0: // Create a group with one tab on double click michael@0: let box = michael@0: new Rect(e.clientX - Math.floor(TabItems.tabWidth/2), michael@0: e.clientY - Math.floor(TabItems.tabHeight/2), michael@0: TabItems.tabWidth, TabItems.tabHeight); michael@0: box.inset(-30, -30); michael@0: michael@0: let opts = {immediately: true, bounds: box}; michael@0: let groupItem = new GroupItem([], opts); michael@0: groupItem.newTab(); michael@0: michael@0: gTabView.firstUseExperienced = true; michael@0: }); michael@0: michael@0: iQ(window).bind("unload", function() { michael@0: self.uninit(); michael@0: }); michael@0: michael@0: // ___ setup DOMWillOpenModalDialog message handler michael@0: let mm = gWindow.messageManager; michael@0: let callback = this._onDOMWillOpenModalDialog.bind(this); michael@0: mm.addMessageListener("Panorama:DOMWillOpenModalDialog", callback); michael@0: michael@0: this._cleanupFunctions.push(function () { michael@0: mm.removeMessageListener("Panorama:DOMWillOpenModalDialog", callback); michael@0: }); michael@0: michael@0: // ___ setup key handlers michael@0: this._setTabViewFrameKeyHandlers(); michael@0: michael@0: // ___ add tab action handlers michael@0: this._addTabActionHandlers(); michael@0: michael@0: // ___ groups michael@0: GroupItems.init(); michael@0: GroupItems.pauseArrange(); michael@0: let hasGroupItemsData = GroupItems.load(); michael@0: michael@0: // ___ tabs michael@0: TabItems.init(); michael@0: TabItems.pausePainting(); michael@0: michael@0: // ___ favicons michael@0: FavIcons.init(); michael@0: michael@0: if (!hasGroupItemsData) michael@0: this.reset(); michael@0: michael@0: // ___ resizing michael@0: if (this._pageBounds) michael@0: this._resize(true); michael@0: else michael@0: this._pageBounds = Items.getPageBounds(); michael@0: michael@0: iQ(window).resize(function() { michael@0: self._resize(); michael@0: }); michael@0: michael@0: // ___ setup event listener to save canvas images michael@0: let onWindowClosing = function () { michael@0: gWindow.removeEventListener("SSWindowClosing", onWindowClosing, false); michael@0: michael@0: // XXX bug #635975 - don't unlink the tab if the dom window is closing. michael@0: self.isDOMWindowClosing = true; michael@0: michael@0: if (self.isTabViewVisible()) michael@0: GroupItems.removeHiddenGroups(); michael@0: michael@0: TabItems.saveAll(); michael@0: michael@0: self._save(); michael@0: }; michael@0: michael@0: gWindow.addEventListener("SSWindowClosing", onWindowClosing); michael@0: this._cleanupFunctions.push(function () { michael@0: gWindow.removeEventListener("SSWindowClosing", onWindowClosing); michael@0: }); michael@0: michael@0: // ___ load frame script michael@0: let frameScript = "chrome://browser/content/tabview-content.js"; michael@0: gWindow.messageManager.loadFrameScript(frameScript, true); michael@0: michael@0: // ___ Done michael@0: this._frameInitialized = true; michael@0: this._save(); michael@0: michael@0: // fire an iframe initialized event so everyone knows tab view is michael@0: // initialized. michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("tabviewframeinitialized", true, false); michael@0: dispatchEvent(event); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: } finally { michael@0: GroupItems.resumeArrange(); michael@0: } michael@0: }, michael@0: michael@0: // Function: uninit michael@0: // Should be called when window is unloaded. michael@0: uninit: function UI_uninit() { michael@0: // call our cleanup functions michael@0: this._cleanupFunctions.forEach(function(func) { michael@0: func(); michael@0: }); michael@0: this._cleanupFunctions = []; michael@0: michael@0: // additional clean up michael@0: TabItems.uninit(); michael@0: GroupItems.uninit(); michael@0: FavIcons.uninit(); michael@0: Storage.uninit(); michael@0: Telemetry.uninit(); michael@0: michael@0: this._removeTabActionHandlers(); michael@0: this._currentTab = null; michael@0: this._pageBounds = null; michael@0: this._reorderTabItemsOnShow = null; michael@0: this._reorderTabsOnHide = null; michael@0: this._frameInitialized = false; michael@0: }, michael@0: michael@0: // Property: rtl michael@0: // Returns true if we are in RTL mode, false otherwise michael@0: rtl: false, michael@0: michael@0: // Function: reset michael@0: // Resets the Panorama view to have just one group with all tabs michael@0: reset: function UI_reset() { michael@0: let padding = Trenches.defaultRadius; michael@0: let welcomeWidth = 300; michael@0: let pageBounds = Items.getPageBounds(); michael@0: pageBounds.inset(padding, padding); michael@0: michael@0: let $actions = iQ("#actions"); michael@0: if ($actions) { michael@0: pageBounds.width -= $actions.width(); michael@0: if (UI.rtl) michael@0: pageBounds.left += $actions.width() - padding; michael@0: } michael@0: michael@0: // ___ make a fresh groupItem michael@0: let box = new Rect(pageBounds); michael@0: box.width = Math.min(box.width * 0.667, michael@0: pageBounds.width - (welcomeWidth + padding)); michael@0: box.height = box.height * 0.667; michael@0: if (UI.rtl) { michael@0: box.left = pageBounds.left + welcomeWidth + 2 * padding; michael@0: } michael@0: michael@0: GroupItems.groupItems.forEach(function(group) { michael@0: group.close(); michael@0: }); michael@0: michael@0: let options = { michael@0: bounds: box, michael@0: immediately: true michael@0: }; michael@0: let groupItem = new GroupItem([], options); michael@0: let items = TabItems.getItems(); michael@0: items.forEach(function(item) { michael@0: if (item.parent) michael@0: item.parent.remove(item); michael@0: groupItem.add(item, {immediately: true}); michael@0: }); michael@0: this.setActive(groupItem); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: blurAll michael@0: // Blurs any currently focused element michael@0: blurAll: function UI_blurAll() { michael@0: iQ(":focus").each(function(element) { michael@0: element.blur(); michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isIdle michael@0: // Returns true if the last interaction was long enough ago to consider the michael@0: // UI idle. Used to determine whether interactivity would be sacrificed if michael@0: // the CPU was to become busy. michael@0: // michael@0: isIdle: function UI_isIdle() { michael@0: let time = Date.now(); michael@0: let maxEvent = Math.max(drag.lastMoveTime, resize.lastMoveTime); michael@0: return (time - maxEvent) > this._maxInteractiveWait; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getActiveTab michael@0: // Returns the currently active tab as a michael@0: getActiveTab: function UI_getActiveTab() { michael@0: return this._activeTab; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _setActiveTab michael@0: // Sets the currently active tab. The idea of a focused tab is useful michael@0: // for keyboard navigation and returning to the last zoomed-in tab. michael@0: // Hitting return/esc brings you to the focused tab, and using the michael@0: // arrow keys lets you navigate between open tabs. michael@0: // michael@0: // Parameters: michael@0: // - Takes a michael@0: _setActiveTab: function UI__setActiveTab(tabItem) { michael@0: if (tabItem == this._activeTab) michael@0: return; michael@0: michael@0: if (this._activeTab) { michael@0: this._activeTab.makeDeactive(); michael@0: this._activeTab.removeSubscriber("close", this._onActiveTabClosed); michael@0: } michael@0: michael@0: this._activeTab = tabItem; michael@0: michael@0: if (this._activeTab) { michael@0: this._activeTab.addSubscriber("close", this._onActiveTabClosed); michael@0: this._activeTab.makeActive(); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _onActiveTabClosed michael@0: // Handles when the currently active tab gets closed. michael@0: // michael@0: // Parameters: michael@0: // - the that is closed michael@0: _onActiveTabClosed: function UI__onActiveTabClosed(tabItem){ michael@0: if (UI._activeTab == tabItem) michael@0: UI._setActiveTab(null); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setActive michael@0: // Sets the active tab item or group item michael@0: // Parameters: michael@0: // michael@0: // options michael@0: // dontSetActiveTabInGroup bool for not setting active tab in group michael@0: setActive: function UI_setActive(item, options) { michael@0: Utils.assert(item, "item must be given"); michael@0: michael@0: if (item.isATabItem) { michael@0: if (item.parent) michael@0: GroupItems.setActiveGroupItem(item.parent); michael@0: if (!options || !options.dontSetActiveTabInGroup) michael@0: this._setActiveTab(item); michael@0: } else { michael@0: GroupItems.setActiveGroupItem(item); michael@0: if (!options || !options.dontSetActiveTabInGroup) { michael@0: let activeTab = item.getActiveTab(); michael@0: if (activeTab) michael@0: this._setActiveTab(activeTab); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: clearActiveTab michael@0: // Sets the active tab to 'null'. michael@0: clearActiveTab: function UI_clearActiveTab() { michael@0: this._setActiveTab(null); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isTabViewVisible michael@0: // Returns true if the TabView UI is currently shown. michael@0: isTabViewVisible: function UI_isTabViewVisible() { michael@0: return gTabViewDeck.selectedPanel == gTabViewFrame; michael@0: }, michael@0: michael@0: // --------- michael@0: // Function: _initPageDirection michael@0: // Initializes the page base direction michael@0: _initPageDirection: function UI__initPageDirection() { michael@0: let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIXULChromeRegistry); michael@0: let dir = chromeReg.isLocaleRTL("global"); michael@0: document.documentElement.setAttribute("dir", dir ? "rtl" : "ltr"); michael@0: this.rtl = dir; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: showTabView michael@0: // Shows TabView and hides the main browser UI. michael@0: // Parameters: michael@0: // zoomOut - true for zoom out animation, false for nothing. michael@0: showTabView: function UI_showTabView(zoomOut) { michael@0: if (this.isTabViewVisible() || this._isChangingVisibility) michael@0: return; michael@0: michael@0: this._isChangingVisibility = true; michael@0: michael@0: // store tab strip smooth scroll value and disable it. michael@0: let tabStrip = gBrowser.tabContainer.mTabstrip; michael@0: this._originalSmoothScroll = tabStrip.smoothScroll; michael@0: tabStrip.smoothScroll = false; michael@0: michael@0: // initialize the direction of the page michael@0: this._initPageDirection(); michael@0: michael@0: var self = this; michael@0: var currentTab = this._currentTab; michael@0: michael@0: this._reorderTabItemsOnShow.forEach(function(groupItem) { michael@0: groupItem.reorderTabItemsBasedOnTabOrder(); michael@0: }); michael@0: this._reorderTabItemsOnShow = []; michael@0: michael@0: #ifdef XP_WIN michael@0: // Restore the full height when showing TabView michael@0: gTabViewFrame.style.marginTop = ""; michael@0: #endif michael@0: gTabViewDeck.selectedPanel = gTabViewFrame; michael@0: gWindow.TabsInTitlebar.allowedBy("tabview-open", false); michael@0: gTabViewFrame.contentWindow.focus(); michael@0: michael@0: gBrowser.updateTitlebar(); michael@0: #ifdef XP_MACOSX michael@0: this.setTitlebarColors(true); michael@0: #endif michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("tabviewshown", true, false); michael@0: michael@0: Storage.saveVisibilityData(gWindow, "true"); michael@0: michael@0: if (zoomOut && currentTab && currentTab._tabViewTabItem) { michael@0: let item = currentTab._tabViewTabItem; michael@0: // If there was a previous currentTab we want to animate michael@0: // its thumbnail (canvas) for the zoom out. michael@0: // Note that we start the animation on the chrome thread. michael@0: michael@0: // Zoom out! michael@0: item.zoomOut(function() { michael@0: if (!currentTab._tabViewTabItem) // if the tab's been destroyed michael@0: item = null; michael@0: michael@0: self.setActive(item); michael@0: michael@0: self._resize(true); michael@0: self._isChangingVisibility = false; michael@0: dispatchEvent(event); michael@0: michael@0: // Flush pending updates michael@0: GroupItems.flushAppTabUpdates(); michael@0: michael@0: TabItems.resumePainting(); michael@0: }); michael@0: } else { michael@0: if (!currentTab || !currentTab._tabViewTabItem) michael@0: self.clearActiveTab(); michael@0: self._isChangingVisibility = false; michael@0: dispatchEvent(event); michael@0: michael@0: // Flush pending updates michael@0: GroupItems.flushAppTabUpdates(); michael@0: michael@0: TabItems.resumePainting(); michael@0: } michael@0: michael@0: if (gTabView.firstUseExperienced) michael@0: gTabView.enableSessionRestore(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: hideTabView michael@0: // Hides TabView and shows the main browser UI. michael@0: hideTabView: function UI_hideTabView() { michael@0: if (!this.isTabViewVisible() || this._isChangingVisibility) michael@0: return; michael@0: michael@0: // another tab might be select if user decides to stay on a page when michael@0: // a onclose confirmation prompts. michael@0: GroupItems.removeHiddenGroups(); michael@0: michael@0: // We need to set this after removing the hidden groups because doing so michael@0: // might show prompts which will cause us to be called again, and we'd get michael@0: // stuck if we prevent re-entrancy before doing that. michael@0: this._isChangingVisibility = true; michael@0: michael@0: TabItems.pausePainting(); michael@0: michael@0: this._reorderTabsOnHide.forEach(function(groupItem) { michael@0: groupItem.reorderTabsBasedOnTabItemOrder(); michael@0: }); michael@0: this._reorderTabsOnHide = []; michael@0: michael@0: #ifdef XP_WIN michael@0: // Push the top of TabView frame to behind the tabbrowser, so glass can show michael@0: // XXX bug 586679: avoid shrinking the iframe and squishing iframe contents michael@0: // as well as avoiding the flash of black as we animate out michael@0: gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px"; michael@0: #endif michael@0: gTabViewDeck.selectedPanel = gBrowserPanel; michael@0: gWindow.TabsInTitlebar.allowedBy("tabview-open", true); michael@0: gBrowser.selectedBrowser.focus(); michael@0: michael@0: gBrowser.updateTitlebar(); michael@0: gBrowser.tabContainer.mTabstrip.smoothScroll = this._originalSmoothScroll; michael@0: #ifdef XP_MACOSX michael@0: this.setTitlebarColors(false); michael@0: #endif michael@0: Storage.saveVisibilityData(gWindow, "false"); michael@0: michael@0: this._isChangingVisibility = false; michael@0: michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("tabviewhidden", true, false); michael@0: dispatchEvent(event); michael@0: }, michael@0: michael@0: #ifdef XP_MACOSX michael@0: // ---------- michael@0: // Function: setTitlebarColors michael@0: // Used on the Mac to make the title bar match the gradient in the rest of the michael@0: // TabView UI. michael@0: // michael@0: // Parameters: michael@0: // colors - (bool or object) true for the special TabView color, false for michael@0: // the normal color, and an object with "active" and "inactive" michael@0: // properties to specify directly. michael@0: setTitlebarColors: function UI_setTitlebarColors(colors) { michael@0: // Mac Only michael@0: var mainWindow = gWindow.document.getElementById("main-window"); michael@0: if (colors === true) { michael@0: mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4"); michael@0: mainWindow.setAttribute("inactivetitlebarcolor", "#EDEDED"); michael@0: } else if (colors && "active" in colors && "inactive" in colors) { michael@0: mainWindow.setAttribute("activetitlebarcolor", colors.active); michael@0: mainWindow.setAttribute("inactivetitlebarcolor", colors.inactive); michael@0: } else { michael@0: mainWindow.removeAttribute("activetitlebarcolor"); michael@0: mainWindow.removeAttribute("inactivetitlebarcolor"); michael@0: } michael@0: }, michael@0: #endif michael@0: michael@0: // ---------- michael@0: // Function: storageBusy michael@0: // Pauses the storage activity that conflicts with sessionstore updates. michael@0: // Calls can be nested. michael@0: storageBusy: function UI_storageBusy() { michael@0: if (this._storageBusy) michael@0: return; michael@0: michael@0: this._storageBusy = true; michael@0: michael@0: TabItems.pauseReconnecting(); michael@0: GroupItems.pauseAutoclose(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: storageReady michael@0: // Resumes the activity paused by storageBusy, and updates for any new group michael@0: // information in sessionstore. Calls can be nested. michael@0: storageReady: function UI_storageReady() { michael@0: if (!this._storageBusy) michael@0: return; michael@0: michael@0: this._storageBusy = false; michael@0: michael@0: let hasGroupItemsData = GroupItems.load(); michael@0: if (!hasGroupItemsData) michael@0: this.reset(); michael@0: michael@0: TabItems.resumeReconnecting(); michael@0: GroupItems._updateTabBar(); michael@0: GroupItems.resumeAutoclose(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _addTabActionHandlers michael@0: // Adds handlers to handle tab actions. michael@0: _addTabActionHandlers: function UI__addTabActionHandlers() { michael@0: var self = this; michael@0: michael@0: // session restore events michael@0: function handleSSWindowStateBusy() { michael@0: self.storageBusy(); michael@0: } michael@0: michael@0: function handleSSWindowStateReady() { michael@0: self.storageReady(); michael@0: } michael@0: michael@0: gWindow.addEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false); michael@0: gWindow.addEventListener("SSWindowStateReady", handleSSWindowStateReady, false); michael@0: michael@0: this._cleanupFunctions.push(function() { michael@0: gWindow.removeEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false); michael@0: gWindow.removeEventListener("SSWindowStateReady", handleSSWindowStateReady, false); michael@0: }); michael@0: michael@0: // TabOpen michael@0: this._eventListeners.open = function (event) { michael@0: let tab = event.target; michael@0: michael@0: // if it's an app tab, add it to all the group items michael@0: if (tab.pinned) michael@0: GroupItems.addAppTab(tab); michael@0: else if (self.isTabViewVisible() && !self._storageBusyCount) michael@0: self._lastOpenedTab = tab; michael@0: }; michael@0: michael@0: // TabClose michael@0: this._eventListeners.close = function (event) { michael@0: let tab = event.target; michael@0: michael@0: // if it's an app tab, remove it from all the group items michael@0: if (tab.pinned) michael@0: GroupItems.removeAppTab(tab); michael@0: michael@0: if (self.isTabViewVisible()) { michael@0: // just closed the selected tab in the TabView interface. michael@0: if (self._currentTab == tab) michael@0: self._closedSelectedTabInTabView = true; michael@0: } else { michael@0: // If we're currently in the process of session store update, michael@0: // we don't want to go to the Tab View UI. michael@0: if (self._storageBusy) michael@0: return; michael@0: michael@0: // if not closing the last tab michael@0: if (gBrowser.tabs.length > 1) { michael@0: // Don't return to TabView if there are any app tabs michael@0: for (let a = 0; a < gBrowser._numPinnedTabs; a++) { michael@0: if (Utils.isValidXULTab(gBrowser.tabs[a])) michael@0: return; michael@0: } michael@0: michael@0: let groupItem = GroupItems.getActiveGroupItem(); michael@0: michael@0: // 1) Only go back to the TabView tab when there you close the last michael@0: // tab of a groupItem. michael@0: let closingLastOfGroup = (groupItem && michael@0: groupItem._children.length == 1 && michael@0: groupItem._children[0].tab == tab); michael@0: michael@0: // 2) When a blank tab is active while restoring a closed tab the michael@0: // blank tab gets removed. The active group is not closed as this is michael@0: // where the restored tab goes. So do not show the TabView. michael@0: let tabItem = tab && tab._tabViewTabItem; michael@0: let closingBlankTabAfterRestore = michael@0: (tabItem && tabItem.isRemovedAfterRestore); michael@0: michael@0: if (closingLastOfGroup && !closingBlankTabAfterRestore) { michael@0: // for the tab focus event to pick up. michael@0: self._closedLastVisibleTab = true; michael@0: self.showTabView(); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // TabMove michael@0: this._eventListeners.move = function (event) { michael@0: let tab = event.target; michael@0: michael@0: if (GroupItems.groupItems.length > 0) { michael@0: if (tab.pinned) { michael@0: if (gBrowser._numPinnedTabs > 1) michael@0: GroupItems.arrangeAppTab(tab); michael@0: } else { michael@0: let activeGroupItem = GroupItems.getActiveGroupItem(); michael@0: if (activeGroupItem) michael@0: self.setReorderTabItemsOnShow(activeGroupItem); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // TabSelect michael@0: this._eventListeners.select = function (event) { michael@0: self.onTabSelect(event.target); michael@0: }; michael@0: michael@0: // TabPinned michael@0: this._eventListeners.pinned = function (event) { michael@0: let tab = event.target; michael@0: michael@0: TabItems.handleTabPin(tab); michael@0: GroupItems.addAppTab(tab); michael@0: }; michael@0: michael@0: // TabUnpinned michael@0: this._eventListeners.unpinned = function (event) { michael@0: let tab = event.target; michael@0: michael@0: TabItems.handleTabUnpin(tab); michael@0: GroupItems.removeAppTab(tab); michael@0: michael@0: let groupItem = tab._tabViewTabItem.parent; michael@0: if (groupItem) michael@0: self.setReorderTabItemsOnShow(groupItem); michael@0: }; michael@0: michael@0: // Actually register the above handlers michael@0: for (let name in this._eventListeners) michael@0: AllTabs.register(name, this._eventListeners[name]); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _removeTabActionHandlers michael@0: // Removes handlers to handle tab actions. michael@0: _removeTabActionHandlers: function UI__removeTabActionHandlers() { michael@0: for (let name in this._eventListeners) michael@0: AllTabs.unregister(name, this._eventListeners[name]); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: goToTab michael@0: // Selects the given xul:tab in the browser. michael@0: goToTab: function UI_goToTab(xulTab) { michael@0: // If it's not focused, the onFocus listener would handle it. michael@0: if (xulTab.selected) michael@0: this.onTabSelect(xulTab); michael@0: else michael@0: gBrowser.selectedTab = xulTab; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: onTabSelect michael@0: // Called when the user switches from one tab to another outside of the TabView UI. michael@0: onTabSelect: function UI_onTabSelect(tab) { michael@0: this._currentTab = tab; michael@0: michael@0: if (this.isTabViewVisible()) { michael@0: // We want to zoom in if: michael@0: // 1) we didn't just restore a tab via Ctrl+Shift+T michael@0: // 2) the currently selected tab is the last created tab and has a tabItem michael@0: if (!this.restoredClosedTab && michael@0: this._lastOpenedTab == tab && tab._tabViewTabItem) { michael@0: tab._tabViewTabItem.zoomIn(true); michael@0: this._lastOpenedTab = null; michael@0: return; michael@0: } michael@0: if (this._closedLastVisibleTab || michael@0: (this._closedSelectedTabInTabView && !this.closedLastTabInTabView) || michael@0: this.restoredClosedTab) { michael@0: if (this.restoredClosedTab) { michael@0: // when the tab view UI is being displayed, update the thumb for the michael@0: // restored closed tab after the page load michael@0: tab.linkedBrowser.addEventListener("load", function onLoad(event) { michael@0: tab.linkedBrowser.removeEventListener("load", onLoad, true); michael@0: TabItems._update(tab); michael@0: }, true); michael@0: } michael@0: this._closedLastVisibleTab = false; michael@0: this._closedSelectedTabInTabView = false; michael@0: this.closedLastTabInTabView = false; michael@0: this.restoredClosedTab = false; michael@0: return; michael@0: } michael@0: } michael@0: // reset these vars, just in case. michael@0: this._closedLastVisibleTab = false; michael@0: this._closedSelectedTabInTabView = false; michael@0: this.closedLastTabInTabView = false; michael@0: this.restoredClosedTab = false; michael@0: this._lastOpenedTab = null; michael@0: michael@0: // if TabView is visible but we didn't just close the last tab or michael@0: // selected tab, show chrome. michael@0: if (this.isTabViewVisible()) { michael@0: // Unhide the group of the tab the user is activating. michael@0: if (tab && tab._tabViewTabItem && tab._tabViewTabItem.parent && michael@0: tab._tabViewTabItem.parent.hidden) michael@0: tab._tabViewTabItem.parent._unhide({immediately: true}); michael@0: michael@0: this.hideTabView(); michael@0: } michael@0: michael@0: // another tab might be selected when hideTabView() is invoked so a michael@0: // validation is needed. michael@0: if (this._currentTab != tab) michael@0: return; michael@0: michael@0: let newItem = null; michael@0: // update the tab bar for the new tab's group michael@0: if (tab && tab._tabViewTabItem) { michael@0: if (!TabItems.reconnectingPaused()) { michael@0: newItem = tab._tabViewTabItem; michael@0: GroupItems.updateActiveGroupItemAndTabBar(newItem); michael@0: } michael@0: } else { michael@0: // No tabItem; must be an app tab. Base the tab bar on the current group. michael@0: // If no current group, figure it out based on what's already in the tab michael@0: // bar. michael@0: if (!GroupItems.getActiveGroupItem()) { michael@0: for (let a = 0; a < gBrowser.tabs.length; a++) { michael@0: let theTab = gBrowser.tabs[a]; michael@0: if (!theTab.pinned) { michael@0: let tabItem = theTab._tabViewTabItem; michael@0: this.setActive(tabItem.parent); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (GroupItems.getActiveGroupItem()) michael@0: GroupItems._updateTabBar(); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _onDOMWillOpenModalDialog michael@0: // Called when a web page is about to show a modal dialog. michael@0: _onDOMWillOpenModalDialog: function UI__onDOMWillOpenModalDialog(cx) { michael@0: if (!this.isTabViewVisible()) michael@0: return; michael@0: michael@0: let index = gBrowser.browsers.indexOf(cx.target); michael@0: if (index == -1) michael@0: return; michael@0: michael@0: let tab = gBrowser.tabs[index]; michael@0: michael@0: // When TabView is visible, we need to call onTabSelect to make sure that michael@0: // TabView is hidden and that the correct group is activated. When a modal michael@0: // dialog is shown for currently selected tab the onTabSelect event handler michael@0: // is not called, so we need to do it. michael@0: if (tab.selected && this._currentTab == tab) michael@0: this.onTabSelect(tab); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setReorderTabsOnHide michael@0: // Sets the groupItem which the tab items' tabs should be re-ordered when michael@0: // switching to the main browser UI. michael@0: // Parameters: michael@0: // groupItem - the groupItem which would be used for re-ordering tabs. michael@0: setReorderTabsOnHide: function UI_setReorderTabsOnHide(groupItem) { michael@0: if (this.isTabViewVisible()) { michael@0: var index = this._reorderTabsOnHide.indexOf(groupItem); michael@0: if (index == -1) michael@0: this._reorderTabsOnHide.push(groupItem); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: setReorderTabItemsOnShow michael@0: // Sets the groupItem which the tab items should be re-ordered when michael@0: // switching to the tab view UI. michael@0: // Parameters: michael@0: // groupItem - the groupItem which would be used for re-ordering tab items. michael@0: setReorderTabItemsOnShow: function UI_setReorderTabItemsOnShow(groupItem) { michael@0: if (!this.isTabViewVisible()) { michael@0: var index = this._reorderTabItemsOnShow.indexOf(groupItem); michael@0: if (index == -1) michael@0: this._reorderTabItemsOnShow.push(groupItem); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: updateTabButton: function UI_updateTabButton() { michael@0: let exitButton = document.getElementById("exit-button"); michael@0: let numberOfGroups = GroupItems.groupItems.length; michael@0: michael@0: exitButton.setAttribute("groups", numberOfGroups); michael@0: gTabView.updateGroupNumberBroadcaster(numberOfGroups); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: getClosestTab michael@0: // Convenience function to get the next tab closest to the entered position michael@0: getClosestTab: function UI_getClosestTab(tabCenter) { michael@0: let cl = null; michael@0: let clDist; michael@0: TabItems.getItems().forEach(function (item) { michael@0: if (!item.parent || item.parent.hidden) michael@0: return; michael@0: let testDist = tabCenter.distance(item.bounds.center()); michael@0: if (cl==null || testDist < clDist) { michael@0: cl = item; michael@0: clDist = testDist; michael@0: } michael@0: }); michael@0: return cl; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _setupBrowserKeys michael@0: // Sets up the allowed browser keys using key elements. michael@0: _setupBrowserKeys: function UI__setupKeyWhiteList() { michael@0: let keys = {}; michael@0: michael@0: [ michael@0: #ifdef XP_UNIX michael@0: "quitApplication", michael@0: #else michael@0: "redo", michael@0: #endif michael@0: #ifdef XP_MACOSX michael@0: "preferencesCmdMac", "minimizeWindow", "hideThisAppCmdMac", michael@0: #endif michael@0: "newNavigator", "newNavigatorTab", "undo", "cut", "copy", "paste", michael@0: "selectAll", "find" michael@0: ].forEach(function(key) { michael@0: let element = gWindow.document.getElementById("key_" + key); michael@0: let code = element.getAttribute("key").toLocaleLowerCase().charCodeAt(0); michael@0: keys[code] = key; michael@0: }); michael@0: this._browserKeys = keys; michael@0: michael@0: keys = {}; michael@0: // The lower case letters are passed to processBrowserKeys() even with shift michael@0: // key when stimulating a key press using EventUtils.synthesizeKey() so need michael@0: // to handle both upper and lower cases here. michael@0: [ michael@0: #ifdef XP_UNIX michael@0: "redo", michael@0: #endif michael@0: #ifdef XP_MACOSX michael@0: "fullScreen", michael@0: #endif michael@0: "closeWindow", "tabview", "undoCloseTab", "undoCloseWindow" michael@0: ].forEach(function(key) { michael@0: let element = gWindow.document.getElementById("key_" + key); michael@0: let code = element.getAttribute("key").toLocaleLowerCase().charCodeAt(0); michael@0: keys[code] = key; michael@0: }); michael@0: this._browserKeysWithShift = keys; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _setTabViewFrameKeyHandlers michael@0: // Sets up the key handlers for navigating between tabs within the TabView UI. michael@0: _setTabViewFrameKeyHandlers: function UI__setTabViewFrameKeyHandlers() { michael@0: let self = this; michael@0: michael@0: this._setupBrowserKeys(); michael@0: michael@0: iQ(window).keyup(function(event) { michael@0: if (!event.metaKey) michael@0: Keys.meta = false; michael@0: }); michael@0: michael@0: iQ(window).keypress(function(event) { michael@0: if (event.metaKey) michael@0: Keys.meta = true; michael@0: michael@0: function processBrowserKeys(evt) { michael@0: // let any keys with alt to pass through michael@0: if (evt.altKey) michael@0: return; michael@0: michael@0: #ifdef XP_MACOSX michael@0: if (evt.metaKey) { michael@0: #else michael@0: if (evt.ctrlKey) { michael@0: #endif michael@0: let preventDefault = true; michael@0: if (evt.shiftKey) { michael@0: // when a user presses ctrl+shift+key, upper case letter charCode michael@0: // is passed to processBrowserKeys() so converting back to lower michael@0: // case charCode before doing the check michael@0: let lowercaseCharCode = michael@0: String.fromCharCode(evt.charCode).toLocaleLowerCase().charCodeAt(0); michael@0: if (lowercaseCharCode in self._browserKeysWithShift) { michael@0: let key = self._browserKeysWithShift[lowercaseCharCode]; michael@0: if (key == "tabview") michael@0: self.exit(); michael@0: else michael@0: preventDefault = false; michael@0: } michael@0: } else { michael@0: if (evt.charCode in self._browserKeys) { michael@0: let key = self._browserKeys[evt.charCode]; michael@0: if (key == "find") michael@0: self.enableSearch(); michael@0: else michael@0: preventDefault = false; michael@0: } michael@0: } michael@0: if (preventDefault) { michael@0: evt.stopPropagation(); michael@0: evt.preventDefault(); michael@0: } michael@0: } michael@0: } michael@0: if ((iQ(":focus").length > 0 && iQ(":focus")[0].nodeName == "INPUT") || michael@0: Search.isEnabled() || self.ignoreKeypressForSearch) { michael@0: self.ignoreKeypressForSearch = false; michael@0: processBrowserKeys(event); michael@0: return; michael@0: } michael@0: michael@0: function getClosestTabBy(norm) { michael@0: if (!self.getActiveTab()) michael@0: return null; michael@0: michael@0: let activeTab = self.getActiveTab(); michael@0: let activeTabGroup = activeTab.parent; michael@0: let myCenter = activeTab.bounds.center(); michael@0: let match; michael@0: michael@0: TabItems.getItems().forEach(function (item) { michael@0: if (!item.parent.hidden && michael@0: (!activeTabGroup.expanded || activeTabGroup.id == item.parent.id)) { michael@0: let itemCenter = item.bounds.center(); michael@0: michael@0: if (norm(itemCenter, myCenter)) { michael@0: let itemDist = myCenter.distance(itemCenter); michael@0: if (!match || match[0] > itemDist) michael@0: match = [itemDist, item]; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: return match && match[1]; michael@0: } michael@0: michael@0: let preventDefault = true; michael@0: let activeTab; michael@0: let activeGroupItem; michael@0: let norm = null; michael@0: switch (event.keyCode) { michael@0: case KeyEvent.DOM_VK_RIGHT: michael@0: norm = function(a, me){return a.x > me.x}; michael@0: break; michael@0: case KeyEvent.DOM_VK_LEFT: michael@0: norm = function(a, me){return a.x < me.x}; michael@0: break; michael@0: case KeyEvent.DOM_VK_DOWN: michael@0: norm = function(a, me){return a.y > me.y}; michael@0: break; michael@0: case KeyEvent.DOM_VK_UP: michael@0: norm = function(a, me){return a.y < me.y} michael@0: break; michael@0: } michael@0: michael@0: if (norm != null) { michael@0: let nextTab = getClosestTabBy(norm); michael@0: if (nextTab) { michael@0: if (nextTab.isStacked && !nextTab.parent.expanded) michael@0: nextTab = nextTab.parent.getChild(0); michael@0: self.setActive(nextTab); michael@0: } michael@0: } else { michael@0: switch(event.keyCode) { michael@0: case KeyEvent.DOM_VK_ESCAPE: michael@0: activeGroupItem = GroupItems.getActiveGroupItem(); michael@0: if (activeGroupItem && activeGroupItem.expanded) michael@0: activeGroupItem.collapse(); michael@0: else michael@0: self.exit(); michael@0: break; michael@0: case KeyEvent.DOM_VK_RETURN: michael@0: activeGroupItem = GroupItems.getActiveGroupItem(); michael@0: if (activeGroupItem) { michael@0: activeTab = self.getActiveTab(); michael@0: michael@0: if (!activeTab || activeTab.parent != activeGroupItem) michael@0: activeTab = activeGroupItem.getActiveTab(); michael@0: michael@0: if (activeTab) michael@0: activeTab.zoomIn(); michael@0: else michael@0: activeGroupItem.newTab(); michael@0: } michael@0: break; michael@0: case KeyEvent.DOM_VK_TAB: michael@0: // tab/shift + tab to go to the next tab. michael@0: activeTab = self.getActiveTab(); michael@0: if (activeTab) { michael@0: let tabItems = (activeTab.parent ? activeTab.parent.getChildren() : michael@0: [activeTab]); michael@0: let length = tabItems.length; michael@0: let currentIndex = tabItems.indexOf(activeTab); michael@0: michael@0: if (length > 1) { michael@0: let newIndex; michael@0: if (event.shiftKey) { michael@0: if (currentIndex == 0) michael@0: newIndex = (length - 1); michael@0: else michael@0: newIndex = (currentIndex - 1); michael@0: } else { michael@0: if (currentIndex == (length - 1)) michael@0: newIndex = 0; michael@0: else michael@0: newIndex = (currentIndex + 1); michael@0: } michael@0: self.setActive(tabItems[newIndex]); michael@0: } michael@0: } michael@0: break; michael@0: default: michael@0: processBrowserKeys(event); michael@0: preventDefault = false; michael@0: } michael@0: if (preventDefault) { michael@0: event.stopPropagation(); michael@0: event.preventDefault(); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: enableSearch michael@0: // Enables the search feature. michael@0: enableSearch: function UI_enableSearch() { michael@0: if (!Search.isEnabled()) { michael@0: Search.ensureShown(); michael@0: Search.switchToInMode(); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _createGroupItemOnDrag michael@0: // Called in response to a mousedown in empty space in the TabView UI; michael@0: // creates a new groupItem based on the user's drag. michael@0: _createGroupItemOnDrag: function UI__createGroupItemOnDrag(e) { michael@0: const minSize = 60; michael@0: const minMinSize = 15; michael@0: michael@0: let lastActiveGroupItem = GroupItems.getActiveGroupItem(); michael@0: michael@0: var startPos = { x: e.clientX, y: e.clientY }; michael@0: var phantom = iQ("
") michael@0: .addClass("groupItem phantom activeGroupItem dragRegion") michael@0: .css({ michael@0: position: "absolute", michael@0: zIndex: -1, michael@0: cursor: "default" michael@0: }) michael@0: .appendTo("body"); michael@0: michael@0: var item = { // a faux-Item michael@0: container: phantom, michael@0: isAFauxItem: true, michael@0: bounds: {}, michael@0: getBounds: function FauxItem_getBounds() { michael@0: return this.container.bounds(); michael@0: }, michael@0: setBounds: function FauxItem_setBounds(bounds) { michael@0: this.container.css(bounds); michael@0: }, michael@0: setZ: function FauxItem_setZ(z) { michael@0: // don't set a z-index because we want to force it to be low. michael@0: }, michael@0: setOpacity: function FauxItem_setOpacity(opacity) { michael@0: this.container.css("opacity", opacity); michael@0: }, michael@0: // we don't need to pushAway the phantom item at the end, because michael@0: // when we create a new GroupItem, it'll do the actual pushAway. michael@0: pushAway: function () {}, michael@0: }; michael@0: item.setBounds(new Rect(startPos.y, startPos.x, 0, 0)); michael@0: michael@0: var dragOutInfo = new Drag(item, e); michael@0: michael@0: function updateSize(e) { michael@0: var box = new Rect(); michael@0: box.left = Math.min(startPos.x, e.clientX); michael@0: box.right = Math.max(startPos.x, e.clientX); michael@0: box.top = Math.min(startPos.y, e.clientY); michael@0: box.bottom = Math.max(startPos.y, e.clientY); michael@0: item.setBounds(box); michael@0: michael@0: // compute the stationaryCorner michael@0: var stationaryCorner = ""; michael@0: michael@0: if (startPos.y == box.top) michael@0: stationaryCorner += "top"; michael@0: else michael@0: stationaryCorner += "bottom"; michael@0: michael@0: if (startPos.x == box.left) michael@0: stationaryCorner += "left"; michael@0: else michael@0: stationaryCorner += "right"; michael@0: michael@0: dragOutInfo.snap(stationaryCorner, false, false); // null for ui, which we don't use anyway. michael@0: michael@0: box = item.getBounds(); michael@0: if (box.width > minMinSize && box.height > minMinSize && michael@0: (box.width > minSize || box.height > minSize)) michael@0: item.setOpacity(1); michael@0: else michael@0: item.setOpacity(0.7); michael@0: michael@0: e.preventDefault(); michael@0: } michael@0: michael@0: let self = this; michael@0: function collapse() { michael@0: let center = phantom.bounds().center(); michael@0: phantom.animate({ michael@0: width: 0, michael@0: height: 0, michael@0: top: center.y, michael@0: left: center.x michael@0: }, { michael@0: duration: 300, michael@0: complete: function() { michael@0: phantom.remove(); michael@0: } michael@0: }); michael@0: self.setActive(lastActiveGroupItem); michael@0: } michael@0: michael@0: function finalize(e) { michael@0: iQ(window).unbind("mousemove", updateSize); michael@0: item.container.removeClass("dragRegion"); michael@0: dragOutInfo.stop(); michael@0: let box = item.getBounds(); michael@0: if (box.width > minMinSize && box.height > minMinSize && michael@0: (box.width > minSize || box.height > minSize)) { michael@0: let opts = {bounds: item.getBounds(), focusTitle: true}; michael@0: let groupItem = new GroupItem([], opts); michael@0: self.setActive(groupItem); michael@0: phantom.remove(); michael@0: dragOutInfo = null; michael@0: gTabView.firstUseExperienced = true; michael@0: } else { michael@0: collapse(); michael@0: } michael@0: } michael@0: michael@0: iQ(window).mousemove(updateSize) michael@0: iQ(gWindow).one("mouseup", finalize); michael@0: e.preventDefault(); michael@0: return false; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _resize michael@0: // Update the TabView UI contents in response to a window size change. michael@0: // Won't do anything if it doesn't deem the resize necessary. michael@0: // Parameters: michael@0: // force - true to update even when "unnecessary"; default false michael@0: _resize: function UI__resize(force) { michael@0: if (!this._pageBounds) michael@0: return; michael@0: michael@0: // Here are reasons why we *won't* resize: michael@0: // 1. Panorama isn't visible (in which case we will resize when we do display) michael@0: // 2. the screen dimensions haven't changed michael@0: // 3. everything on the screen fits and nothing feels cramped michael@0: if (!force && !this.isTabViewVisible()) michael@0: return; michael@0: michael@0: let oldPageBounds = new Rect(this._pageBounds); michael@0: let newPageBounds = Items.getPageBounds(); michael@0: if (newPageBounds.equals(oldPageBounds)) michael@0: return; michael@0: michael@0: if (!this.shouldResizeItems()) michael@0: return; michael@0: michael@0: var items = Items.getTopLevelItems(); michael@0: michael@0: // compute itemBounds: the union of all the top-level items' bounds. michael@0: var itemBounds = new Rect(this._pageBounds); michael@0: // We start with pageBounds so that we respect the empty space the user michael@0: // has left on the page. michael@0: itemBounds.width = 1; michael@0: itemBounds.height = 1; michael@0: items.forEach(function(item) { michael@0: var bounds = item.getBounds(); michael@0: itemBounds = (itemBounds ? itemBounds.union(bounds) : new Rect(bounds)); michael@0: }); michael@0: michael@0: if (newPageBounds.width < this._pageBounds.width && michael@0: newPageBounds.width > itemBounds.width) michael@0: newPageBounds.width = this._pageBounds.width; michael@0: michael@0: if (newPageBounds.height < this._pageBounds.height && michael@0: newPageBounds.height > itemBounds.height) michael@0: newPageBounds.height = this._pageBounds.height; michael@0: michael@0: var wScale; michael@0: var hScale; michael@0: if (Math.abs(newPageBounds.width - this._pageBounds.width) michael@0: > Math.abs(newPageBounds.height - this._pageBounds.height)) { michael@0: wScale = newPageBounds.width / this._pageBounds.width; michael@0: hScale = newPageBounds.height / itemBounds.height; michael@0: } else { michael@0: wScale = newPageBounds.width / itemBounds.width; michael@0: hScale = newPageBounds.height / this._pageBounds.height; michael@0: } michael@0: michael@0: var scale = Math.min(hScale, wScale); michael@0: var self = this; michael@0: var pairs = []; michael@0: items.forEach(function(item) { michael@0: var bounds = item.getBounds(); michael@0: bounds.left += (UI.rtl ? -1 : 1) * (newPageBounds.left - self._pageBounds.left); michael@0: bounds.left *= scale; michael@0: bounds.width *= scale; michael@0: michael@0: bounds.top += newPageBounds.top - self._pageBounds.top; michael@0: bounds.top *= scale; michael@0: bounds.height *= scale; michael@0: michael@0: pairs.push({ michael@0: item: item, michael@0: bounds: bounds michael@0: }); michael@0: }); michael@0: michael@0: Items.unsquish(pairs); michael@0: michael@0: pairs.forEach(function(pair) { michael@0: pair.item.setBounds(pair.bounds, true); michael@0: pair.item.snap(); michael@0: }); michael@0: michael@0: this._pageBounds = Items.getPageBounds(); michael@0: this._save(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: shouldResizeItems michael@0: // Returns whether we should resize the items on the screen, based on whether michael@0: // the top-level items fit in the screen or not and whether they feel michael@0: // "cramped" or not. michael@0: // These computations may be done using cached values. The cache can be michael@0: // cleared with UI.clearShouldResizeItems(). michael@0: shouldResizeItems: function UI_shouldResizeItems() { michael@0: let newPageBounds = Items.getPageBounds(); michael@0: michael@0: // If we don't have cached cached values... michael@0: if (this._minimalRect === undefined || this._feelsCramped === undefined) { michael@0: michael@0: // Loop through every top-level Item for two operations: michael@0: // 1. check if it is feeling "cramped" due to squishing (a technical term), michael@0: // 2. union its bounds with the minimalRect michael@0: let feelsCramped = false; michael@0: let minimalRect = new Rect(0, 0, 1, 1); michael@0: michael@0: Items.getTopLevelItems() michael@0: .forEach(function UI_shouldResizeItems_checkItem(item) { michael@0: let bounds = new Rect(item.getBounds()); michael@0: feelsCramped = feelsCramped || (item.userSize && michael@0: (item.userSize.x > bounds.width || item.userSize.y > bounds.height)); michael@0: bounds.inset(-Trenches.defaultRadius, -Trenches.defaultRadius); michael@0: minimalRect = minimalRect.union(bounds); michael@0: }); michael@0: michael@0: // ensure the minimalRect extends to, but not beyond, the origin michael@0: minimalRect.left = 0; michael@0: minimalRect.top = 0; michael@0: michael@0: this._minimalRect = minimalRect; michael@0: this._feelsCramped = feelsCramped; michael@0: } michael@0: michael@0: return this._minimalRect.width > newPageBounds.width || michael@0: this._minimalRect.height > newPageBounds.height || michael@0: this._feelsCramped; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: clearShouldResizeItems michael@0: // Clear the cache of whether we should resize the items on the Panorama michael@0: // screen, forcing a recomputation on the next UI.shouldResizeItems() michael@0: // call. michael@0: clearShouldResizeItems: function UI_clearShouldResizeItems() { michael@0: delete this._minimalRect; michael@0: delete this._feelsCramped; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: exit michael@0: // Exits TabView UI. michael@0: exit: function UI_exit() { michael@0: let self = this; michael@0: let zoomedIn = false; michael@0: michael@0: if (Search.isEnabled()) { michael@0: let matcher = Search.createSearchTabMatcher(); michael@0: let matches = matcher.matched(); michael@0: michael@0: if (matches.length > 0) { michael@0: matches[0].zoomIn(); michael@0: zoomedIn = true; michael@0: } michael@0: Search.hide(); michael@0: } michael@0: michael@0: if (!zoomedIn) { michael@0: let unhiddenGroups = GroupItems.groupItems.filter(function(groupItem) { michael@0: return (!groupItem.hidden && groupItem.getChildren().length > 0); michael@0: }); michael@0: // no pinned tabs and no visible groups: open a new group. open a blank michael@0: // tab and return michael@0: if (!unhiddenGroups.length) { michael@0: let emptyGroups = GroupItems.groupItems.filter(function (groupItem) { michael@0: return (!groupItem.hidden && !groupItem.getChildren().length); michael@0: }); michael@0: let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup()); michael@0: if (!gBrowser._numPinnedTabs) { michael@0: group.newTab(null, { closedLastTab: true }); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If there's an active TabItem, zoom into it. If not (for instance when the michael@0: // selected tab is an app tab), just go there. michael@0: let activeTabItem = this.getActiveTab(); michael@0: if (!activeTabItem) { michael@0: let tabItem = gBrowser.selectedTab._tabViewTabItem; michael@0: if (tabItem) { michael@0: if (!tabItem.parent || !tabItem.parent.hidden) { michael@0: activeTabItem = tabItem; michael@0: } else { // set active tab item if there is at least one unhidden group michael@0: if (unhiddenGroups.length > 0) michael@0: activeTabItem = unhiddenGroups[0].getActiveTab(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (activeTabItem) { michael@0: activeTabItem.zoomIn(); michael@0: } else { michael@0: if (gBrowser._numPinnedTabs > 0) { michael@0: if (gBrowser.selectedTab.pinned) { michael@0: self.goToTab(gBrowser.selectedTab); michael@0: } else { michael@0: Array.some(gBrowser.tabs, function(tab) { michael@0: if (tab.pinned) { michael@0: self.goToTab(tab); michael@0: return true; michael@0: } michael@0: return false michael@0: }); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: storageSanity michael@0: // Given storage data for this object, returns true if it looks valid. michael@0: _storageSanity: function UI__storageSanity(data) { michael@0: if (Utils.isEmptyObject(data)) michael@0: return true; michael@0: michael@0: if (!Utils.isRect(data.pageBounds)) { michael@0: Utils.log("UI.storageSanity: bad pageBounds", data.pageBounds); michael@0: data.pageBounds = null; michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _save michael@0: // Saves the data for this object to persistent storage michael@0: _save: function UI__save() { michael@0: if (!this._frameInitialized) michael@0: return; michael@0: michael@0: var data = { michael@0: pageBounds: this._pageBounds michael@0: }; michael@0: michael@0: if (this._storageSanity(data)) michael@0: Storage.saveUIData(gWindow, data); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _saveAll michael@0: // Saves all data associated with TabView. michael@0: _saveAll: function UI__saveAll() { michael@0: this._save(); michael@0: GroupItems.saveAll(); michael@0: TabItems.saveAll(); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: notifySessionRestoreEnabled michael@0: // Notify the user that session restore has been automatically enabled michael@0: // by showing a banner that expects no user interaction. It fades out after michael@0: // some seconds. michael@0: notifySessionRestoreEnabled: function UI_notifySessionRestoreEnabled() { michael@0: let brandBundle = gWindow.document.getElementById("bundle_brand"); michael@0: let brandShortName = brandBundle.getString("brandShortName"); michael@0: let notificationText = tabviewBundle.formatStringFromName( michael@0: "tabview.notification.sessionStore", [brandShortName], 1); michael@0: michael@0: let banner = iQ("
") michael@0: .text(notificationText) michael@0: .addClass("banner") michael@0: .appendTo("body"); michael@0: michael@0: let onFadeOut = function () { michael@0: banner.remove(); michael@0: }; michael@0: michael@0: let onFadeIn = function () { michael@0: setTimeout(function () { michael@0: banner.animate({opacity: 0}, {duration: 1500, complete: onFadeOut}); michael@0: }, 5000); michael@0: }; michael@0: michael@0: banner.animate({opacity: 0.7}, {duration: 1500, complete: onFadeIn}); michael@0: } michael@0: }; michael@0: michael@0: // ---------- michael@0: UI.init();