mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1616 lines
51 KiB
JavaScript
1616 lines
51 KiB
JavaScript
/* ***** 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
|
|
* the Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2010
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Ian Gilman <ian@iangilman.com>
|
|
* Aza Raskin <aza@mozilla.com>
|
|
* Michael Yoshitaka Erlewine <mitcho@mitcho.com>
|
|
* Ehsan Akhgari <ehsan@mozilla.com>
|
|
* Raymond Lee <raymond@appcoast.com>
|
|
* Sean Dunn <seanedunn@yahoo.com>
|
|
* Tim Taubert <tim.taubert@gmx.de>
|
|
*
|
|
* 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
|
|
|
|
let Keys = { meta: false };
|
|
|
|
// ##########
|
|
// Class: UI
|
|
// Singleton top-level UI manager.
|
|
let UI = {
|
|
// Constant: DBLCLICK_INTERVAL
|
|
// Defines the maximum time (in ms) between two clicks for it to count as
|
|
// a double click.
|
|
DBLCLICK_INTERVAL: 500,
|
|
|
|
// Constant: DBLCLICK_OFFSET
|
|
// Defines the maximum offset (in pixels) between two clicks for it to count as
|
|
// a double click.
|
|
DBLCLICK_OFFSET: 5,
|
|
|
|
// Variable: _frameInitialized
|
|
// True if the Tab View UI frame has been initialized.
|
|
_frameInitialized: 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: restoredClosedTab
|
|
// If true, a closed tab has just been restored.
|
|
restoredClosedTab: false,
|
|
|
|
// Variable: _reorderTabItemsOnShow
|
|
// Keeps track of the <GroupItem>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 <GroupItem>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 xul:tab we are currently on.
|
|
// Used to facilitate zooming down from a previous tab.
|
|
_currentTab: null,
|
|
|
|
// Variable: _lastClick
|
|
// Keeps track of the time of last click event to detect double click.
|
|
// Used to create tabs on double-click since we cannot attach 'dblclick'
|
|
_lastClick: 0,
|
|
|
|
// Variable: _eventListeners
|
|
// Keeps track of event listeners added to the AllTabs object.
|
|
_eventListeners: {},
|
|
|
|
// Variable: _cleanupFunctions
|
|
// An array of functions to be called at uninit time
|
|
_cleanupFunctions: [],
|
|
|
|
// Constant: _maxInteractiveWait
|
|
// If the UI is in the middle of an operation, this is the max amount of
|
|
// milliseconds to wait between input events before we no longer consider
|
|
// the operation interactive.
|
|
_maxInteractiveWait: 250,
|
|
|
|
// Variable: _privateBrowsing
|
|
// Keeps track of info related to private browsing, including:
|
|
// transitionMode - whether we're entering or exiting PB
|
|
// wasInTabView - whether TabView was visible before we went into PB
|
|
_privateBrowsing: {
|
|
transitionMode: "",
|
|
wasInTabView: false
|
|
},
|
|
|
|
// Variable: _storageBusyCount
|
|
// Used to keep track of how many calls to storageBusy vs storageReady.
|
|
_storageBusyCount: 0,
|
|
|
|
// Variable: isDOMWindowClosing
|
|
// Tells wether the parent window is about to close
|
|
isDOMWindowClosing: false,
|
|
|
|
// Variable: _browserKeys
|
|
// Used to keep track of allowed browser keys.
|
|
_browserKeys: null,
|
|
|
|
// Variable: ignoreKeypressForSearch
|
|
// Used to prevent keypress being handled after quitting search mode.
|
|
ignoreKeypressForSearch: false,
|
|
|
|
// ----------
|
|
// Function: toString
|
|
// Prints [UI] for debug use
|
|
toString: function UI_toString() {
|
|
return "[UI]";
|
|
},
|
|
|
|
// ----------
|
|
// Function: init
|
|
// Must be called after the object is created.
|
|
init: function UI_init() {
|
|
try {
|
|
let self = this;
|
|
|
|
// initialize the direction of the page
|
|
this._initPageDirection();
|
|
|
|
// ___ thumbnail storage
|
|
ThumbnailStorage.init();
|
|
|
|
// ___ storage
|
|
Storage.init();
|
|
let data = Storage.readUIData(gWindow);
|
|
this._storageSanity(data);
|
|
this._pageBounds = data.pageBounds;
|
|
|
|
// ___ currentTab
|
|
this._currentTab = gBrowser.selectedTab;
|
|
|
|
// ___ exit button
|
|
iQ("#exit-button").click(function() {
|
|
self.exit();
|
|
self.blurAll();
|
|
});
|
|
|
|
// When you click on the background/empty part of TabView,
|
|
// we create a new groupItem.
|
|
iQ(gTabViewFrame.contentDocument).mousedown(function(e) {
|
|
if (iQ(":focus").length > 0) {
|
|
iQ(":focus").each(function(element) {
|
|
// don't fire blur event if the same input element is clicked.
|
|
if (e.target != element && element.nodeName == "INPUT")
|
|
element.blur();
|
|
});
|
|
}
|
|
if (e.originalTarget.id == "content") {
|
|
if (!Utils.isLeftClick(e)) {
|
|
self._lastClick = 0;
|
|
self._lastClickPositions = null;
|
|
} else {
|
|
// Create an orphan tab on double click
|
|
if (Date.now() - self._lastClick <= self.DBLCLICK_INTERVAL &&
|
|
(self._lastClickPositions.x - self.DBLCLICK_OFFSET) <= e.clientX &&
|
|
(self._lastClickPositions.x + self.DBLCLICK_OFFSET) >= e.clientX &&
|
|
(self._lastClickPositions.y - self.DBLCLICK_OFFSET) <= e.clientY &&
|
|
(self._lastClickPositions.y + self.DBLCLICK_OFFSET) >= e.clientY) {
|
|
self.setActive(null);
|
|
TabItems.creatingNewOrphanTab = true;
|
|
|
|
let newTab =
|
|
gBrowser.loadOneTab("about:blank", { inBackground: true });
|
|
|
|
let box =
|
|
new Rect(e.clientX - Math.floor(TabItems.tabWidth/2),
|
|
e.clientY - Math.floor(TabItems.tabHeight/2),
|
|
TabItems.tabWidth, TabItems.tabHeight);
|
|
newTab._tabViewTabItem.setBounds(box, true);
|
|
newTab._tabViewTabItem.pushAway(true);
|
|
self.setActive(newTab._tabViewTabItem);
|
|
|
|
TabItems.creatingNewOrphanTab = false;
|
|
newTab._tabViewTabItem.zoomIn(true);
|
|
|
|
self._lastClick = 0;
|
|
self._lastClickPositions = null;
|
|
gTabView.firstUseExperienced = true;
|
|
} else {
|
|
self._lastClick = Date.now();
|
|
self._lastClickPositions = new Point(e.clientX, e.clientY);
|
|
self._createGroupItemOnDrag(e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
iQ(window).bind("unload", function() {
|
|
self.uninit();
|
|
});
|
|
|
|
// ___ setup key handlers
|
|
this._setTabViewFrameKeyHandlers();
|
|
|
|
// ___ add tab action handlers
|
|
this._addTabActionHandlers();
|
|
|
|
// ___ groups
|
|
GroupItems.init();
|
|
GroupItems.pauseArrange();
|
|
let hasGroupItemsData = GroupItems.load();
|
|
|
|
// ___ tabs
|
|
TabItems.init();
|
|
TabItems.pausePainting();
|
|
|
|
if (!hasGroupItemsData)
|
|
this.reset();
|
|
|
|
// ___ resizing
|
|
if (this._pageBounds)
|
|
this._resize(true);
|
|
else
|
|
this._pageBounds = Items.getPageBounds();
|
|
|
|
iQ(window).resize(function() {
|
|
self._resize();
|
|
});
|
|
|
|
// ___ setup event listener to save canvas images
|
|
gWindow.addEventListener("SSWindowClosing", function onWindowClosing() {
|
|
gWindow.removeEventListener("SSWindowClosing", onWindowClosing, false);
|
|
|
|
self.isDOMWindowClosing = true;
|
|
|
|
if (self.isTabViewVisible())
|
|
GroupItems.removeHiddenGroups();
|
|
|
|
Storage.saveActiveGroupName(gWindow);
|
|
TabItems.saveAll(true);
|
|
self._save();
|
|
}, false);
|
|
|
|
// ___ Done
|
|
this._frameInitialized = true;
|
|
this._save();
|
|
|
|
// fire an iframe initialized event so everyone knows tab view is
|
|
// initialized.
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("tabviewframeinitialized", true, false);
|
|
dispatchEvent(event);
|
|
} catch(e) {
|
|
Utils.log(e);
|
|
} finally {
|
|
GroupItems.resumeArrange();
|
|
}
|
|
},
|
|
|
|
// Function: uninit
|
|
// Should be called when window is unloaded.
|
|
uninit: function UI_uninit() {
|
|
// call our cleanup functions
|
|
this._cleanupFunctions.forEach(function(func) {
|
|
func();
|
|
});
|
|
this._cleanupFunctions = [];
|
|
|
|
// additional clean up
|
|
TabItems.uninit();
|
|
GroupItems.uninit();
|
|
Storage.uninit();
|
|
ThumbnailStorage.uninit();
|
|
|
|
this._removeTabActionHandlers();
|
|
this._currentTab = null;
|
|
this._pageBounds = null;
|
|
this._reorderTabItemsOnShow = null;
|
|
this._reorderTabsOnHide = null;
|
|
this._frameInitialized = false;
|
|
},
|
|
|
|
// Property: rtl
|
|
// Returns true if we are in RTL mode, false otherwise
|
|
rtl: false,
|
|
|
|
// Function: reset
|
|
// Resets the Panorama view to have just one group with all tabs
|
|
reset: function UI_reset() {
|
|
let padding = Trenches.defaultRadius;
|
|
let welcomeWidth = 300;
|
|
let pageBounds = Items.getPageBounds();
|
|
pageBounds.inset(padding, padding);
|
|
|
|
let $actions = iQ("#actions");
|
|
if ($actions) {
|
|
pageBounds.width -= $actions.width();
|
|
if (UI.rtl)
|
|
pageBounds.left += $actions.width() - padding;
|
|
}
|
|
|
|
// ___ make a fresh groupItem
|
|
let box = new Rect(pageBounds);
|
|
box.width = Math.min(box.width * 0.667,
|
|
pageBounds.width - (welcomeWidth + padding));
|
|
box.height = box.height * 0.667;
|
|
if (UI.rtl) {
|
|
box.left = pageBounds.left + welcomeWidth + 2 * padding;
|
|
}
|
|
|
|
GroupItems.groupItems.forEach(function(group) {
|
|
group.close();
|
|
});
|
|
|
|
let options = {
|
|
bounds: box,
|
|
immediately: true
|
|
};
|
|
let groupItem = new GroupItem([], options);
|
|
let items = TabItems.getItems();
|
|
items.forEach(function(item) {
|
|
if (item.parent)
|
|
item.parent.remove(item);
|
|
groupItem.add(item, {immediately: true});
|
|
});
|
|
this.setActive(groupItem);
|
|
},
|
|
|
|
// ----------
|
|
// Function: blurAll
|
|
// Blurs any currently focused element
|
|
blurAll: function UI_blurAll() {
|
|
iQ(":focus").each(function(element) {
|
|
element.blur();
|
|
});
|
|
},
|
|
|
|
// ----------
|
|
// Function: isIdle
|
|
// Returns true if the last interaction was long enough ago to consider the
|
|
// UI idle. Used to determine whether interactivity would be sacrificed if
|
|
// the CPU was to become busy.
|
|
//
|
|
isIdle: function UI_isIdle() {
|
|
let time = Date.now();
|
|
let maxEvent = Math.max(drag.lastMoveTime, resize.lastMoveTime);
|
|
return (time - maxEvent) > this._maxInteractiveWait;
|
|
},
|
|
|
|
// ----------
|
|
// Function: getActiveTab
|
|
// Returns the currently active tab as a <TabItem>
|
|
getActiveTab: function UI_getActiveTab() {
|
|
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 <TabItem>
|
|
_setActiveTab: function UI__setActiveTab(tabItem) {
|
|
if (tabItem == this._activeTab)
|
|
return;
|
|
|
|
if (this._activeTab) {
|
|
this._activeTab.makeDeactive();
|
|
this._activeTab.removeSubscriber(this, "close");
|
|
}
|
|
this._activeTab = tabItem;
|
|
|
|
if (this._activeTab) {
|
|
let self = this;
|
|
this._activeTab.addSubscriber(this, "close", function(closedTabItem) {
|
|
if (self._activeTab == closedTabItem)
|
|
self._setActiveTab(null);
|
|
});
|
|
|
|
this._activeTab.makeActive();
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: getActiveOrphanTab
|
|
// Returns the currently active orphan tab as a <TabItem>
|
|
getActiveOrphanTab: function UI_getActiveOrphanTab() {
|
|
return (this._activeTab && !this._activeTab.parent) ? this._activeTab : null;
|
|
},
|
|
|
|
// ----------
|
|
// Function: setActive
|
|
// Sets the active tab item or group item
|
|
// Parameters:
|
|
//
|
|
// options
|
|
// dontSetActiveTabInGroup bool for not setting active tab in group
|
|
// onlyRemoveActiveGroup bool for removing active group
|
|
// onlyRemoveActiveTab bool for removing active tab
|
|
setActive: function UI_setActive(item, options) {
|
|
if (item) {
|
|
if (item.isATabItem) {
|
|
if (item.parent)
|
|
GroupItems.setActiveGroupItem(item.parent);
|
|
else
|
|
GroupItems.setActiveGroupItem(null);
|
|
this._setActiveTab(item);
|
|
} else {
|
|
GroupItems.setActiveGroupItem(item);
|
|
if (!options || !options.dontSetActiveTabInGroup) {
|
|
let activeTab = item.getActiveTab()
|
|
if (activeTab)
|
|
this._setActiveTab(activeTab);
|
|
}
|
|
}
|
|
} else {
|
|
if (options) {
|
|
if (options.onlyRemoveActiveGroup)
|
|
GroupItems.setActiveGroupItem(null);
|
|
else if (options.onlyRemoveActiveTab)
|
|
this._setActiveTab(null);
|
|
} else {
|
|
GroupItems.setActiveGroupItem(null);
|
|
this._setActiveTab(null);
|
|
}
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: isTabViewVisible
|
|
// Returns true if the TabView UI is currently shown.
|
|
isTabViewVisible: function UI_isTabViewVisible() {
|
|
return gTabViewDeck.selectedPanel == gTabViewFrame;
|
|
},
|
|
|
|
// ---------
|
|
// Function: _initPageDirection
|
|
// Initializes the page base direction
|
|
_initPageDirection: function UI__initPageDirection() {
|
|
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
|
|
getService(Ci.nsIXULChromeRegistry);
|
|
let dir = chromeReg.isLocaleRTL("global");
|
|
document.documentElement.setAttribute("dir", dir ? "rtl" : "ltr");
|
|
this.rtl = dir;
|
|
},
|
|
|
|
// ----------
|
|
// Function: showTabView
|
|
// Shows TabView and hides the main browser UI.
|
|
// Parameters:
|
|
// zoomOut - true for zoom out animation, false for nothing.
|
|
showTabView: function UI_showTabView(zoomOut) {
|
|
if (this.isTabViewVisible())
|
|
return;
|
|
|
|
// initialize the direction of the page
|
|
this._initPageDirection();
|
|
|
|
var self = this;
|
|
var currentTab = this._currentTab;
|
|
var item = null;
|
|
|
|
this._reorderTabItemsOnShow.forEach(function(groupItem) {
|
|
groupItem.reorderTabItemsBasedOnTabOrder();
|
|
});
|
|
this._reorderTabItemsOnShow = [];
|
|
|
|
#ifdef XP_WIN
|
|
// Restore the full height when showing TabView
|
|
gTabViewFrame.style.marginTop = "";
|
|
#endif
|
|
gTabViewDeck.selectedPanel = gTabViewFrame;
|
|
gWindow.TabsInTitlebar.allowedBy("tabview-open", false);
|
|
gTabViewFrame.contentWindow.focus();
|
|
|
|
gBrowser.updateTitlebar();
|
|
#ifdef XP_MACOSX
|
|
this.setTitlebarColors(true);
|
|
#endif
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("tabviewshown", true, false);
|
|
|
|
Storage.saveVisibilityData(gWindow, "true");
|
|
|
|
// Close the active group if it was empty. This will happen when the
|
|
// user returns to Panorama after looking at an app tab, having
|
|
// closed all other tabs. (If the user is looking at an orphan tab, then
|
|
// there is no active group for the purposes of this check.)
|
|
let activeGroupItem = null;
|
|
if (!UI.getActiveOrphanTab()) {
|
|
activeGroupItem = GroupItems.getActiveGroupItem();
|
|
if (activeGroupItem && activeGroupItem.closeIfEmpty())
|
|
activeGroupItem = null;
|
|
}
|
|
|
|
if (zoomOut && currentTab && currentTab._tabViewTabItem) {
|
|
item = currentTab._tabViewTabItem;
|
|
// 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._tabViewTabItem) // if the tab's been destroyed
|
|
item = null;
|
|
|
|
self.setActive(item);
|
|
|
|
self._resize(true);
|
|
dispatchEvent(event);
|
|
|
|
// Flush pending updates
|
|
GroupItems.flushAppTabUpdates();
|
|
|
|
TabItems.resumePainting();
|
|
});
|
|
} else {
|
|
self.setActive(null, { onlyRemoveActiveTab: true });
|
|
dispatchEvent(event);
|
|
|
|
// Flush pending updates
|
|
GroupItems.flushAppTabUpdates();
|
|
|
|
TabItems.resumePainting();
|
|
}
|
|
|
|
if (gTabView.firstUseExperienced)
|
|
gTabView.enableSessionRestore();
|
|
},
|
|
|
|
// ----------
|
|
// Function: hideTabView
|
|
// Hides TabView and shows the main browser UI.
|
|
hideTabView: function UI_hideTabView() {
|
|
if (!this.isTabViewVisible())
|
|
return;
|
|
|
|
// another tab might be select if user decides to stay on a page when
|
|
// a onclose confirmation prompts.
|
|
GroupItems.removeHiddenGroups();
|
|
TabItems.pausePainting();
|
|
|
|
this._reorderTabsOnHide.forEach(function(groupItem) {
|
|
groupItem.reorderTabsBasedOnTabItemOrder();
|
|
});
|
|
this._reorderTabsOnHide = [];
|
|
|
|
#ifdef XP_WIN
|
|
// Push the top of TabView frame to behind the tabbrowser, so glass can show
|
|
// XXX bug 586679: avoid shrinking the iframe and squishing iframe contents
|
|
// as well as avoiding the flash of black as we animate out
|
|
gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px";
|
|
#endif
|
|
gTabViewDeck.selectedPanel = gBrowserPanel;
|
|
gWindow.TabsInTitlebar.allowedBy("tabview-open", true);
|
|
gBrowser.contentWindow.focus();
|
|
|
|
gBrowser.updateTitlebar();
|
|
#ifdef XP_MACOSX
|
|
this.setTitlebarColors(false);
|
|
#endif
|
|
Storage.saveVisibilityData(gWindow, "false");
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("tabviewhidden", true, false);
|
|
dispatchEvent(event);
|
|
},
|
|
|
|
#ifdef XP_MACOSX
|
|
// ----------
|
|
// Function: setTitlebarColors
|
|
// Used on the Mac to make the title bar match the gradient in the rest of the
|
|
// TabView UI.
|
|
//
|
|
// Parameters:
|
|
// colors - (bool or object) true for the special TabView color, false for
|
|
// the normal color, and an object with "active" and "inactive"
|
|
// properties to specify directly.
|
|
setTitlebarColors: function UI_setTitlebarColors(colors) {
|
|
// Mac Only
|
|
var mainWindow = gWindow.document.getElementById("main-window");
|
|
if (colors === true) {
|
|
mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4");
|
|
mainWindow.setAttribute("inactivetitlebarcolor", "#EDEDED");
|
|
} else if (colors && "active" in colors && "inactive" in colors) {
|
|
mainWindow.setAttribute("activetitlebarcolor", colors.active);
|
|
mainWindow.setAttribute("inactivetitlebarcolor", colors.inactive);
|
|
} else {
|
|
mainWindow.removeAttribute("activetitlebarcolor");
|
|
mainWindow.removeAttribute("inactivetitlebarcolor");
|
|
}
|
|
},
|
|
#endif
|
|
|
|
// ----------
|
|
// Function: storageBusy
|
|
// Pauses the storage activity that conflicts with sessionstore updates and
|
|
// private browsing mode switches. Calls can be nested.
|
|
storageBusy: function UI_storageBusy() {
|
|
if (!this._storageBusyCount) {
|
|
TabItems.pauseReconnecting();
|
|
GroupItems.pauseAutoclose();
|
|
}
|
|
|
|
this._storageBusyCount++;
|
|
},
|
|
|
|
// ----------
|
|
// Function: storageReady
|
|
// Resumes the activity paused by storageBusy, and updates for any new group
|
|
// information in sessionstore. Calls can be nested.
|
|
storageReady: function UI_storageReady() {
|
|
this._storageBusyCount--;
|
|
if (!this._storageBusyCount) {
|
|
let hasGroupItemsData = GroupItems.load();
|
|
if (!hasGroupItemsData)
|
|
this.reset();
|
|
|
|
TabItems.resumeReconnecting();
|
|
GroupItems._updateTabBar();
|
|
GroupItems.resumeAutoclose();
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: _addTabActionHandlers
|
|
// Adds handlers to handle tab actions.
|
|
_addTabActionHandlers: function UI__addTabActionHandlers() {
|
|
var self = this;
|
|
|
|
// session restore events
|
|
function handleSSWindowStateBusy() {
|
|
self.storageBusy();
|
|
}
|
|
|
|
function handleSSWindowStateReady() {
|
|
self.storageReady();
|
|
}
|
|
|
|
gWindow.addEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false);
|
|
gWindow.addEventListener("SSWindowStateReady", handleSSWindowStateReady, false);
|
|
|
|
this._cleanupFunctions.push(function() {
|
|
gWindow.removeEventListener("SSWindowStateBusy", handleSSWindowStateBusy, false);
|
|
gWindow.removeEventListener("SSWindowStateReady", handleSSWindowStateReady, false);
|
|
});
|
|
|
|
// Private Browsing:
|
|
// When transitioning to PB, we exit Panorama if necessary (making note of the
|
|
// fact that we were there so we can return after PB) and make sure we
|
|
// don't reenter Panorama due to all of the session restore tab
|
|
// manipulation (which otherwise we might). When transitioning away from
|
|
// PB, we reenter Panorama if we had been there directly before PB.
|
|
function pbObserver(subject, topic, data) {
|
|
if (topic == "private-browsing") {
|
|
// We could probably do this in private-browsing-change-granted, but
|
|
// this seems like a nicer spot, right in the middle of the process.
|
|
if (data == "enter") {
|
|
// If we are in Tab View, exit.
|
|
self._privateBrowsing.wasInTabView = self.isTabViewVisible();
|
|
if (self.isTabViewVisible())
|
|
self.goToTab(gBrowser.selectedTab);
|
|
}
|
|
} else if (topic == "private-browsing-change-granted") {
|
|
if (data == "enter" || data == "exit") {
|
|
self._privateBrowsing.transitionMode = data;
|
|
self.storageBusy();
|
|
}
|
|
} else if (topic == "private-browsing-transition-complete") {
|
|
// We use .transitionMode here, as aData is empty.
|
|
if (self._privateBrowsing.transitionMode == "exit" &&
|
|
self._privateBrowsing.wasInTabView)
|
|
self.showTabView(false);
|
|
|
|
self._privateBrowsing.transitionMode = "";
|
|
self.storageReady();
|
|
}
|
|
}
|
|
|
|
Services.obs.addObserver(pbObserver, "private-browsing", false);
|
|
Services.obs.addObserver(pbObserver, "private-browsing-change-granted", false);
|
|
Services.obs.addObserver(pbObserver, "private-browsing-transition-complete", false);
|
|
|
|
this._cleanupFunctions.push(function() {
|
|
Services.obs.removeObserver(pbObserver, "private-browsing");
|
|
Services.obs.removeObserver(pbObserver, "private-browsing-change-granted");
|
|
Services.obs.removeObserver(pbObserver, "private-browsing-transition-complete");
|
|
});
|
|
|
|
// TabOpen
|
|
this._eventListeners.open = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
// if it's an app tab, add it to all the group items
|
|
if (tab.pinned)
|
|
GroupItems.addAppTab(tab);
|
|
};
|
|
|
|
// TabClose
|
|
this._eventListeners.close = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
// if it's an app tab, remove it from all the group items
|
|
if (tab.pinned)
|
|
GroupItems.removeAppTab(tab);
|
|
|
|
if (self.isTabViewVisible()) {
|
|
// just closed the selected tab in the TabView interface.
|
|
if (self._currentTab == tab)
|
|
self._closedSelectedTabInTabView = true;
|
|
} else {
|
|
// If we're currently in the process of entering private browsing,
|
|
// we don't want to go to the Tab View UI.
|
|
if (self._storageBusyCount)
|
|
return;
|
|
|
|
// if not closing the last tab
|
|
if (gBrowser.tabs.length > 1) {
|
|
// Don't return to TabView if there are any app tabs
|
|
for (let a = 0; a < gBrowser._numPinnedTabs; a++) {
|
|
if (!gBrowser.tabs[a].closing)
|
|
return;
|
|
}
|
|
|
|
var groupItem = GroupItems.getActiveGroupItem();
|
|
|
|
// 1) Only go back to the TabView tab when there you close the last
|
|
// tab of a groupItem.
|
|
let closingLastOfGroup = (groupItem &&
|
|
groupItem._children.length == 1 &&
|
|
groupItem._children[0].tab == tab);
|
|
|
|
// 2) Take care of the case where you've closed the last tab in
|
|
// an un-named groupItem, which means that the groupItem is gone (null) and
|
|
// there are no visible tabs.
|
|
let closingUnnamedGroup = (groupItem == null &&
|
|
gBrowser.visibleTabs.length <= 1);
|
|
|
|
// 3) When a blank tab is active while restoring a closed tab the
|
|
// blank tab gets removed. The active group is not closed as this is
|
|
// where the restored tab goes. So do not show the TabView.
|
|
let closingBlankTabAfterRestore =
|
|
(tab && tab._tabViewTabIsRemovedAfterRestore);
|
|
|
|
if ((closingLastOfGroup || closingUnnamedGroup) &&
|
|
!closingBlankTabAfterRestore) {
|
|
// for the tab focus event to pick up.
|
|
self._closedLastVisibleTab = true;
|
|
self.showTabView();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// TabMove
|
|
this._eventListeners.move = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
if (GroupItems.groupItems.length > 0) {
|
|
if (tab.pinned) {
|
|
if (gBrowser._numPinnedTabs > 1)
|
|
GroupItems.arrangeAppTab(tab);
|
|
} else {
|
|
let activeGroupItem = GroupItems.getActiveGroupItem();
|
|
if (activeGroupItem)
|
|
self.setReorderTabItemsOnShow(activeGroupItem);
|
|
}
|
|
}
|
|
};
|
|
|
|
// TabSelect
|
|
this._eventListeners.select = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
self.onTabSelect(tab);
|
|
};
|
|
|
|
// TabPinned
|
|
this._eventListeners.pinned = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
TabItems.handleTabPin(tab);
|
|
GroupItems.addAppTab(tab);
|
|
};
|
|
|
|
// TabUnpinned
|
|
this._eventListeners.unpinned = function(tab) {
|
|
if (tab.ownerDocument.defaultView != gWindow)
|
|
return;
|
|
|
|
TabItems.handleTabUnpin(tab);
|
|
GroupItems.removeAppTab(tab);
|
|
|
|
let groupItem = tab._tabViewTabItem.parent;
|
|
if (groupItem)
|
|
self.setReorderTabItemsOnShow(groupItem);
|
|
};
|
|
|
|
// Actually register the above handlers
|
|
for (let name in this._eventListeners)
|
|
AllTabs.register(name, this._eventListeners[name]);
|
|
},
|
|
|
|
// ----------
|
|
// Function: _removeTabActionHandlers
|
|
// Removes handlers to handle tab actions.
|
|
_removeTabActionHandlers: function UI__removeTabActionHandlers() {
|
|
for (let name in this._eventListeners)
|
|
AllTabs.unregister(name, this._eventListeners[name]);
|
|
},
|
|
|
|
// ----------
|
|
// Function: goToTab
|
|
// Selects the given xul:tab in the browser.
|
|
goToTab: function UI_goToTab(xulTab) {
|
|
// If it's not focused, the onFocus listener would handle it.
|
|
if (gBrowser.selectedTab == xulTab)
|
|
this.onTabSelect(xulTab);
|
|
else
|
|
gBrowser.selectedTab = xulTab;
|
|
},
|
|
|
|
// ----------
|
|
// Function: onTabSelect
|
|
// Called when the user switches from one tab to another outside of the TabView UI.
|
|
onTabSelect: function UI_onTabSelect(tab) {
|
|
let currentTab = this._currentTab;
|
|
this._currentTab = tab;
|
|
|
|
// if the last visible tab has just been closed, don't show the chrome UI.
|
|
if (this.isTabViewVisible() &&
|
|
(this._closedLastVisibleTab || this._closedSelectedTabInTabView ||
|
|
this.restoredClosedTab)) {
|
|
if (this.restoredClosedTab) {
|
|
// when the tab view UI is being displayed, update the thumb for the
|
|
// restored closed tab after the page load
|
|
tab.linkedBrowser.addEventListener("load", function (event) {
|
|
tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
|
|
TabItems._update(tab);
|
|
}, true);
|
|
}
|
|
this._closedLastVisibleTab = false;
|
|
this._closedSelectedTabInTabView = false;
|
|
this.restoredClosedTab = false;
|
|
return;
|
|
}
|
|
// reset these vars, just in case.
|
|
this._closedLastVisibleTab = false;
|
|
this._closedSelectedTabInTabView = false;
|
|
this.restoredClosedTab = false;
|
|
|
|
// if TabView is visible but we didn't just close the last tab or
|
|
// selected tab, show chrome.
|
|
if (this.isTabViewVisible())
|
|
this.hideTabView();
|
|
|
|
// another tab might be selected when hideTabView() is invoked so a
|
|
// validation is needed.
|
|
if (this._currentTab != tab)
|
|
return;
|
|
|
|
let oldItem = null;
|
|
let newItem = null;
|
|
|
|
if (currentTab && currentTab._tabViewTabItem)
|
|
oldItem = currentTab._tabViewTabItem;
|
|
|
|
// update the tab bar for the new tab's group
|
|
if (tab && tab._tabViewTabItem) {
|
|
if (!TabItems.reconnectingPaused()) {
|
|
newItem = tab._tabViewTabItem;
|
|
GroupItems.updateActiveGroupItemAndTabBar(newItem);
|
|
}
|
|
} else {
|
|
// No tabItem; must be an app tab. Base the tab bar on the current group.
|
|
// If no current group or orphan tab, figure it out based on what's
|
|
// already in the tab bar.
|
|
if (!GroupItems.getActiveGroupItem() && !UI.getActiveOrphanTab()) {
|
|
for (let a = 0; a < gBrowser.tabs.length; a++) {
|
|
let theTab = gBrowser.tabs[a];
|
|
if (!theTab.pinned) {
|
|
let tabItem = theTab._tabViewTabItem;
|
|
this.setActive(tabItem.parent);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (GroupItems.getActiveGroupItem() || UI.getActiveOrphanTab())
|
|
GroupItems._updateTabBar();
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: setReorderTabsOnHide
|
|
// Sets the groupItem which the tab items' tabs should be re-ordered when
|
|
// switching to the main browser UI.
|
|
// Parameters:
|
|
// groupItem - the groupItem which would be used for re-ordering tabs.
|
|
setReorderTabsOnHide: function UI_setReorderTabsOnHide(groupItem) {
|
|
if (this.isTabViewVisible()) {
|
|
var index = this._reorderTabsOnHide.indexOf(groupItem);
|
|
if (index == -1)
|
|
this._reorderTabsOnHide.push(groupItem);
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: setReorderTabItemsOnShow
|
|
// Sets the groupItem which the tab items should be re-ordered when
|
|
// switching to the tab view UI.
|
|
// Parameters:
|
|
// groupItem - the groupItem which would be used for re-ordering tab items.
|
|
setReorderTabItemsOnShow: function UI_setReorderTabItemsOnShow(groupItem) {
|
|
if (!this.isTabViewVisible()) {
|
|
var index = this._reorderTabItemsOnShow.indexOf(groupItem);
|
|
if (index == -1)
|
|
this._reorderTabItemsOnShow.push(groupItem);
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
updateTabButton: function UI__updateTabButton() {
|
|
let exitButton = document.getElementById("exit-button");
|
|
let numberOfGroups = GroupItems.groupItems.length;
|
|
|
|
exitButton.setAttribute("groups", numberOfGroups);
|
|
gTabView.updateGroupNumberBroadcaster(numberOfGroups);
|
|
},
|
|
|
|
// ----------
|
|
// Function: getClosestTab
|
|
// Convenience function to get the next tab closest to the entered position
|
|
getClosestTab: function UI_getClosestTab(tabCenter) {
|
|
let cl = null;
|
|
let clDist;
|
|
TabItems.getItems().forEach(function (item) {
|
|
if (item.parent && item.parent.hidden)
|
|
return;
|
|
let testDist = tabCenter.distance(item.bounds.center());
|
|
if (cl==null || testDist < clDist) {
|
|
cl = item;
|
|
clDist = testDist;
|
|
}
|
|
});
|
|
return cl;
|
|
},
|
|
|
|
// ----------
|
|
// Function: _setupBrowserKeys
|
|
// Sets up the allowed browser keys using key elements.
|
|
_setupBrowserKeys: function UI__setupKeyWhiteList() {
|
|
let keys = {};
|
|
|
|
[
|
|
#ifdef XP_UNIX
|
|
"quitApplication",
|
|
#else
|
|
"redo",
|
|
#endif
|
|
#ifdef XP_MACOSX
|
|
"preferencesCmdMac", "minimizeWindow", "hideThisAppCmdMac",
|
|
#endif
|
|
"newNavigator", "newNavigatorTab", "undo", "cut", "copy", "paste",
|
|
"selectAll", "find"
|
|
].forEach(function(key) {
|
|
let element = gWindow.document.getElementById("key_" + key);
|
|
keys[key] = element.getAttribute("key").toLocaleLowerCase().charCodeAt(0);
|
|
});
|
|
|
|
// for key combinations with shift key, the charCode of upper case letters
|
|
// are different to the lower case ones so need to handle them differently.
|
|
[
|
|
#ifdef XP_UNIX
|
|
"redo",
|
|
#endif
|
|
"closeWindow", "tabview", "undoCloseTab", "undoCloseWindow",
|
|
"privatebrowsing"
|
|
].forEach(function(key) {
|
|
let element = gWindow.document.getElementById("key_" + key);
|
|
keys[key] = element.getAttribute("key").toLocaleUpperCase().charCodeAt(0);
|
|
});
|
|
|
|
delete this._browserKeys;
|
|
this._browserKeys = keys;
|
|
},
|
|
|
|
// ----------
|
|
// Function: _setTabViewFrameKeyHandlers
|
|
// Sets up the key handlers for navigating between tabs within the TabView UI.
|
|
_setTabViewFrameKeyHandlers: function UI__setTabViewFrameKeyHandlers() {
|
|
let self = this;
|
|
|
|
this._setupBrowserKeys();
|
|
|
|
iQ(window).keyup(function(event) {
|
|
if (!event.metaKey)
|
|
Keys.meta = false;
|
|
});
|
|
|
|
iQ(window).keypress(function(event) {
|
|
if (event.metaKey)
|
|
Keys.meta = true;
|
|
|
|
function processBrowserKeys(evt) {
|
|
// let any keys with alt to pass through
|
|
if (evt.altKey)
|
|
return;
|
|
|
|
#ifdef XP_MACOSX
|
|
if (evt.metaKey) {
|
|
#else
|
|
if (evt.ctrlKey) {
|
|
#endif
|
|
let preventDefault = true;
|
|
if (evt.shiftKey) {
|
|
switch (evt.charCode) {
|
|
case self._browserKeys.tabview:
|
|
self.exit();
|
|
break;
|
|
#ifdef XP_UNIX
|
|
case self._browserKeys.redo:
|
|
#endif
|
|
case self._browserKeys.closeWindow:
|
|
case self._browserKeys.undoCloseTab:
|
|
case self._browserKeys.undoCloseWindow:
|
|
case self._browserKeys.privatebrowsing:
|
|
preventDefault = false;
|
|
break;
|
|
}
|
|
} else {
|
|
switch (evt.charCode) {
|
|
case self._browserKeys.find:
|
|
self.enableSearch();
|
|
break;
|
|
#ifdef XP_UNIX
|
|
case self._browserKeys.quitApplication:
|
|
#else
|
|
case self._browserKeys.redo:
|
|
#endif
|
|
#ifdef XP_MACOSX
|
|
case self._browserKeys.preferencesCmdMac:
|
|
case self._browserKeys.minimizeWindow:
|
|
case self._browserKeys.hideThisAppCmdMac:
|
|
#endif
|
|
case self._browserKeys.newNavigator:
|
|
case self._browserKeys.newNavigatorTab:
|
|
case self._browserKeys.undo:
|
|
case self._browserKeys.cut:
|
|
case self._browserKeys.copy:
|
|
case self._browserKeys.paste:
|
|
case self._browserKeys.selectAll:
|
|
preventDefault = false;
|
|
break;
|
|
}
|
|
}
|
|
if (preventDefault) {
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
if ((iQ(":focus").length > 0 && iQ(":focus")[0].nodeName == "INPUT") ||
|
|
isSearchEnabled() || self.ignoreKeypressForSearch) {
|
|
self.ignoreKeypressForSearch = false;
|
|
processBrowserKeys(event);
|
|
return;
|
|
}
|
|
|
|
function getClosestTabBy(norm) {
|
|
if (!self.getActiveTab())
|
|
return null;
|
|
let centers =
|
|
[[item.bounds.center(), item]
|
|
for each(item in TabItems.getItems()) if (!item.parent || !item.parent.hidden)];
|
|
let myCenter = self.getActiveTab().bounds.center();
|
|
let 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;
|
|
}
|
|
|
|
let preventDefault = true;
|
|
let activeTab;
|
|
let norm = null;
|
|
switch (event.keyCode) {
|
|
case KeyEvent.DOM_VK_RIGHT:
|
|
norm = function(a, me){return a.x > me.x};
|
|
break;
|
|
case KeyEvent.DOM_VK_LEFT:
|
|
norm = function(a, me){return a.x < me.x};
|
|
break;
|
|
case KeyEvent.DOM_VK_DOWN:
|
|
norm = function(a, me){return a.y > me.y};
|
|
break;
|
|
case KeyEvent.DOM_VK_UP:
|
|
norm = function(a, me){return a.y < me.y}
|
|
break;
|
|
}
|
|
|
|
if (norm != null) {
|
|
var nextTab = getClosestTabBy(norm);
|
|
if (nextTab) {
|
|
if (nextTab.isStacked && !nextTab.parent.expanded)
|
|
nextTab = nextTab.parent.getChild(0);
|
|
self.setActive(nextTab);
|
|
}
|
|
} else {
|
|
switch(event.keyCode) {
|
|
case KeyEvent.DOM_VK_ESCAPE:
|
|
let activeGroupItem = GroupItems.getActiveGroupItem();
|
|
if (activeGroupItem && activeGroupItem.expanded)
|
|
activeGroupItem.collapse();
|
|
else
|
|
self.exit();
|
|
break;
|
|
case KeyEvent.DOM_VK_RETURN:
|
|
case KeyEvent.DOM_VK_ENTER:
|
|
activeTab = self.getActiveTab();
|
|
if (activeTab)
|
|
activeTab.zoomIn();
|
|
break;
|
|
case KeyEvent.DOM_VK_TAB:
|
|
// tab/shift + tab to go to the next tab.
|
|
activeTab = self.getActiveTab();
|
|
if (activeTab) {
|
|
let tabItems = (activeTab.parent ? activeTab.parent.getChildren() :
|
|
[activeTab]);
|
|
let length = tabItems.length;
|
|
let 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.setActive(tabItems[newIndex]);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
processBrowserKeys(event);
|
|
preventDefault = false;
|
|
}
|
|
if (preventDefault) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
// ----------
|
|
// Function: enableSearch
|
|
// Enables the search feature.
|
|
enableSearch: function UI_enableSearch() {
|
|
if (!isSearchEnabled()) {
|
|
ensureSearchShown();
|
|
SearchEventHandler.switchToInMode();
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: _createGroupItemOnDrag
|
|
// Called in response to a mousedown in empty space in the TabView UI;
|
|
// creates a new groupItem based on the user's drag.
|
|
_createGroupItemOnDrag: function UI__createGroupItemOnDrag(e) {
|
|
const minSize = 60;
|
|
const minMinSize = 15;
|
|
|
|
let lastActiveGroupItem = GroupItems.getActiveGroupItem();
|
|
this.setActive(null, { onlyRemoveActiveGroup: true });
|
|
|
|
var startPos = { x: e.clientX, y: e.clientY };
|
|
var phantom = iQ("<div>")
|
|
.addClass("groupItem phantom activeGroupItem dragRegion")
|
|
.css({
|
|
position: "absolute",
|
|
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) {
|
|
// don't set a z-index because we want to force it to be low.
|
|
},
|
|
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 GroupItem, it'll do the actual pushAway.
|
|
pushAway: function () {},
|
|
};
|
|
item.setBounds(new Rect(startPos.y, startPos.x, 0, 0));
|
|
|
|
var dragOutInfo = new Drag(item, e);
|
|
|
|
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();
|
|
}
|
|
|
|
let self = this;
|
|
function collapse() {
|
|
let center = phantom.bounds().center();
|
|
phantom.animate({
|
|
width: 0,
|
|
height: 0,
|
|
top: center.y,
|
|
left: center.x
|
|
}, {
|
|
duration: 300,
|
|
complete: function() {
|
|
phantom.remove();
|
|
}
|
|
});
|
|
self.setActive(lastActiveGroupItem);
|
|
}
|
|
|
|
function finalize(e) {
|
|
iQ(window).unbind("mousemove", updateSize);
|
|
item.container.removeClass("dragRegion");
|
|
dragOutInfo.stop();
|
|
let box = item.getBounds();
|
|
if (box.width > minMinSize && box.height > minMinSize &&
|
|
(box.width > minSize || box.height > minSize)) {
|
|
var bounds = item.getBounds();
|
|
|
|
// Add all of the orphaned tabs that are contained inside the new groupItem
|
|
// to that groupItem.
|
|
var tabs = GroupItems.getOrphanedTabs();
|
|
var insideTabs = [];
|
|
for each(let tab in tabs) {
|
|
if (bounds.contains(tab.bounds))
|
|
insideTabs.push(tab);
|
|
}
|
|
|
|
var groupItem = new GroupItem(insideTabs,{bounds:bounds});
|
|
self.setActive(groupItem);
|
|
phantom.remove();
|
|
dragOutInfo = null;
|
|
gTabView.firstUseExperienced = true;
|
|
} else {
|
|
collapse();
|
|
}
|
|
}
|
|
|
|
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 UI__resize(force) {
|
|
if (!this._pageBounds)
|
|
return;
|
|
|
|
// Here are reasons why we *won't* resize:
|
|
// 1. Panorama isn't visible (in which case we will resize when we do display)
|
|
// 2. the screen dimensions haven't changed
|
|
// 3. everything on the screen fits and nothing feels cramped
|
|
if (!force && !this.isTabViewVisible())
|
|
return;
|
|
|
|
let oldPageBounds = new Rect(this._pageBounds);
|
|
let newPageBounds = Items.getPageBounds();
|
|
if (newPageBounds.equals(oldPageBounds))
|
|
return;
|
|
|
|
if (!this.shouldResizeItems())
|
|
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) {
|
|
var bounds = item.getBounds();
|
|
itemBounds = (itemBounds ? itemBounds.union(bounds) : new Rect(bounds));
|
|
});
|
|
|
|
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) {
|
|
var bounds = item.getBounds();
|
|
bounds.left += (UI.rtl ? -1 : 1) * (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: shouldResizeItems
|
|
// Returns whether we should resize the items on the screen, based on whether
|
|
// the top-level items fit in the screen or not and whether they feel
|
|
// "cramped" or not.
|
|
// These computations may be done using cached values. The cache can be
|
|
// cleared with UI.clearShouldResizeItems().
|
|
shouldResizeItems: function UI_shouldResizeItems() {
|
|
let newPageBounds = Items.getPageBounds();
|
|
|
|
// If we don't have cached cached values...
|
|
if (this._minimalRect === undefined || this._feelsCramped === undefined) {
|
|
|
|
// Loop through every top-level Item for two operations:
|
|
// 1. check if it is feeling "cramped" due to squishing (a technical term),
|
|
// 2. union its bounds with the minimalRect
|
|
let feelsCramped = false;
|
|
let minimalRect = new Rect(0, 0, 1, 1);
|
|
|
|
Items.getTopLevelItems()
|
|
.forEach(function UI_shouldResizeItems_checkItem(item) {
|
|
let bounds = new Rect(item.getBounds());
|
|
feelsCramped = feelsCramped || (item.userSize &&
|
|
(item.userSize.x > bounds.width || item.userSize.y > bounds.height));
|
|
bounds.inset(-Trenches.defaultRadius, -Trenches.defaultRadius);
|
|
minimalRect = minimalRect.union(bounds);
|
|
});
|
|
|
|
// ensure the minimalRect extends to, but not beyond, the origin
|
|
minimalRect.left = 0;
|
|
minimalRect.top = 0;
|
|
|
|
this._minimalRect = minimalRect;
|
|
this._feelsCramped = feelsCramped;
|
|
}
|
|
|
|
return this._minimalRect.width > newPageBounds.width ||
|
|
this._minimalRect.height > newPageBounds.height ||
|
|
this._feelsCramped;
|
|
},
|
|
|
|
// ----------
|
|
// Function: clearShouldResizeItems
|
|
// Clear the cache of whether we should resize the items on the Panorama
|
|
// screen, forcing a recomputation on the next UI.shouldResizeItems()
|
|
// call.
|
|
clearShouldResizeItems: function UI_clearShouldResizeItems() {
|
|
delete this._minimalRect;
|
|
delete this._feelsCramped;
|
|
},
|
|
|
|
// ----------
|
|
// Function: exit
|
|
// Exits TabView UI.
|
|
exit: function UI_exit() {
|
|
let self = this;
|
|
let zoomedIn = false;
|
|
|
|
if (isSearchEnabled()) {
|
|
let matcher = createSearchTabMacher();
|
|
let matches = matcher.matched();
|
|
|
|
if (matches.length > 0) {
|
|
matches[0].zoomIn();
|
|
zoomedIn = true;
|
|
}
|
|
hideSearch(null);
|
|
}
|
|
|
|
if (!zoomedIn) {
|
|
let unhiddenGroups = GroupItems.groupItems.filter(function(groupItem) {
|
|
return (!groupItem.hidden && groupItem.getChildren().length > 0);
|
|
});
|
|
// no pinned tabs, no visible groups and no orphaned tabs: open a new
|
|
// group. open a blank tab and return
|
|
if (!unhiddenGroups.length && !GroupItems.getOrphanedTabs().length) {
|
|
let emptyGroups = GroupItems.groupItems.filter(function (groupItem) {
|
|
return (!groupItem.hidden && !groupItem.getChildren().length);
|
|
});
|
|
let group = (emptyGroups.length ? emptyGroups[0] : GroupItems.newGroup());
|
|
if (!gBrowser._numPinnedTabs) {
|
|
group.newTab();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If there's an active TabItem, zoom into it. If not (for instance when the
|
|
// selected tab is an app tab), just go there.
|
|
let activeTabItem = this.getActiveTab();
|
|
if (!activeTabItem) {
|
|
let tabItem = gBrowser.selectedTab._tabViewTabItem;
|
|
if (tabItem) {
|
|
if (!tabItem.parent || !tabItem.parent.hidden) {
|
|
activeTabItem = tabItem;
|
|
} else { // set active tab item if there is at least one unhidden group
|
|
if (unhiddenGroups.length > 0)
|
|
activeTabItem = unhiddenGroups[0].getActiveTab();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeTabItem) {
|
|
activeTabItem.zoomIn();
|
|
} else {
|
|
if (gBrowser._numPinnedTabs > 0) {
|
|
if (gBrowser.selectedTab.pinned) {
|
|
self.goToTab(gBrowser.selectedTab);
|
|
} else {
|
|
Array.some(gBrowser.tabs, function(tab) {
|
|
if (tab.pinned) {
|
|
self.goToTab(tab);
|
|
return true;
|
|
}
|
|
return false
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// ----------
|
|
// Function: storageSanity
|
|
// Given storage data for this object, returns true if it looks valid.
|
|
_storageSanity: function UI__storageSanity(data) {
|
|
if (Utils.isEmptyObject(data))
|
|
return true;
|
|
|
|
if (!Utils.isRect(data.pageBounds)) {
|
|
Utils.log("UI.storageSanity: bad pageBounds", data.pageBounds);
|
|
data.pageBounds = null;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
// ----------
|
|
// Function: _save
|
|
// Saves the data for this object to persistent storage
|
|
_save: function UI__save() {
|
|
if (!this._frameInitialized)
|
|
return;
|
|
|
|
var data = {
|
|
pageBounds: this._pageBounds
|
|
};
|
|
|
|
if (this._storageSanity(data))
|
|
Storage.saveUIData(gWindow, data);
|
|
},
|
|
|
|
// ----------
|
|
// Function: _saveAll
|
|
// Saves all data associated with TabView.
|
|
// TODO: Save info items
|
|
_saveAll: function UI__saveAll() {
|
|
this._save();
|
|
GroupItems.saveAll();
|
|
TabItems.saveAll();
|
|
},
|
|
|
|
// ----------
|
|
// Function: shouldLoadFavIcon
|
|
// Takes a xul:browser and checks whether we should display a favicon for it.
|
|
shouldLoadFavIcon: function UI_shouldLoadFavIcon(browser) {
|
|
return !(browser.contentDocument instanceof window.ImageDocument) &&
|
|
(browser.currentURI.schemeIs("about") ||
|
|
gBrowser.shouldLoadFavIcon(browser.contentDocument.documentURIObject));
|
|
},
|
|
|
|
// ----------
|
|
// Function: getFavIconUrlForTab
|
|
// Gets fav icon url for the given xul:tab.
|
|
getFavIconUrlForTab: function UI_getFavIconUrlForTab(tab) {
|
|
let url;
|
|
|
|
// use the tab image if it doesn't start with http e.g. data:image/png, chrome://
|
|
if (tab.image && !(/^https?:/.test(tab.image)))
|
|
url = tab.image;
|
|
else
|
|
url = gFavIconService.getFaviconImageForPage(tab.linkedBrowser.currentURI).spec;
|
|
|
|
return url;
|
|
}
|
|
};
|
|
|
|
// ----------
|
|
UI.init();
|