/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is ui.js. * * The Initial Developer of the Original Code is * Ian Gilman . * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Aza Raskin * Michael Yoshitaka Erlewine * Ehsan Akhgari * Raymond Lee * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ // ********** // Title: ui.js (function() { window.Keys = { meta: false }; // ########## // Class: UIManager // Singleton top-level UI manager. var UIManager = { // Variable: _frameInitalized // True if the Tab View UI frame has been initialized. _frameInitalized: false, // Variable: _pageBounds // Stores the page bounds. _pageBounds : null, // Variable: _closedLastVisibleTab // If true, the last visible tab has just been closed in the tab strip. _closedLastVisibleTab : false, // Variable: _closedSelectedTabInTabView // If true, a select tab has just been closed in TabView. _closedSelectedTabInTabView : false, // Variable: _stopZoomPreparation // If true, prevent the next zoom preparation. _stopZoomPreparation : false, // Variable: _reorderTabItemsOnShow // Keeps track of the s which their tab items' tabs have been moved // and re-orders the tab items when switching to TabView. _reorderTabItemsOnShow : [], // Variable: _reorderTabsOnHide // Keeps track of the s which their tab items have been moved in // TabView UI and re-orders the tabs when switcing back to main browser. _reorderTabsOnHide : [], // Variable: _currentTab // Keeps track of which tab we are currently on. // Used to facilitate zooming down from a previous tab. _currentTab : null, // ---------- // Function: init // Must be called after the object is created. init: function() { var self = this; Profile.checkpoint(); Storage.onReady(function() { self._delayInit(); }); }, // ---------- // Function: _delayInit // Called automatically by init once sessionstore is online. _delayInit : function() { try { Profile.checkpoint("delay until _delayInit"); let self = this; // ___ storage Storage.init(); let data = Storage.readUIData(gWindow); this._storageSanity(data); this._pageBounds = data.pageBounds; // ___ hook into the browser this._setBrowserKeyHandlers(); gWindow.addEventListener("tabviewshow", function() { self.showTabView(true); }, false); // ___ show TabView at startup based on last session. if (data.tabViewVisible) { this._stopZoomPreparation = true; this.showTabView(); // ensure the tabs in the tab strip are in the same order as the tab // items in groups when switching back to main browser UI for the first // time. Groups.groups.forEach(function(group) { self._reorderTabsOnHide.push(group); }); } } catch(e) { Utils.log(e); } }, // ---------- // Function: _initFrame // Initializes the TabView UI _initFrame: function() { try { Utils.assert("must not be already initialized", !this._frameInitalized); let self = this; this._currentTab = gBrowser.selectedTab; // ___ Dev Menu this._addDevMenu(); // When you click on the background/empty part of TabView, // we create a new group. iQ(gTabViewFrame.contentDocument).mousedown(function(e) { if (iQ(":focus").length > 0) { iQ(":focus").each(function(element) { if (element.nodeName == "INPUT") element.blur(); }); } if (e.originalTarget.id == "content") self._createGroupOnDrag(e) }); iQ(window).bind("beforeunload", function() { Array.forEach(gBrowser.tabs, function(tab) { tab.hidden = false; }); }); gWindow.addEventListener("tabviewhide", function() { var activeTab = self.getActiveTab(); if (activeTab) activeTab.zoomIn(); }, false); // ___ setup key handlers this._setTabViewFrameKeyHandlers(); // ___ add tab action handlers this._addTabActionHandlers(); // ___ Storage var groupsData = Storage.readGroupsData(gWindow); var firstTime = !groupsData || Utils.isEmptyObject(groupsData); var groupData = Storage.readGroupData(gWindow); Groups.reconstitute(groupsData, groupData); if (firstTime) { var padding = 10; var infoWidth = 350; var infoHeight = 350; var pageBounds = Items.getPageBounds(); pageBounds.inset(padding, padding); // ___ make a fresh group var box = new Rect(pageBounds); box.width = Math.min(box.width * 0.667, pageBounds.width - (infoWidth + padding)); box.height = box.height * 0.667; var options = { bounds: box }; var group = new Group([], options); var items = TabItems.getItems(); items.forEach(function(item) { if (item.parent) item.parent.remove(item); group.add(item); }); // ___ make info item var html = "
" + "

Welcome to Firefox Tab Sets

" // TODO: This needs to be localized if it's kept in + "
(more goes here)

" + "
"; box.left = box.right + padding; box.width = infoWidth; box.height = infoHeight; var infoItem = new InfoItem(box); infoItem.html(html); } // ___ tabs TabItems.init(); // ___ resizing if (this._pageBounds) this._resize(true); else this._pageBounds = Items.getPageBounds(); iQ(window).resize(function() { self._resize(); }); // ___ setup observer to save canvas images var observer = { observe : function(subject, topic, data) { if (topic == "quit-application-requested") { if (self._isTabViewVisible()) TabItems.saveAll(true); self._save(); } } }; Services.obs.addObserver(observer, "quit-application-requested", false); // ___ Done this._frameInitalized = true; this._save(); } catch(e) { Utils.log(e); } }, // ---------- // Function: getActiveTab // Returns the currently active tab as a // getActiveTab: function() { return this._activeTab; }, // ---------- // Function: setActiveTab // Sets the currently active tab. The idea of a focused tab is useful // for keyboard navigation and returning to the last zoomed-in tab. // Hitting return/esc brings you to the focused tab, and using the // arrow keys lets you navigate between open tabs. // // Parameters: // - Takes a setActiveTab: function(tab) { if (tab == this._activeTab) return; if (this._activeTab) { this._activeTab.makeDeactive(); this._activeTab.removeSubscriber(this, "close"); } this._activeTab = tab; if (this._activeTab) { var self = this; this._activeTab.addSubscriber(this, "close", function() { self._activeTab = null; }); this._activeTab.makeActive(); } }, // ---------- // Function: _isTabViewVisible // Returns true if the TabView UI is currently shown. _isTabViewVisible: function() { return gTabViewDeck.selectedIndex == 1; }, // ---------- // Function: showTabView // Shows TabView and hides the main browser UI. // Parameters: // zoomOut - true for zoom out animation, false for nothing. showTabView: function(zoomOut) { var self = this; if (!this._frameInitalized) this._initFrame(); var currentTab = this._currentTab; var item = null; this._reorderTabItemsOnShow.forEach(function(group) { group.reorderTabItemsBasedOnTabOrder(); }); this._reorderTabItemsOnShow = []; gTabViewDeck.selectedIndex = 1; gTabViewFrame.contentWindow.focus(); gBrowser.updateTitlebar(); #ifdef XP_MACOSX this._setActiveTitleColor(true); #endif if (zoomOut && currentTab && currentTab.tabItem) { item = currentTab.tabItem; // If there was a previous currentTab we want to animate // its thumbnail (canvas) for the zoom out. // Note that we start the animation on the chrome thread. // Zoom out! item.zoomOut(function() { if (!currentTab.tabItem) // if the tab's been destroyed item = null; self.setActiveTab(item); var activeGroup = Groups.getActiveGroup(); if (activeGroup) activeGroup.setTopChild(item); window.Groups.setActiveGroup(null); self._resize(true); }); } }, // ---------- // Function: hideTabView // Hides TabView and shows the main browser UI. hideTabView: function() { this._reorderTabsOnHide.forEach(function(group) { group.reorderTabsBasedOnTabItemOrder(); }); this._reorderTabsOnHide = []; gTabViewDeck.selectedIndex = 0; gBrowser.contentWindow.focus(); // set the close button on tab /* Utils.timeout(function() { // Marshal event from chrome thread to DOM thread */ gBrowser.tabContainer.adjustTabstrip(); /* }, 1); */ gBrowser.updateTitlebar(); #ifdef XP_MACOSX this._setActiveTitleColor(false); #endif }, #ifdef XP_MACOSX // ---------- // Function: _setActiveTitleColor // Used on the Mac to make the title bar match the gradient in the rest of the // TabView UI. // // Parameters: // set - true for the special TabView color, false for the normal color. _setActiveTitleColor: function(set) { // Mac Only var mainWindow = gWindow.document.getElementById("main-window"); if (set) mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4"); else mainWindow.removeAttribute("activetitlebarcolor"); }, #endif // ---------- // Function: _addTabActionHandlers // Adds handlers to handle tab actions. _addTabActionHandlers: function() { var self = this; Tabs.onClose(function() { if (this.ownerDocument.defaultView != gWindow) return; if (self._isTabViewVisible()) { // just closed the selected tab in the TabView interface. if (self._currentTab == this) self._closedSelectedTabInTabView = true; } else { // if not closing the last tab if (gBrowser.tabs.length > 1) { var group = Groups.getActiveGroup(); // 1) Only go back to the TabView tab when there you close the last // tab of a group. // 2) Take care of the case where you've closed the last tab in // an un-named group, which means that the group is gone (null) and // there are no visible tabs. // Can't use timeout here because user would see a flicker of // switching to another tab before the TabView interface shows up. if ((group && group._children.length == 1) || (group == null && gBrowser.visibleTabs.length == 1)) { // for the tab focus event to pick up. self._closedLastVisibleTab = true; // remove the zoom prep. if (this && this.tabItem) this.tabItem.setZoomPrep(false); self.showTabView(); } // ToDo: When running unit tests, everything happens so quick so // new tabs might be added after a tab is closing. Therefore, this // hack is used. We should look for a better solution. Utils.timeout(function() { // Marshal event from chrome thread to DOM thread if ((group && group._children.length > 0) || (group == null && gBrowser.visibleTabs.length > 0)) self.hideTabView(); }, 1); } } return false; }); Tabs.onMove(function() { if (this.ownerDocument.defaultView != gWindow) return; Utils.timeout(function() { // Marshal event from chrome thread to DOM thread if (!self._isTabViewVisible()) { var activeGroup = Groups.getActiveGroup(); if (activeGroup) { var index = self._reorderTabItemsOnShow.indexOf(activeGroup); if (index == -1) self._reorderTabItemsOnShow.push(activeGroup); } } }, 1); }); Tabs.onSelect(function() { if (this.ownerDocument.defaultView != gWindow) return; self.tabOnFocus(this); }); }, // ---------- // Function: tabOnFocus // Called when the user switches from one tab to another outside of the TabView UI. tabOnFocus: function(tab) { var self = this; var focusTab = tab; var currentTab = this._currentTab; this._currentTab = focusTab; // if the last visible tab has just been closed, don't show the chrome UI. if (this._isTabViewVisible() && (this._closedLastVisibleTab || this._closedSelectedTabInTabView)) { this._closedLastVisibleTab = false; this._closedSelectedTabInTabView = false; return; } // if TabView is visible but we didn't just close the last tab or // selected tab, show chrome. if (this._isTabViewVisible()) this.hideTabView(); // reset these vars, just in case. this._closedLastVisibleTab = false; this._closedSelectedTabInTabView = false; Utils.timeout(function() { // Marshal event from chrome thread to DOM thread // this value is true when TabView is open at browser startup. if (self._stopZoomPreparation) { self._stopZoomPreparation = false; if (focusTab && focusTab.tabItem) self.setActiveTab(focusTab.tabItem); return; } if (focusTab != self._currentTab) { // things have changed while we were in timeout return; } var visibleTabCount = gBrowser.visibleTabs.length; var newItem = null; if (focusTab && focusTab.tabItem) { newItem = focusTab.tabItem; Groups.setActiveGroup(newItem.parent); } // ___ prepare for when we return to TabView var oldItem = null; if (currentTab && currentTab.tabItem) oldItem = currentTab.tabItem; if (newItem != oldItem) { if (oldItem) oldItem.setZoomPrep(false); // if the last visible tab is removed, don't set zoom prep because // we shoud be in the TabView interface. if (visibleTabCount > 0 && newItem && !self._isTabViewVisible()) newItem.setZoomPrep(true); } else { // the tab is already focused so the new and old items are the // same. if (oldItem) oldItem.setZoomPrep(!self._isTabViewVisible()); } }, 1); }, // ---------- // Function: setReorderTabsOnHide // Sets the group which the tab items' tabs should be re-ordered when // switching to the main browser UI. // Parameters: // group - the group which would be used for re-ordering tabs. setReorderTabsOnHide: function(group) { if (this._isTabViewVisible()) { var index = this._reorderTabsOnHide.indexOf(group); if (index == -1) this._reorderTabsOnHide.push(group); } }, // ---------- // Function: _setBrowserKeyHandlers // Overrides the browser's keys for navigating between tab (outside of the // TabView UI) so they do the right thing in respect to groups. _setBrowserKeyHandlers : function() { var self = this; gWindow.addEventListener("keypress", function(event) { if (self._isTabViewVisible()) return; var charCode = event.charCode; #ifdef XP_MACOSX // if a text box in a webpage has the focus, the event.altKey would // return false so we are depending on the charCode here. if (!event.ctrlKey && !event.metaKey && !event.shiftKey && charCode == 160) { // alt + space #else if (event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey && charCode == 32) { // ctrl + space #endif event.stopPropagation(); event.preventDefault(); self.showTabView(true); return; } // Control (+ Shift) + ` if (event.ctrlKey && !event.metaKey && !event.altKey && (charCode == 96 || charCode == 126)) { event.stopPropagation(); event.preventDefault(); var tabItem = Groups.getNextGroupTab(event.shiftKey); if (tabItem) gBrowser.selectedTab = tabItem.tab; } }, true); }, // ---------- // Function: _setTabViewFrameKeyHandlers // Sets up the key handlers for navigating between tabs within the TabView UI. _setTabViewFrameKeyHandlers: function() { var self = this; iQ(window).keyup(function(event) { if (!event.metaKey) window.Keys.meta = false; }); iQ(window).keydown(function(event) { if (event.metaKey) window.Keys.meta = true; if (!self.getActiveTab() || iQ(":focus").length > 0) { // prevent the default action when tab is pressed so it doesn't gives // us problem with content focus. if (event.which == 9) { event.stopPropagation(); event.preventDefault(); } return; } function getClosestTabBy(norm) { var centers = [[item.bounds.center(), item] for each(item in TabItems.getItems())]; var myCenter = self.getActiveTab().bounds.center(); var matches = centers .filter(function(item){return norm(item[0], myCenter)}) .sort(function(a,b){ return myCenter.distance(a[0]) - myCenter.distance(b[0]); }); if (matches.length > 0) return matches[0][1]; return null; } var norm = null; switch (event.which) { case 39: // Right norm = function(a, me){return a.x > me.x}; break; case 37: // Left norm = function(a, me){return a.x < me.x}; break; case 40: // Down norm = function(a, me){return a.y > me.y}; break; case 38: // Up norm = function(a, me){return a.y < me.y} break; } if (norm != null) { var nextTab = getClosestTabBy(norm); if (nextTab) { if (nextTab.inStack() && !nextTab.parent.expanded) nextTab = nextTab.parent.getChild(0); self.setActiveTab(nextTab); } event.stopPropagation(); event.preventDefault(); } else if (event.which == 32) { // alt/control + space to zoom into the active tab. #ifdef XP_MACOSX if (event.altKey && !event.metaKey && !event.shiftKey && !event.ctrlKey) { #else if (event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { #endif var activeTab = self.getActiveTab(); if (activeTab) activeTab.zoomIn(); event.stopPropagation(); event.preventDefault(); } } else if (event.which == 27 || event.which == 13) { // esc or return to zoom into the active tab. var activeTab = self.getActiveTab(); if (activeTab) activeTab.zoomIn(); event.stopPropagation(); event.preventDefault(); } else if (event.which == 9) { // tab/shift + tab to go to the next tab. var activeTab = self.getActiveTab(); if (activeTab) { var tabItems = (activeTab.parent ? activeTab.parent.getChildren() : Groups.getOrphanedTabs()); var length = tabItems.length; var currentIndex = tabItems.indexOf(activeTab); if (length > 1) { if (event.shiftKey) { if (currentIndex == 0) newIndex = (length - 1); else newIndex = (currentIndex - 1); } else { if (currentIndex == (length - 1)) newIndex = 0; else newIndex = (currentIndex + 1); } self.setActiveTab(tabItems[newIndex]); } } event.stopPropagation(); event.preventDefault(); } }); }, // ---------- // Function: _createGroupOnDrag // Called in response to a mousedown in empty space in the TabView UI; // creates a new group based on the user's drag. _createGroupOnDrag: function(e) { const minSize = 60; const minMinSize = 15; var startPos = { x: e.clientX, y: e.clientY }; var phantom = iQ("
") .addClass("group phantom") .css({ position: "absolute", opacity: .7, zIndex: -1, cursor: "default" }) .appendTo("body"); var item = { // a faux-Item container: phantom, isAFauxItem: true, bounds: {}, getBounds: function FauxItem_getBounds() { return this.container.bounds(); }, setBounds: function FauxItem_setBounds(bounds) { this.container.css(bounds); }, setZ: function FauxItem_setZ(z) { this.container.css("z-index", z); }, setOpacity: function FauxItem_setOpacity(opacity) { this.container.css("opacity", opacity); }, // we don't need to pushAway the phantom item at the end, because // when we create a new Group, it'll do the actual pushAway. pushAway: function () {}, }; item.setBounds(new Rect(startPos.y, startPos.x, 0, 0)); var dragOutInfo = new Drag(item, e, true); // true = isResizing function updateSize(e) { var box = new Rect(); box.left = Math.min(startPos.x, e.clientX); box.right = Math.max(startPos.x, e.clientX); box.top = Math.min(startPos.y, e.clientY); box.bottom = Math.max(startPos.y, e.clientY); item.setBounds(box); // compute the stationaryCorner var stationaryCorner = ""; if (startPos.y == box.top) stationaryCorner += "top"; else stationaryCorner += "bottom"; if (startPos.x == box.left) stationaryCorner += "left"; else stationaryCorner += "right"; dragOutInfo.snap(stationaryCorner, false, false); // null for ui, which we don't use anyway. box = item.getBounds(); if (box.width > minMinSize && box.height > minMinSize && (box.width > minSize || box.height > minSize)) item.setOpacity(1); else item.setOpacity(0.7); e.preventDefault(); } function collapse() { phantom.animate({ width: 0, height: 0, top: phantom.position().top + phantom.height()/2, left: phantom.position().left + phantom.width()/2 }, { duration: 300, complete: function() { phantom.remove(); } }); } function finalize(e) { iQ(window).unbind("mousemove", updateSize); dragOutInfo.stop(); if (phantom.css("opacity") != 1) collapse(); else { var bounds = item.getBounds(); // Add all of the orphaned tabs that are contained inside the new group // to that group. var tabs = Groups.getOrphanedTabs(); var insideTabs = []; for each(tab in tabs) { if (bounds.contains(tab.bounds)) insideTabs.push(tab); } var group = new Group(insideTabs,{bounds:bounds}); phantom.remove(); dragOutInfo = null; } } iQ(window).mousemove(updateSize) iQ(gWindow).one("mouseup", finalize); e.preventDefault(); return false; }, // ---------- // Function: _resize // Update the TabView UI contents in response to a window size change. // Won't do anything if it doesn't deem the resize necessary. // Parameters: // force - true to update even when "unnecessary"; default false _resize: function(force) { if (typeof(force) == "undefined") force = false; // If TabView isn't focused and is not showing, don't perform a resize. // This resize really slows things down. if (!force && !this._isTabViewVisible()) return; var oldPageBounds = new Rect(this._pageBounds); var newPageBounds = Items.getPageBounds(); if (newPageBounds.equals(oldPageBounds)) return; var items = Items.getTopLevelItems(); // compute itemBounds: the union of all the top-level items' bounds. var itemBounds = new Rect(this._pageBounds); // We start with pageBounds so that we respect the empty space the user // has left on the page. itemBounds.width = 1; itemBounds.height = 1; items.forEach(function(item) { if (item.locked.bounds) return; var bounds = item.getBounds(); itemBounds = (itemBounds ? itemBounds.union(bounds) : new Rect(bounds)); }); Groups.repositionNewTabGroup(); // TODO: if (newPageBounds.width < this._pageBounds.width && newPageBounds.width > itemBounds.width) newPageBounds.width = this._pageBounds.width; if (newPageBounds.height < this._pageBounds.height && newPageBounds.height > itemBounds.height) newPageBounds.height = this._pageBounds.height; var wScale; var hScale; if (Math.abs(newPageBounds.width - this._pageBounds.width) > Math.abs(newPageBounds.height - this._pageBounds.height)) { wScale = newPageBounds.width / this._pageBounds.width; hScale = newPageBounds.height / itemBounds.height; } else { wScale = newPageBounds.width / itemBounds.width; hScale = newPageBounds.height / this._pageBounds.height; } var scale = Math.min(hScale, wScale); var self = this; var pairs = []; items.forEach(function(item) { if (item.locked.bounds) return; var bounds = item.getBounds(); bounds.left += newPageBounds.left - self._pageBounds.left; bounds.left *= scale; bounds.width *= scale; bounds.top += newPageBounds.top - self._pageBounds.top; bounds.top *= scale; bounds.height *= scale; pairs.push({ item: item, bounds: bounds }); }); Items.unsquish(pairs); pairs.forEach(function(pair) { pair.item.setBounds(pair.bounds, true); pair.item.snap(); }); this._pageBounds = Items.getPageBounds(); this._save(); }, // ---------- // Function: _addDevMenu // Fills out the "dev menu" in the TabView UI. _addDevMenu: function() { try { var self = this; var $select = iQ("