gecko-dev/browser/base/content/browser-ctrlTab.js

592 строки
17 KiB
JavaScript

/*
#ifdef 0
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
#endif
*/
/**
* Tab previews utility, produces thumbnails
*/
var tabPreviews = {
init: function tabPreviews_init() {
if (this._selectedTab)
return;
this._selectedTab = gBrowser.selectedTab;
gBrowser.tabContainer.addEventListener("TabSelect", this, false);
gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
.getService(Ci.nsIScreenManager);
let left = {}, top = {}, width = {}, height = {};
screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
this.aspectRatio = height.value / width.value;
},
get: function tabPreviews_get(aTab) {
let uri = aTab.linkedBrowser.currentURI.spec;
if (aTab.__thumbnail_lastURI &&
aTab.__thumbnail_lastURI != uri) {
aTab.__thumbnail = null;
aTab.__thumbnail_lastURI = null;
}
if (aTab.__thumbnail)
return aTab.__thumbnail;
if (aTab.getAttribute("pending") == "true") {
let img = new Image;
img.src = PageThumbs.getThumbnailURL(uri);
return img;
}
return this.capture(aTab, !aTab.hasAttribute("busy"));
},
capture: function tabPreviews_capture(aTab, aShouldCache) {
let browser = aTab.linkedBrowser;
let uri = browser.currentURI.spec;
let canvas = PageThumbs.createCanvas(window);
PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => {
if (aDoStore && aShouldCache) {
PageThumbs.captureAndStore(browser, function () {
let img = new Image;
img.src = PageThumbs.getThumbnailURL(uri);
aTab.__thumbnail = img;
aTab.__thumbnail_lastURI = uri;
canvas.getContext("2d").drawImage(img, 0, 0);
});
} else {
PageThumbs.captureToCanvas(browser, canvas, () => {
if (aShouldCache) {
aTab.__thumbnail = canvas;
aTab.__thumbnail_lastURI = uri;
}
});
}
});
return canvas;
},
handleEvent: function tabPreviews_handleEvent(event) {
switch (event.type) {
case "TabSelect":
if (this._selectedTab &&
this._selectedTab.parentNode &&
!this._pendingUpdate) {
// Generate a thumbnail for the tab that was selected.
// The timeout keeps the UI snappy and prevents us from generating thumbnails
// for tabs that will be closed. During that timeout, don't generate other
// thumbnails in case multiple TabSelect events occur fast in succession.
this._pendingUpdate = true;
setTimeout(function (self, aTab) {
self._pendingUpdate = false;
if (aTab.parentNode &&
!aTab.hasAttribute("busy") &&
!aTab.hasAttribute("pending"))
self.capture(aTab, true);
}, 2000, this, this._selectedTab);
}
this._selectedTab = event.target;
break;
case "SSTabRestored":
this.capture(event.target, true);
break;
}
}
};
var tabPreviewPanelHelper = {
opening: function (host) {
host.panel.hidden = false;
var handler = this._generateHandler(host);
host.panel.addEventListener("popupshown", handler, false);
host.panel.addEventListener("popuphiding", handler, false);
host._prevFocus = document.commandDispatcher.focusedElement;
},
_generateHandler: function (host) {
var self = this;
return function (event) {
if (event.target == host.panel) {
host.panel.removeEventListener(event.type, arguments.callee, false);
self["_" + event.type](host);
}
};
},
_popupshown: function (host) {
if ("setupGUI" in host)
host.setupGUI();
},
_popuphiding: function (host) {
if ("suspendGUI" in host)
host.suspendGUI();
if (host._prevFocus) {
Services.focus.setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL);
host._prevFocus = null;
} else
gBrowser.selectedBrowser.focus();
if (host.tabToSelect) {
gBrowser.selectedTab = host.tabToSelect;
host.tabToSelect = null;
}
}
};
/**
* Ctrl-Tab panel
*/
var ctrlTab = {
get panel () {
delete this.panel;
return this.panel = document.getElementById("ctrlTab-panel");
},
get showAllButton () {
delete this.showAllButton;
return this.showAllButton = document.getElementById("ctrlTab-showAll");
},
get previews () {
delete this.previews;
return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
},
get maxTabPreviews () {
delete this.maxTabPreviews;
return this.maxTabPreviews = this.previews.length - 1;
},
get canvasWidth () {
delete this.canvasWidth;
return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews);
},
get canvasHeight () {
delete this.canvasHeight;
return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
},
get keys () {
var keys = {};
["close", "find", "selectAll"].forEach(function (key) {
keys[key] = document.getElementById("key_" + key)
.getAttribute("key")
.toLocaleLowerCase().charCodeAt(0);
});
delete this.keys;
return this.keys = keys;
},
_selectedIndex: 0,
get selected () {
return this._selectedIndex < 0 ?
document.activeElement :
this.previews.item(this._selectedIndex);
},
get isOpen () {
return this.panel.state == "open" || this.panel.state == "showing" || this._timer;
},
get tabCount () {
return this.tabList.length;
},
get tabPreviewCount () {
return Math.min(this.maxTabPreviews, this.tabCount);
},
get tabList () {
return this._recentlyUsedTabs;
},
init: function ctrlTab_init() {
if (!this._recentlyUsedTabs) {
tabPreviews.init();
this._initRecentlyUsedTabs();
this._init(true);
}
},
uninit: function ctrlTab_uninit() {
this._recentlyUsedTabs = null;
this._init(false);
},
prefName: "browser.ctrlTab.previews",
readPref: function ctrlTab_readPref() {
var enable =
gPrefService.getBoolPref(this.prefName) &&
(!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
!gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
if (enable)
this.init();
else
this.uninit();
},
observe: function (aSubject, aTopic, aPrefName) {
this.readPref();
},
updatePreviews: function ctrlTab_updatePreviews() {
for (let i = 0; i < this.previews.length; i++)
this.updatePreview(this.previews[i], this.tabList[i]);
var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label");
this.showAllButton.label =
PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount);
this.showAllButton.hidden = !allTabs.canOpen;
},
updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
if (aPreview == this.showAllButton)
return;
aPreview._tab = aTab;
if (aPreview.firstChild)
aPreview.removeChild(aPreview.firstChild);
if (aTab) {
let canvasWidth = this.canvasWidth;
let canvasHeight = this.canvasHeight;
aPreview.appendChild(tabPreviews.get(aTab));
aPreview.setAttribute("label", aTab.label);
aPreview.setAttribute("tooltiptext", aTab.label);
aPreview.setAttribute("crop", aTab.crop);
aPreview.setAttribute("canvaswidth", canvasWidth);
aPreview.setAttribute("canvasstyle",
"max-width:" + canvasWidth + "px;" +
"min-width:" + canvasWidth + "px;" +
"max-height:" + canvasHeight + "px;" +
"min-height:" + canvasHeight + "px;");
if (aTab.image)
aPreview.setAttribute("image", aTab.image);
else
aPreview.removeAttribute("image");
aPreview.hidden = false;
} else {
aPreview.hidden = true;
aPreview.removeAttribute("label");
aPreview.removeAttribute("tooltiptext");
aPreview.removeAttribute("image");
}
},
advanceFocus: function ctrlTab_advanceFocus(aForward) {
let selectedIndex = Array.indexOf(this.previews, this.selected);
do {
selectedIndex += aForward ? 1 : -1;
if (selectedIndex < 0)
selectedIndex = this.previews.length - 1;
else if (selectedIndex >= this.previews.length)
selectedIndex = 0;
} while (this.previews[selectedIndex].hidden);
if (this._selectedIndex == -1) {
// Focus is already in the panel.
this.previews[selectedIndex].focus();
} else {
this._selectedIndex = selectedIndex;
}
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
this._openPanel();
}
},
_mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
if (this._trackMouseOver)
aPreview.focus();
},
pick: function ctrlTab_pick(aPreview) {
if (!this.tabCount)
return;
var select = (aPreview || this.selected);
if (select == this.showAllButton)
this.showAllTabs();
else
this.close(select._tab);
},
showAllTabs: function ctrlTab_showAllTabs(aPreview) {
this.close();
document.getElementById("Browser:ShowAllTabs").doCommand();
},
remove: function ctrlTab_remove(aPreview) {
if (aPreview._tab)
gBrowser.removeTab(aPreview._tab);
},
attachTab: function ctrlTab_attachTab(aTab, aPos) {
if (aTab.closing)
return;
if (aPos == 0)
this._recentlyUsedTabs.unshift(aTab);
else if (aPos)
this._recentlyUsedTabs.splice(aPos, 0, aTab);
else
this._recentlyUsedTabs.push(aTab);
},
detachTab: function ctrlTab_detachTab(aTab) {
var i = this._recentlyUsedTabs.indexOf(aTab);
if (i >= 0)
this._recentlyUsedTabs.splice(i, 1);
},
open: function ctrlTab_open() {
if (this.isOpen)
return;
document.addEventListener("keyup", this, true);
this.updatePreviews();
this._selectedIndex = 1;
// Add a slight delay before showing the UI, so that a quick
// "ctrl-tab" keypress just flips back to the MRU tab.
this._timer = setTimeout(function (self) {
self._timer = null;
self._openPanel();
}, 200, this);
},
_openPanel: function ctrlTab_openPanel() {
tabPreviewPanelHelper.opening(this);
this.panel.width = Math.min(screen.availWidth * .99,
this.canvasWidth * 1.25 * this.tabPreviewCount);
var estimateHeight = this.canvasHeight * 1.25 + 75;
this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
screen.availTop + (screen.availHeight - estimateHeight) / 2,
false);
},
close: function ctrlTab_close(aTabToSelect) {
if (!this.isOpen)
return;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
this.suspendGUI();
if (aTabToSelect)
gBrowser.selectedTab = aTabToSelect;
return;
}
this.tabToSelect = aTabToSelect;
this.panel.hidePopup();
},
setupGUI: function ctrlTab_setupGUI() {
this.selected.focus();
this._selectedIndex = -1;
// Track mouse movement after a brief delay so that the item that happens
// to be under the mouse pointer initially won't be selected unintentionally.
this._trackMouseOver = false;
setTimeout(function (self) {
if (self.isOpen)
self._trackMouseOver = true;
}, 0, this);
},
suspendGUI: function ctrlTab_suspendGUI() {
document.removeEventListener("keyup", this, true);
for (let preview of this.previews) {
this.updatePreview(preview, null);
}
},
onKeyPress: function ctrlTab_onKeyPress(event) {
var isOpen = this.isOpen;
if (isOpen) {
event.preventDefault();
event.stopPropagation();
}
switch (event.keyCode) {
case event.DOM_VK_TAB:
if (event.ctrlKey && !event.altKey && !event.metaKey) {
if (isOpen) {
this.advanceFocus(!event.shiftKey);
} else if (!event.shiftKey) {
event.preventDefault();
event.stopPropagation();
let tabs = gBrowser.visibleTabs;
if (tabs.length > 2) {
this.open();
} else if (tabs.length == 2) {
let index = tabs[0].selected ? 1 : 0;
gBrowser.selectedTab = tabs[index];
}
}
}
break;
default:
if (isOpen && event.ctrlKey) {
if (event.keyCode == event.DOM_VK_DELETE) {
this.remove(this.selected);
break;
}
switch (event.charCode) {
case this.keys.close:
this.remove(this.selected);
break;
case this.keys.find:
case this.keys.selectAll:
this.showAllTabs();
break;
}
}
}
},
removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
if (this.tabCount == 2) {
this.close();
return;
}
this.updatePreviews();
if (this.selected.hidden)
this.advanceFocus(false);
if (this.selected == this.showAllButton)
this.advanceFocus(false);
// If the current tab is removed, another tab can steal our focus.
if (aTab.selected && this.panel.state == "open") {
setTimeout(function (selected) {
selected.focus();
}, 0, this.selected);
}
},
handleEvent: function ctrlTab_handleEvent(event) {
switch (event.type) {
case "SSWindowStateReady":
this._initRecentlyUsedTabs();
break;
case "TabAttrModified":
// tab attribute modified (e.g. label, crop, busy, image, selected)
for (let i = this.previews.length - 1; i >= 0; i--) {
if (this.previews[i]._tab && this.previews[i]._tab == event.target) {
this.updatePreview(this.previews[i], event.target);
break;
}
}
break;
case "TabSelect":
this.detachTab(event.target);
this.attachTab(event.target, 0);
break;
case "TabOpen":
this.attachTab(event.target, 1);
break;
case "TabClose":
this.detachTab(event.target);
if (this.isOpen)
this.removeClosingTabFromUI(event.target);
break;
case "keypress":
this.onKeyPress(event);
break;
case "keyup":
if (event.keyCode == event.DOM_VK_CONTROL)
this.pick();
break;
case "popupshowing":
if (event.target.id == "menu_viewPopup")
document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
break;
}
},
filterForThumbnailExpiration: function (aCallback) {
// Save a few more thumbnails than we actually display, so that when tabs
// are closed, the previews we add instead still get thumbnails.
const extraThumbnails = 3;
const thumbnailCount = Math.min(this.tabPreviewCount + extraThumbnails,
this.tabCount);
let urls = [];
for (let i = 0; i < thumbnailCount; i++)
urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
aCallback(urls);
},
_initRecentlyUsedTabs: function () {
this._recentlyUsedTabs =
Array.filter(gBrowser.tabs, tab => !tab.closing)
.sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
},
_init: function ctrlTab__init(enable) {
var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
window[toggleEventListener]("SSWindowStateReady", this, false);
var tabContainer = gBrowser.tabContainer;
tabContainer[toggleEventListener]("TabOpen", this, false);
tabContainer[toggleEventListener]("TabAttrModified", this, false);
tabContainer[toggleEventListener]("TabSelect", this, false);
tabContainer[toggleEventListener]("TabClose", this, false);
document[toggleEventListener]("keypress", this, false);
gBrowser.mTabBox.handleCtrlTab = !enable;
if (enable)
PageThumbs.addExpirationFilter(this);
else
PageThumbs.removeExpirationFilter(this);
// If we're not running, hide the "Show All Tabs" menu item,
// as Shift+Ctrl+Tab will be handled by the tab bar.
document.getElementById("menu_showAllTabs").hidden = !enable;
document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this);
// Also disable the <key> to ensure Shift+Ctrl+Tab never triggers
// Show All Tabs.
var key_showAllTabs = document.getElementById("key_showAllTabs");
if (enable)
key_showAllTabs.removeAttribute("disabled");
else
key_showAllTabs.setAttribute("disabled", "true");
}
};
/**
* All Tabs menu
*/
var allTabs = {
get toolbarButton() {
return document.getElementById("alltabs-button");
},
get canOpen() {
return isElementVisible(this.toolbarButton);
},
open: function allTabs_open() {
if (this.canOpen) {
// Without setTimeout, the menupopup won't stay open when invoking
// "View > Show All Tabs" and the menu bar auto-hides.
setTimeout(() => {
this.toolbarButton.open = true;
}, 0);
}
}
};