Files
tubestation/browser/components/tabbrowser/content/tab-hover-preview.mjs
Brian Grinstead f6bb34aa25 Bug 1925724 - Use page wireframe for tab hover preview as a fallback when a normal thumnail is not available;r=mconley,sessionstore-reviewers,tabbrowser-reviewers,farre
This will only work when the pref is enabled (browser.history.collectWireframes), and is being landed primarily
to allow some expanded testing to see if it's worth cleaning this up further and turning it on.

Differential Revision: https://phabricator.services.mozilla.com/D226080
2024-10-24 03:34:40 +00:00

410 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,
"_prefShowPidAndActiveness",
"browser.tabs.tooltipsShowPidAndActiveness",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefCollectWireframes",
"browser.history.collectWireframes"
);
this._panelOpener = new TabPreviewPanelTimedFunction(
() => {
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: 4,
};
}
return {
position: "topright topleft",
x: 0,
y: 4,
};
}
getPrettyURI(uri) {
try {
let url = new URL(uri);
if (url.protocol == "about:" && url.pathname == "reader") {
url = new URL(url.searchParams.get("url"));
}
if (url.protocol === "about:") {
return url.href;
}
return `${url.hostname}`.replace(/^w{3}\./, "");
} catch {
return 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._prefShowPidAndActiveness) {
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;
}
}