+ TabItem updates (painting, etc) no longer happen when the Tab Candy UI is not visible; they're queued up and happen when you return to the UI + Fixed a couple problems with the TabItems pause painting mechanism + If a whole bunch of TabItem updates come at the same time, we spread them out (using a heartbeat that lasts until we run out of backlog) + This should fix both Bug 580954 (Replace the heartbeat with something smarter) and Bug 583420 (Tab thumbnails don't repaint after being resized)
1107 lines
32 KiB
JavaScript
1107 lines
32 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
|
|
* Ian Gilman <ian@iangilman.com>.
|
|
* Portions created by the Initial Developer are Copyright (C) 2010
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Aza Raskin <aza@mozilla.com>
|
|
* Michael Yoshitaka Erlewine <mitcho@mitcho.com>
|
|
* Ehsan Akhgari <ehsan@mozilla.com>
|
|
* Raymond Lee <raymond@appcoast.com>
|
|
*
|
|
* 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 <Group>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 <Group>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 <Tabs> 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 =
|
|
"<div class='intro'>"
|
|
+ "<h1>Welcome to Firefox Tab Sets</h1>" // TODO: This needs to be localized if it's kept in
|
|
+ "<div>(more goes here)</div><br>"
|
|
+ "<video src='http://people.mozilla.org/~araskin/movies/tabcandy_howto.webm' "
|
|
+ "width='100%' preload controls>"
|
|
+ "</div>";
|
|
|
|
box.left = box.right + padding;
|
|
box.width = infoWidth;
|
|
box.height = infoHeight;
|
|
var infoItem = new InfoItem(box);
|
|
infoItem.html(html);
|
|
}
|
|
|
|
// ___ tabs
|
|
TabItems.init();
|
|
TabItems.pausePainting();
|
|
|
|
// ___ 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 <TabItem>
|
|
//
|
|
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 <TabItem>
|
|
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);
|
|
});
|
|
}
|
|
|
|
TabItems.resumePainting();
|
|
},
|
|
|
|
// ----------
|
|
// Function: hideTabView
|
|
// Hides TabView and shows the main browser UI.
|
|
hideTabView: function() {
|
|
TabItems.pausePainting();
|
|
|
|
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("<div>")
|
|
.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("<select>")
|
|
.css({
|
|
position: "absolute",
|
|
bottom: 5,
|
|
right: 5,
|
|
zIndex: 99999,
|
|
opacity: .2
|
|
})
|
|
.appendTo("#content")
|
|
.change(function () {
|
|
var index = iQ(this).val();
|
|
try {
|
|
commands[index].code.apply(commands[index].element);
|
|
} catch(e) {
|
|
Utils.log("dev menu error", e);
|
|
}
|
|
iQ(this).val(0);
|
|
});
|
|
|
|
var commands = [{
|
|
name: "dev menu",
|
|
code: function() { }
|
|
}, {
|
|
name: "show trenches",
|
|
code: function() {
|
|
Trenches.toggleShown();
|
|
iQ(this).html((Trenches.showDebug ? "hide" : "show") + " trenches");
|
|
}
|
|
}, {
|
|
/*
|
|
name: "refresh",
|
|
code: function() {
|
|
location.href = "tabview.html";
|
|
}
|
|
}, {
|
|
name: "reset",
|
|
code: function() {
|
|
self._reset();
|
|
}
|
|
}, {
|
|
*/
|
|
name: "save",
|
|
code: function() {
|
|
self._saveAll();
|
|
}
|
|
}, {
|
|
name: "group sites",
|
|
code: function() {
|
|
self._arrangeBySite();
|
|
}
|
|
}];
|
|
|
|
var count = commands.length;
|
|
var a;
|
|
for (a = 0; a < count; a++) {
|
|
commands[a].element = (iQ("<option>")
|
|
.val(a)
|
|
.html(commands[a].name)
|
|
.appendTo($select))[0];
|
|
}
|
|
} catch(e) {
|
|
Utils.log(e);
|
|
}
|
|
},
|
|
|
|
// -----------
|
|
// Function: _reset
|
|
// Wipes all TabView storage and refreshes, giving you the "first-run" state.
|
|
_reset: function() {
|
|
Storage.wipe();
|
|
location.href = "";
|
|
},
|
|
|
|
// ----------
|
|
// Function: storageSanity
|
|
// Given storage data for this object, returns true if it looks valid.
|
|
_storageSanity: function(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() {
|
|
if (!this._frameInitalized)
|
|
return;
|
|
|
|
var data = {
|
|
tabViewVisible: this._isTabViewVisible(),
|
|
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() {
|
|
this._save();
|
|
Groups.saveAll();
|
|
TabItems.saveAll();
|
|
},
|
|
|
|
// ----------
|
|
// Function: _arrangeBySite
|
|
// Blows away all existing groups and organizes the tabs into new groups based
|
|
// on domain.
|
|
_arrangeBySite: function() {
|
|
function putInGroup(set, key) {
|
|
var group = Groups.getGroupWithTitle(key);
|
|
if (group) {
|
|
set.forEach(function(el) {
|
|
group.add(el);
|
|
});
|
|
} else
|
|
new Group(set, { dontPush: true, dontArrange: true, title: key });
|
|
}
|
|
|
|
Groups.removeAll();
|
|
|
|
var newTabsGroup = Groups.getNewTabGroup();
|
|
var groups = [];
|
|
var items = TabItems.getItems();
|
|
items.forEach(function(item) {
|
|
var url = item.tab.linkedBrowser.currentURI.spec;
|
|
var domain = url.split('/')[2];
|
|
|
|
if (!domain)
|
|
newTabsGroup.add(item);
|
|
else {
|
|
var domainParts = domain.split(".");
|
|
var mainDomain = domainParts[domainParts.length - 2];
|
|
if (groups[mainDomain])
|
|
groups[mainDomain].push(item.container);
|
|
else
|
|
groups[mainDomain] = [item.container];
|
|
}
|
|
});
|
|
|
|
var leftovers = [];
|
|
for (key in groups) {
|
|
var set = groups[key];
|
|
if (set.length > 1) {
|
|
putInGroup(set, key);
|
|
} else
|
|
leftovers.push(set[0]);
|
|
}
|
|
|
|
if (leftovers.length)
|
|
putInGroup(leftovers, "mixed");
|
|
|
|
Groups.arrange();
|
|
},
|
|
};
|
|
|
|
// ----------
|
|
Profile.checkpoint("script load");
|
|
Profile.wrap(UIManager, "UIManager");
|
|
Profile.wrap(Storage, "Storage");
|
|
Profile.wrap(Items, "Items");
|
|
Profile.wrap(TabItems, "TabItems");
|
|
Profile.wrap(Groups, "Groups");
|
|
|
|
window.UI = UIManager;
|
|
window.UI.init();
|
|
|
|
})();
|