405 lines
11 KiB
JavaScript
405 lines
11 KiB
JavaScript
/* 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/. */
|
|
|
|
var { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PageWireframes: "resource:///modules/sessionstore/PageWireframes.sys.mjs",
|
|
});
|
|
|
|
const ZERO_DELAY_ACTIVATION_TIME = 300;
|
|
|
|
/**
|
|
* Detailed preview card that displays when hovering a tab
|
|
*/
|
|
export default class TabHoverPreviewPanel {
|
|
constructor(panel) {
|
|
this._panel = panel;
|
|
this._win = panel.ownerGlobal;
|
|
this._tab = null;
|
|
this._thumbnailElement = null;
|
|
|
|
// Observe changes to this tab's DOM, and
|
|
// update the preview if the tab title changes
|
|
this._tabObserver = new this._win.MutationObserver(
|
|
(mutationList, _observer) => {
|
|
for (const mutation of mutationList) {
|
|
if (mutation.attributeName === "label") {
|
|
this._updatePreview();
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
this._setExternalPopupListeners();
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"_prefDisableAutohide",
|
|
"ui.popup.disable_autohide",
|
|
false
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"_prefPreviewDelay",
|
|
"ui.tooltip.delay_ms"
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"_prefDisplayThumbnail",
|
|
"browser.tabs.hoverPreview.showThumbnails",
|
|
false
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"_prefCollectWireframes",
|
|
"browser.history.collectWireframes"
|
|
);
|
|
|
|
this._panelOpener = new TabPreviewPanelTimedFunction(
|
|
() => {
|
|
if (!this._isDisabled()) {
|
|
this._panel.openPopup(this._tab, this.#popupOptions);
|
|
}
|
|
},
|
|
this._prefPreviewDelay,
|
|
ZERO_DELAY_ACTIVATION_TIME,
|
|
this._win
|
|
);
|
|
}
|
|
|
|
get #verticalMode() {
|
|
return this._win.gBrowser.tabContainer.verticalMode;
|
|
}
|
|
|
|
get #popupOptions() {
|
|
if (!this.#verticalMode) {
|
|
return {
|
|
position: "bottomleft topleft",
|
|
x: 0,
|
|
y: -2,
|
|
};
|
|
}
|
|
if (!this._win.SidebarController._positionStart) {
|
|
return {
|
|
position: "topleft topright",
|
|
x: 0,
|
|
y: 3,
|
|
};
|
|
}
|
|
return {
|
|
position: "topright topleft",
|
|
x: 0,
|
|
y: 3,
|
|
};
|
|
}
|
|
|
|
getPrettyURI(uri) {
|
|
let url = URL.parse(uri);
|
|
if (!url) {
|
|
return uri;
|
|
}
|
|
|
|
if (url.protocol == "about:" && url.pathname == "reader") {
|
|
url = URL.parse(url.searchParams.get("url"));
|
|
}
|
|
|
|
if (url?.protocol === "about:") {
|
|
return url.href;
|
|
}
|
|
return url ? url.hostname.replace(/^w{3}\./, "") : uri;
|
|
}
|
|
|
|
_hasValidWireframeState(tab) {
|
|
return (
|
|
this._prefCollectWireframes &&
|
|
this._prefDisplayThumbnail &&
|
|
tab &&
|
|
!tab.selected &&
|
|
!!lazy.PageWireframes.getWireframeState(tab)
|
|
);
|
|
}
|
|
|
|
_hasValidThumbnailState(tab) {
|
|
return (
|
|
this._prefDisplayThumbnail &&
|
|
tab &&
|
|
tab.linkedBrowser &&
|
|
!tab.getAttribute("pending") &&
|
|
!tab.selected
|
|
);
|
|
}
|
|
|
|
_maybeRequestThumbnail() {
|
|
let tab = this._tab;
|
|
|
|
if (!this._hasValidThumbnailState(tab)) {
|
|
let wireframeElement = lazy.PageWireframes.getWireframeElementForTab(tab);
|
|
if (wireframeElement) {
|
|
this._thumbnailElement = wireframeElement;
|
|
this._updatePreview();
|
|
}
|
|
return;
|
|
}
|
|
let thumbnailCanvas = this._win.document.createElement("canvas");
|
|
thumbnailCanvas.width = 280 * this._win.devicePixelRatio;
|
|
thumbnailCanvas.height = 140 * this._win.devicePixelRatio;
|
|
|
|
this._win.PageThumbs.captureTabPreviewThumbnail(
|
|
tab.linkedBrowser,
|
|
thumbnailCanvas
|
|
)
|
|
.then(() => {
|
|
// in case we've changed tabs after capture started, ensure we still want to show the thumbnail
|
|
if (this._tab == tab && this._hasValidThumbnailState(tab)) {
|
|
this._thumbnailElement = thumbnailCanvas;
|
|
this._updatePreview();
|
|
}
|
|
})
|
|
.catch(e => {
|
|
// Most likely the window was killed before capture completed, so just log the error
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
activate(tab) {
|
|
if (this._isDisabled()) {
|
|
return;
|
|
}
|
|
|
|
this._tab = tab;
|
|
this._tabObserver.observe(this._tab, {
|
|
attributes: true,
|
|
});
|
|
|
|
// Calling `moveToAnchor` in advance of the call to `openPopup` ensures
|
|
// that race conditions can be avoided in cases where the user hovers
|
|
// over a different tab while the preview panel is still opening.
|
|
// This will ensure the move operation is carried out even if the popup is
|
|
// in an intermediary state (opening but not fully open).
|
|
//
|
|
// If the popup is closed this call will be ignored.
|
|
this._movePanel();
|
|
|
|
this._thumbnailElement = null;
|
|
this._maybeRequestThumbnail();
|
|
if (this._panel.state == "open" || this._panel.state == "showing") {
|
|
this._updatePreview();
|
|
}
|
|
this._panelOpener.execute();
|
|
this._win.addEventListener("TabSelect", this);
|
|
this._panel.addEventListener("popupshowing", this);
|
|
}
|
|
|
|
deactivate(leavingTab = null) {
|
|
if (leavingTab) {
|
|
if (this._tab != leavingTab) {
|
|
return;
|
|
}
|
|
this._win.requestAnimationFrame(() => {
|
|
if (this._tab == leavingTab) {
|
|
this.deactivate();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
this._tab = null;
|
|
this._tabObserver.disconnect();
|
|
this._thumbnailElement = null;
|
|
this._panel.removeEventListener("popupshowing", this);
|
|
this._win.removeEventListener("TabSelect", this);
|
|
if (!this._prefDisableAutohide) {
|
|
this._panel.hidePopup();
|
|
}
|
|
this._panelOpener.setZeroDelay();
|
|
}
|
|
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case "popupshowing":
|
|
this._updatePreview();
|
|
break;
|
|
case "TabSelect":
|
|
this.deactivate();
|
|
break;
|
|
}
|
|
}
|
|
|
|
_updatePreview() {
|
|
this._panel.querySelector(".tab-preview-title").textContent =
|
|
this._displayTitle;
|
|
this._panel.querySelector(".tab-preview-uri").textContent =
|
|
this._displayURI;
|
|
|
|
if (this._win.gBrowser.showPidAndActiveness) {
|
|
this._panel.querySelector(".tab-preview-pid").textContent =
|
|
this._displayPids;
|
|
this._panel.querySelector(".tab-preview-activeness").textContent =
|
|
this._displayActiveness;
|
|
} else {
|
|
this._panel.querySelector(".tab-preview-pid").textContent = "";
|
|
this._panel.querySelector(".tab-preview-activeness").textContent = "";
|
|
}
|
|
|
|
let thumbnailContainer = this._panel.querySelector(
|
|
".tab-preview-thumbnail-container"
|
|
);
|
|
thumbnailContainer.classList.toggle(
|
|
"hide-thumbnail",
|
|
!this._hasValidThumbnailState(this._tab) &&
|
|
!this._hasValidWireframeState(this._tab)
|
|
);
|
|
if (thumbnailContainer.firstChild != this._thumbnailElement) {
|
|
thumbnailContainer.replaceChildren();
|
|
if (this._thumbnailElement) {
|
|
thumbnailContainer.appendChild(this._thumbnailElement);
|
|
}
|
|
this._panel.dispatchEvent(
|
|
new CustomEvent("previewThumbnailUpdated", {
|
|
detail: {
|
|
thumbnail: this._thumbnailElement,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
this._movePanel();
|
|
}
|
|
|
|
_movePanel() {
|
|
if (this._tab) {
|
|
this._panel.moveToAnchor(
|
|
this._tab,
|
|
this.#popupOptions.position,
|
|
this.#popupOptions.x,
|
|
this.#popupOptions.y
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen for any panels or menupopups that open or close anywhere else in the DOM tree
|
|
* and maintain a list of the ones that are currently open.
|
|
* This is used to disable tab previews until such time as the other panels are closed.
|
|
*/
|
|
_setExternalPopupListeners() {
|
|
// Since the tab preview panel is lazy loaded, there is a possibility that panels could
|
|
// already be open on init. Therefore we need to initialize _openPopups with existing panels
|
|
// the first time.
|
|
const initialPopups = this._win.document.querySelectorAll(
|
|
"panel[panelopen=true]:not(#tab-preview-panel), panel[animating=true]:not(#tab-preview-panel), menupopup[open=true]"
|
|
);
|
|
this._openPopups = new Set(initialPopups);
|
|
|
|
const handleExternalPopupEvent = (eventName, setMethod) => {
|
|
this._win.addEventListener(eventName, ev => {
|
|
if (
|
|
ev.target !== this._panel &&
|
|
(ev.target.nodeName == "panel" || ev.target.nodeName == "menupopup")
|
|
) {
|
|
this._openPopups[setMethod](ev.target);
|
|
}
|
|
});
|
|
};
|
|
handleExternalPopupEvent("popupshowing", "add");
|
|
handleExternalPopupEvent("popuphiding", "delete");
|
|
}
|
|
|
|
_isDisabled() {
|
|
return (
|
|
// Other popups are open.
|
|
this._openPopups.size ||
|
|
// TODO (bug 1899556): for now disable in background windows, as there are
|
|
// issues with windows ordering on Linux (bug 1897475), plus intermittent
|
|
// persistence of previews after session restore (bug 1888148).
|
|
this._win != Services.focus.activeWindow
|
|
);
|
|
}
|
|
|
|
get _displayTitle() {
|
|
if (!this._tab) {
|
|
return "";
|
|
}
|
|
return this._tab.textLabel.textContent;
|
|
}
|
|
|
|
get _displayURI() {
|
|
if (!this._tab || !this._tab.linkedBrowser) {
|
|
return "";
|
|
}
|
|
return this.getPrettyURI(this._tab.linkedBrowser.currentURI.spec);
|
|
}
|
|
|
|
get _displayPids() {
|
|
const pids = this._win.gBrowser.getTabPids(this._tab);
|
|
if (!pids.length) {
|
|
return "";
|
|
}
|
|
|
|
let pidLabel = pids.length > 1 ? "pids" : "pid";
|
|
return `${pidLabel}: ${pids.join(", ")}`;
|
|
}
|
|
|
|
get _displayActiveness() {
|
|
return this._tab?.linkedBrowser?.docShellIsActive ? "[A]" : "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper that allows for delayed function execution, but with the
|
|
* ability to "zero" (i.e. cancel) the delay for a predetermined period
|
|
*/
|
|
class TabPreviewPanelTimedFunction {
|
|
constructor(target, delay, zeroDelayTime, win) {
|
|
this._target = target;
|
|
this._delay = delay;
|
|
this._zeroDelayTime = zeroDelayTime;
|
|
this._win = win;
|
|
|
|
this._timer = null;
|
|
this._useZeroDelay = false;
|
|
}
|
|
|
|
execute() {
|
|
if (this.delayActive) {
|
|
return;
|
|
}
|
|
|
|
// Always setting a timer, even in the situation where the
|
|
// delay is zero, seems to prevent a class of race conditions
|
|
// where multiple tabs are hovered in quick succession
|
|
this._timer = this._win.setTimeout(
|
|
() => {
|
|
this._timer = null;
|
|
this._target();
|
|
},
|
|
this._useZeroDelay ? 0 : this._delay
|
|
);
|
|
}
|
|
|
|
clear() {
|
|
if (this._timer) {
|
|
this._win.clearTimeout(this._timer);
|
|
this._timer = null;
|
|
}
|
|
}
|
|
|
|
setZeroDelay() {
|
|
this.clear();
|
|
|
|
if (this._useZeroDelay) {
|
|
return;
|
|
}
|
|
|
|
this._win.setTimeout(() => {
|
|
this._useZeroDelay = false;
|
|
}, this._zeroDelayTime);
|
|
this._useZeroDelay = true;
|
|
}
|
|
|
|
get delayActive() {
|
|
return this._timer !== null;
|
|
}
|
|
}
|