diff --git a/browser/base/content/browser-box.inc.xhtml b/browser/base/content/browser-box.inc.xhtml
index afa7f8e7dd74..7f27e13a4463 100644
--- a/browser/base/content/browser-box.inc.xhtml
+++ b/browser/base/content/browser-box.inc.xhtml
@@ -6,6 +6,10 @@
+
diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js
index a5095e61b838..d5964680186f 100644
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -1510,6 +1510,8 @@ var PlacesControllerDragHelper = {
* @returns {string} The most relevant flavor, or undefined.
*/
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.
flavors = Array.from(flavors);
return PlacesUIUtils.SUPPORTED_FLAVORS.find(f => flavors.includes(f));
@@ -1526,6 +1528,8 @@ var PlacesControllerDragHelper = {
* @returns {boolean}
*/
canDrop: function PCDH_canDrop(ip, dt) {
+ if (dt.mozTypesAt(0).contains(PlacesUtils.TYPE_X_WS_TREE))
+ return true;
let dropCount = dt.mozItemCount;
// Check every dragged item.
diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js
index 597da4246167..00f3141a7924 100644
--- a/browser/components/sidebar/browser-sidebar.js
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -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.
*/
@@ -760,6 +919,7 @@ var SidebarController = {
if (content && content.updatePosition) {
content.updatePosition();
}
+ this.treeVerticalTabsBrowser?.reload();
},
/**
@@ -857,6 +1017,10 @@ var SidebarController = {
return;
}
+ if (this.treeVerticalTabsEnabled) {
+ this.toggleTreeVerticalTabs();
+ }
+
let sourceWindow = window.opener;
// 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
@@ -1979,6 +2143,9 @@ var SidebarController = {
arrowScrollbox.setAttribute("orient", "horizontal");
tabStrip.removeAttribute("expanded");
tabStrip.setAttribute("orient", "horizontal");
+ if (this.treeVerticalTabsEnabled) {
+ this.toggleTreeVerticalTabs(false);
+ }
}
let verticalToolbar = document.getElementById(
@@ -2307,3 +2474,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
}
}
);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarController,
+ "treeVerticalTabsEnabled",
+ "browser.sidebar.enabled",
+ false,
+ (_aPreference, _previousValue, newValue) => {
+ SidebarController.toggleTreeVerticalTabs(newValue);
+ }
+);
diff --git a/browser/components/sidebar/sidebar-customize.html b/browser/components/sidebar/sidebar-customize.html
index 3996bae0ea6e..70014e75775f 100644
--- a/browser/components/sidebar/sidebar-customize.html
+++ b/browser/components/sidebar/sidebar-customize.html
@@ -14,6 +14,7 @@
+
+
`
)}
@@ -356,6 +365,11 @@ export class SidebarCustomize extends SidebarPage {
});
}
+ #handleTreeVerticalTabsChange(e) {
+ e.stopPropagation();
+ this.getWindow().SidebarController.toggleTreeVerticalTabs(e.target.checked);
+ }
+
#toggleExpandOnHover(e) {
e.stopPropagation();
if (e.target.checked) {
diff --git a/browser/components/sidebar/sidebar-main.mjs b/browser/components/sidebar/sidebar-main.mjs
index 0b6d809f0a10..42a5bca380a9 100644
--- a/browser/components/sidebar/sidebar-main.mjs
+++ b/browser/components/sidebar/sidebar-main.mjs
@@ -132,6 +132,9 @@ export default class SidebarMain extends MozLitElement {
}
onSidebarPopupShowing(event) {
+ if (event.target == SidebarController.treeVerticalTabsBrowser) {
+ return;
+ }
// Store the context menu target which holds the id required for managing sidebar items
let targetHost = event.explicitOriginalTarget.getRootNode().host;
let toolbarContextMenuTarget =
diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs
index 3280a391bd80..05259df4702c 100644
--- a/browser/components/tabbrowser/content/tab-hover-preview.mjs
+++ b/browser/components/tabbrowser/content/tab-hover-preview.mjs
@@ -62,7 +62,14 @@ export default class TabHoverPreviewPanel {
this._panelOpener = new TabPreviewPanelTimedFunction(
() => {
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,
@@ -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) {
if (this._isDisabled()) {
return;
@@ -270,6 +289,12 @@ export default class TabHoverPreviewPanel {
_movePanel() {
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._tab,
this.#popupOptions.position,
diff --git a/toolkit/components/places/PlacesUtils.sys.mjs b/toolkit/components/places/PlacesUtils.sys.mjs
index d11573403881..5f779b00eadf 100644
--- a/toolkit/components/places/PlacesUtils.sys.mjs
+++ b/toolkit/components/places/PlacesUtils.sys.mjs
@@ -430,6 +430,9 @@ export var PlacesUtils = {
// Used to track the action that populated the clipboard.
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.
LMANNO_FEEDURI: "livemark/feedURI",
LMANNO_SITEURI: "livemark/siteURI",
@@ -1305,6 +1308,17 @@ export var PlacesUtils = {
}
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:
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}