feat(sidebar): part 2 - tree vertical tabs v1.1

This commit is contained in:
Alex Kontos
2025-08-04 12:34:24 +01:00
parent 8678621548
commit 6614ff9a2f
8 changed files with 243 additions and 1 deletions

View File

@@ -6,6 +6,10 @@
<box context="sidebar-context-menu" id="sidebar-main" hidden="true"> <box context="sidebar-context-menu" id="sidebar-main" hidden="true">
<html:sidebar-main flex="1"> <html:sidebar-main flex="1">
<box id="vertical-tabs" slot="tabstrip" customizable="true" contextmenu="toolbar-context-menu"></box> <box id="vertical-tabs" slot="tabstrip" customizable="true" contextmenu="toolbar-context-menu"></box>
<box id="tree-vertical-tabs-box"
slot="tabstrip"
orient="vertical"
hidden="true"/>
</html:sidebar-main> </html:sidebar-main>
</box> </box>
<splitter id="sidebar-launcher-splitter" class="chromeclass-extrachrome sidebar-splitter" resizebefore="sibling" resizeafter="none" hidden="true"/> <splitter id="sidebar-launcher-splitter" class="chromeclass-extrachrome sidebar-splitter" resizebefore="sibling" resizeafter="none" hidden="true"/>

View File

@@ -1510,6 +1510,8 @@ var PlacesControllerDragHelper = {
* @returns {string} The most relevant flavor, or undefined. * @returns {string} The most relevant flavor, or undefined.
*/ */
getMostRelevantFlavor(flavors) { getMostRelevantFlavor(flavors) {
if (flavors.contains(PlacesUtils.TYPE_X_WS_TREE))
return PlacesUtils.TYPE_X_WS_TREE;
// The DnD API returns a DOMStringList, but tests may pass an Array. // The DnD API returns a DOMStringList, but tests may pass an Array.
flavors = Array.from(flavors); flavors = Array.from(flavors);
return PlacesUIUtils.SUPPORTED_FLAVORS.find(f => flavors.includes(f)); return PlacesUIUtils.SUPPORTED_FLAVORS.find(f => flavors.includes(f));
@@ -1526,6 +1528,8 @@ var PlacesControllerDragHelper = {
* @returns {boolean} * @returns {boolean}
*/ */
canDrop: function PCDH_canDrop(ip, dt) { canDrop: function PCDH_canDrop(ip, dt) {
if (dt.mozTypesAt(0).contains(PlacesUtils.TYPE_X_WS_TREE))
return true;
let dropCount = dt.mozItemCount; let dropCount = dt.mozItemCount;
// Check every dragged item. // Check every dragged item.

View File

@@ -580,6 +580,165 @@ var SidebarController = {
); );
}, },
_toggleTreeInProgress: false,
/**
* Toggle the vertical tabs preference.
*/
async toggleTreeVerticalTabs(shouldShow) {
// Using a lock to prevent re-entrancy from the preference observer
// while the function is already being executed from the UI event.
if (this._toggleTreeInProgress) {
return;
}
this._toggleTreeInProgress = true;
try {
const treeVerticalTabsBox = document.querySelector(
"#tree-vertical-tabs-box"
);
const verticalTabs = document.querySelector("#vertical-tabs");
shouldShow ??= !this.treeVerticalTabsBrowser;
const addon = await AddonManager.getAddonByID("sidebar@waterfox.net");
if (shouldShow != this.treeVerticalTabsEnabled) {
Services.prefs.setBoolPref("browser.sidebar.enabled", shouldShow);
}
if (shouldShow) {
if (this.treeVerticalTabsBrowser) {
return;
}
if (!addon.isActive) {
await addon.enable({ allowSystemAddons: true });
}
await addon.startupPromise;
if (!Services.prefs.getBoolPref("sidebar.revamp")) {
await this.toggleRevampSidebar();
}
if (!Services.prefs.getBoolPref("sidebar.verticalTabs")) {
Services.prefs.setBoolPref("sidebar.verticalTabs", true);
// The sidebar-main maybe not initialized yet at the first time.
while (!this.sidebarMain.requestUpdate) {
await new Promise(resolve => setTimeout(resolve, 100));
}
this.toggleTabstrip();
}
treeVerticalTabsBox.removeAttribute("hidden");
treeVerticalTabsBox.setAttribute("shown", true);
verticalTabs.setAttribute("hidden", true);
verticalTabs.removeAttribute("visible");
const policy = WebExtensionPolicy.getByID("sidebar@waterfox.net");
const treeVerticalTabs = document.createXULElement("browser");
treeVerticalTabs.setAttribute("id", "tree-vertical-tabs");
treeVerticalTabs.setAttribute("type", "content");
treeVerticalTabs.setAttribute("flex", "1");
treeVerticalTabs.setAttribute("disableglobalhistory", "true");
treeVerticalTabs.setAttribute(
"messagemanagergroup",
"webext-browsers"
);
treeVerticalTabs.setAttribute("webextension-view-type", "sidebar");
treeVerticalTabs.setAttribute("context", "contentAreaContextMenu");
treeVerticalTabs.setAttribute("tooltip", "aHTMLTooltip");
treeVerticalTabs.setAttribute("autocompletepopup", "PopupAutoComplete");
treeVerticalTabs.setAttribute("transparent", "true");
treeVerticalTabs.setAttribute("remote", "true");
treeVerticalTabs.setAttribute("remoteType", "extension");
treeVerticalTabs.setAttribute("maychangeremoteness", "true");
treeVerticalTabs.setAttribute("remoteType", "extension");
treeVerticalTabs.setAttribute("transparent", "true");
treeVerticalTabs.setAttribute(
"initialBrowsingContextGroupId",
policy.browsingContextGroupId
);
const { ExtensionUtils } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionUtils.sys.mjs"
);
const { promiseEvent } = ExtensionUtils;
const initBrowser = () => {
const { ExtensionParent } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionParent.sys.mjs"
);
ExtensionParent.apiManager.emit(
"extension-browser-inserted",
treeVerticalTabs,
{}
);
treeVerticalTabs.messageManager.loadFrameScript(
"chrome://extensions/content/ext-browser-content.js",
false,
true
);
treeVerticalTabs.messageManager.sendAsyncMessage(
"Extension:InitBrowser",
{}
);
const sidebarPanelUrl = policy.getURL("sidebar/sidebar.html");
const uri = Services.io.newURI(sidebarPanelUrl);
const triggeringPrincipal =
Services.scriptSecurityManager.createContentPrincipal(uri, {});
treeVerticalTabs.fixupAndLoadURIString(sidebarPanelUrl, {
triggeringPrincipal,
});
};
promiseEvent(treeVerticalTabs, "XULFrameLoaderCreated").then(
initBrowser
);
treeVerticalTabs.addEventListener(
"DidChangeBrowserRemoteness",
initBrowser
);
treeVerticalTabsBox.appendChild(treeVerticalTabs);
const event = new CustomEvent("TreeVerticalTabsShown", {
bubbles: true,
});
treeVerticalTabsBox.dispatchEvent(event);
} else {
const treeVerticalTabs = this.treeVerticalTabsBrowser;
if (!treeVerticalTabs) {
return;
}
const uri = Services.io.newURI("about:blank");
const triggeringPrincipal =
Services.scriptSecurityManager.createContentPrincipal(uri, {});
treeVerticalTabs.fixupAndLoadURIString("about:blank", {
triggeringPrincipal,
});
treeVerticalTabsBox.removeChild(treeVerticalTabs);
treeVerticalTabsBox.setAttribute("hidden", true);
treeVerticalTabsBox.removeAttribute("shown");
verticalTabs.removeAttribute("hidden");
if (Services.prefs.getBoolPref("sidebar.verticalTabs")) {
verticalTabs.setAttribute("visible", "");
}
const event = new CustomEvent("TreeVerticalTabsHidden", {
bubbles: true,
});
treeVerticalTabsBox.dispatchEvent(event);
if (!shouldShow && addon.isActive) {
await addon.disable({ allowSystemAddons: true });
}
}
} finally {
this._toggleTreeInProgress = false;
}
},
get treeVerticalTabsBrowser() {
return document.querySelector("#tree-vertical-tabs");
},
/** /**
* The handler for Services.obs.addObserver. * The handler for Services.obs.addObserver.
*/ */
@@ -760,6 +919,7 @@ var SidebarController = {
if (content && content.updatePosition) { if (content && content.updatePosition) {
content.updatePosition(); content.updatePosition();
} }
this.treeVerticalTabsBrowser?.reload();
}, },
/** /**
@@ -857,6 +1017,10 @@ var SidebarController = {
return; return;
} }
if (this.treeVerticalTabsEnabled) {
this.toggleTreeVerticalTabs();
}
let sourceWindow = window.opener; let sourceWindow = window.opener;
// No source window means this is the initial window. If we're being // No source window means this is the initial window. If we're being
// opened from another window, check that it is one we might open a sidebar // opened from another window, check that it is one we might open a sidebar
@@ -1979,6 +2143,9 @@ var SidebarController = {
arrowScrollbox.setAttribute("orient", "horizontal"); arrowScrollbox.setAttribute("orient", "horizontal");
tabStrip.removeAttribute("expanded"); tabStrip.removeAttribute("expanded");
tabStrip.setAttribute("orient", "horizontal"); tabStrip.setAttribute("orient", "horizontal");
if (this.treeVerticalTabsEnabled) {
this.toggleTreeVerticalTabs(false);
}
} }
let verticalToolbar = document.getElementById( let verticalToolbar = document.getElementById(
@@ -2307,3 +2474,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
} }
} }
); );
XPCOMUtils.defineLazyPreferenceGetter(
SidebarController,
"treeVerticalTabsEnabled",
"browser.sidebar.enabled",
false,
(_aPreference, _previousValue, newValue) => {
SidebarController.toggleTreeVerticalTabs(newValue);
}
);

View File

@@ -14,6 +14,7 @@
<title data-l10n-id="sidebar-customize-title"></title> <title data-l10n-id="sidebar-customize-title"></title>
<link rel="localization" href="branding/brand.ftl" /> <link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/sidebar.ftl" /> <link rel="localization" href="browser/sidebar.ftl" />
<link rel="localization" href="browser/waterfox.ftl" />
<link <link
rel="stylesheet" rel="stylesheet"
href="chrome://browser/content/sidebar/sidebar.css" href="chrome://browser/content/sidebar/sidebar.css"

View File

@@ -296,6 +296,15 @@ export class SidebarCustomize extends SidebarPage {
?disabled=${this.getWindow().SidebarController._state ?disabled=${this.getWindow().SidebarController._state
.revampVisibility === "expand-on-hover"} .revampVisibility === "expand-on-hover"}
></moz-checkbox> ></moz-checkbox>
<moz-checkbox
slot="nested"
type="checkbox"
id="tree-vertical-tabs"
name="treeVerticalTabs"
data-l10n-id="sidebar-tree-vertical-tabs"
@change=${this.#handleTreeVerticalTabsChange}
?checked=${this.getWindow().SidebarController.treeVerticalTabsEnabled}
></moz-checkbox>
` `
)} )}
</moz-checkbox> </moz-checkbox>
@@ -356,6 +365,11 @@ export class SidebarCustomize extends SidebarPage {
}); });
} }
#handleTreeVerticalTabsChange(e) {
e.stopPropagation();
this.getWindow().SidebarController.toggleTreeVerticalTabs(e.target.checked);
}
#toggleExpandOnHover(e) { #toggleExpandOnHover(e) {
e.stopPropagation(); e.stopPropagation();
if (e.target.checked) { if (e.target.checked) {

View File

@@ -132,6 +132,9 @@ export default class SidebarMain extends MozLitElement {
} }
onSidebarPopupShowing(event) { onSidebarPopupShowing(event) {
if (event.target == SidebarController.treeVerticalTabsBrowser) {
return;
}
// Store the context menu target which holds the id required for managing sidebar items // Store the context menu target which holds the id required for managing sidebar items
let targetHost = event.explicitOriginalTarget.getRootNode().host; let targetHost = event.explicitOriginalTarget.getRootNode().host;
let toolbarContextMenuTarget = let toolbarContextMenuTarget =

View File

@@ -62,7 +62,14 @@ export default class TabHoverPreviewPanel {
this._panelOpener = new TabPreviewPanelTimedFunction( this._panelOpener = new TabPreviewPanelTimedFunction(
() => { () => {
if (!this._isDisabled()) { if (!this._isDisabled()) {
this._panel.openPopup(this._tab, this.#popupOptions); if (this._win.SidebarController.treeVerticalTabsEnabled &&
typeof this.__ws__top == 'number') {
const [x, y] = this.calculateCoordinatesForVertical();
this._panel.openPopupAtScreen(x, y, false);
}
else {
this._panel.openPopup(this._tab, this.#popupOptions);
}
} }
}, },
this._prefPreviewDelay, this._prefPreviewDelay,
@@ -165,6 +172,18 @@ export default class TabHoverPreviewPanel {
}); });
} }
calculateCoordinatesForVertical() {
const treeVerticalTabs = this._win.document.querySelector('#tree-vertical-tabs');
const treeVerticalTabsRect = treeVerticalTabs.getBoundingClientRect();
const align = Services.prefs.getBoolPref('sidebar.position_start') ? 'left' : 'right';
const previewRect = this._panel.getBoundingClientRect();
const x = align == 'left' ?
treeVerticalTabs.screenX + treeVerticalTabsRect.width - 2 :
treeVerticalTabs.screenX - previewRect.width + 2;
const y = Math.min(treeVerticalTabs.screenY + this.__ws__top, treeVerticalTabs.screenY + treeVerticalTabsRect.height - previewRect.height);
return [x, y];
}
activate(tab) { activate(tab) {
if (this._isDisabled()) { if (this._isDisabled()) {
return; return;
@@ -270,6 +289,12 @@ export default class TabHoverPreviewPanel {
_movePanel() { _movePanel() {
if (this._tab) { if (this._tab) {
if (this._win.SidebarController.treeVerticalTabsEnabled &&
typeof this.__ws__top == 'number') {
const [x, y] = this.calculateCoordinatesForVertical();
this._panel.moveTo(x, y);
return;
}
this._panel.moveToAnchor( this._panel.moveToAnchor(
this._tab, this._tab,
this.#popupOptions.position, this.#popupOptions.position,

View File

@@ -430,6 +430,9 @@ export var PlacesUtils = {
// Used to track the action that populated the clipboard. // Used to track the action that populated the clipboard.
TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action", TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
// Used to track actouns from the Tree Vertical Tabs sidebar.
TYPE_X_WS_TREE: "application/x-ws-tree",
// Deprecated: Remaining only for supporting migration of old livemarks. // Deprecated: Remaining only for supporting migration of old livemarks.
LMANNO_FEEDURI: "livemark/feedURI", LMANNO_FEEDURI: "livemark/feedURI",
LMANNO_SITEURI: "livemark/siteURI", LMANNO_SITEURI: "livemark/siteURI",
@@ -1305,6 +1308,17 @@ export var PlacesUtils = {
} }
break; break;
} }
case this.TYPE_X_WS_TREE: {
const data = JSON.parse(blob);
for (const tab of data.tabs) {
validNodes.push({
uri: tab.url,
title: tab.title,
type: "text/x-moz-url",
});
}
break;
}
default: default:
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
} }