mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-02 09:18:36 +02:00
The "list all tabs" menu displayed tab groups and allowed drag/dropping tabs around, but an error would be thrown when trying to drag an entire tab group. This patch allows dragging tab groups in the "list all tabs" menu and dropping them elsewhere. This accounts for a number of edge cases: - drag tab group to the end of the menu - drag tab right before a tab group - drag tab right after a collapsed tab group - drag an ungrouped tab into the position of the first tab of a tab group The tab group menu items didn't have a `tab` property like the tab menu items. That property is what gets passed to the drag-drop code to do its work. I made `tab` and `tabGroup` properties available on the `toolbaritem` elements and included some helper functions to access them consistently. With that in place, tab groups would generally drag and drop OK, but the "list all tabs" menu wouldn't update. The menu needs to be rebuilt when a tab group moves, so I started listening for the `TabGroupMoved` event. TabsPanel._onDrop was using `_tPos` on the menu item's `tab`, but that isn't present on tab group labels. I tried modifying the logic to use the `elementIndex`, but that led to edge cases with `Tabbrowser.moveTabTo`. Since the TabsPanel was already tracking the drop direction, I switched to using moveTabBefore/moveTabAfter for simplicity. The TabsListBase._addTab method was still assuming that the tab strip was a linear list of tabs, so it wasn't doing great at handling the scenario of dropping a tab next to a tab group label. The drop could result in 1) no change, 2) tab moves correctly but it becomes the last item of the "list all tabs" menu, 3) console error. I switched to using the <tabs> element's `findNextTab`. I found an edge case where Tabbrowser.moveTabAfter would move a tab into the first position of a collapsed tab group instead of moving the tab after the whole tab group. I updated Tabbrowser.#moveTabNextTo for that case. I encountered an edge case when dragging a tab in "list all tabs" between two pinned tabs when the first item in the unpinned tab strip is a tab group. The dropped tab was being inserted into the group as the first grouped tab instead of dropping as an ungrouped tab. I added a general fix in Tabbrowser.#moveTabNextTo -- not sure if other code paths would be affected by this bug or just the "list all tabs" menu When we added tab groups to the "list all tabs" menu in bug 1908431, this had two side effects: 1. the hidden tabs menus would show tab group labels if a hidden tab was in an expanded tab group 2. hidden tabs would NOT show up in the hidden tabs menus if they were in a collapsed tab group Our team decided that (1) was desirable because it added helpful context to those hidden tabs. (2) is not desirable, so this patch tries to ensure that hidden tabs always appear in the different hidden tabs lists even if they are also in collapsed tab groups. For consistency with tabs in the "list all tabs" menu and consistency with the tab strip, middle-clicking on a tab group label in the "list all tabs" menu will now save and close the tab group. Differential Revision: https://phabricator.services.mozilla.com/D247031
230 lines
7 KiB
JavaScript
230 lines
7 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/. */
|
|
|
|
import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
|
|
import { TabMetrics } from "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs";
|
|
|
|
const MAX_INITIAL_ITEMS = 5;
|
|
|
|
export class GroupsPanel {
|
|
constructor({ view, containerNode, showAll = false }) {
|
|
this.view = view;
|
|
this.#showAll = showAll;
|
|
this.containerNode = containerNode;
|
|
this.win = containerNode.ownerGlobal;
|
|
this.doc = containerNode.ownerDocument;
|
|
this.panelMultiView = null;
|
|
this.view.addEventListener("ViewShowing", this);
|
|
}
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "ViewShowing":
|
|
if (event.target == this.view) {
|
|
this.panelMultiView = this.view.panelMultiView;
|
|
this.#populate();
|
|
this.#addObservers();
|
|
this.win.addEventListener("unload", this);
|
|
}
|
|
break;
|
|
case "PanelMultiViewHidden":
|
|
if ((this.panelMultiView = event.target)) {
|
|
this.#cleanup();
|
|
this.#removeObservers();
|
|
this.panelMultiView = null;
|
|
}
|
|
break;
|
|
case "unload":
|
|
if (this.panelMultiView) {
|
|
this.#removeObservers();
|
|
}
|
|
break;
|
|
case "command":
|
|
this.#handleCommand(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#addObservers() {
|
|
Services.obs.addObserver(this, "sessionstore-closed-objects-changed");
|
|
Services.obs.addObserver(this, "browser-tabgroup-removed-from-dom");
|
|
}
|
|
|
|
#removeObservers() {
|
|
Services.obs.removeObserver(this, "sessionstore-closed-objects-changed");
|
|
Services.obs.removeObserver(this, "browser-tabgroup-removed-from-dom");
|
|
}
|
|
|
|
observe(aSubject, aTopic) {
|
|
switch (aTopic) {
|
|
case "sessionstore-closed-objects-changed":
|
|
case "browser-tabgroup-removed-from-dom":
|
|
this.#cleanup();
|
|
this.#populate();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#handleCommand(event) {
|
|
let { tabGroupId, command } = event.target.dataset;
|
|
|
|
switch (command) {
|
|
case "allTabsGroupView_selectGroup": {
|
|
let group = this.win.gBrowser.getTabGroupById(tabGroupId);
|
|
group.select();
|
|
group.ownerGlobal.focus();
|
|
break;
|
|
}
|
|
|
|
case "allTabsGroupView_restoreGroup":
|
|
this.win.SessionStore.openSavedTabGroup(tabGroupId, this.win, {
|
|
source: TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
#setupListeners() {
|
|
this.view.addEventListener("command", this);
|
|
this.panelMultiView.addEventListener("PanelMultiViewHidden", this);
|
|
}
|
|
|
|
#cleanup() {
|
|
this.containerNode.innerHTML = "";
|
|
this.view.removeEventListener("command", this);
|
|
}
|
|
|
|
#showAll;
|
|
#populate() {
|
|
let fragment = this.doc.createDocumentFragment();
|
|
|
|
let openGroups = this.win.gBrowser.getAllTabGroups({
|
|
sortByLastSeenActive: true,
|
|
});
|
|
let savedGroups = [];
|
|
if (!PrivateBrowsingUtils.isWindowPrivate(this.win)) {
|
|
savedGroups = this.win.SessionStore.savedGroups.toSorted(
|
|
(group1, group2) => group2.closedAt - group1.closedAt
|
|
);
|
|
}
|
|
|
|
let totalItemCount = savedGroups.length + openGroups.length;
|
|
if (totalItemCount && !this.#showAll) {
|
|
let header = this.doc.createElement("h2");
|
|
header.setAttribute("class", "subview-subheader");
|
|
this.doc.l10n.setAttributes(
|
|
header,
|
|
"all-tabs-menu-recent-tab-groups-header"
|
|
);
|
|
fragment.appendChild(header);
|
|
}
|
|
|
|
let addShowAllButton = !this.#showAll && totalItemCount > MAX_INITIAL_ITEMS;
|
|
let itemCount = addShowAllButton ? 1 : 0;
|
|
for (let groupData of openGroups) {
|
|
if (itemCount >= MAX_INITIAL_ITEMS && !this.#showAll) {
|
|
break;
|
|
}
|
|
itemCount++;
|
|
let row = this.#createRow(groupData);
|
|
let button = row.querySelector("toolbarbutton");
|
|
button.dataset.command = "allTabsGroupView_selectGroup";
|
|
button.setAttribute("context", "open-tab-group-context-menu");
|
|
fragment.appendChild(row);
|
|
}
|
|
|
|
for (let groupData of savedGroups) {
|
|
if (itemCount >= MAX_INITIAL_ITEMS && !this.#showAll) {
|
|
break;
|
|
}
|
|
itemCount++;
|
|
let row = this.#createRow(groupData, { isOpen: false });
|
|
let button = row.querySelector("toolbarbutton");
|
|
button.dataset.command = "allTabsGroupView_restoreGroup";
|
|
button.classList.add("all-tabs-group-saved-group");
|
|
button.setAttribute("context", "saved-tab-group-context-menu");
|
|
fragment.appendChild(row);
|
|
}
|
|
|
|
if (addShowAllButton) {
|
|
let button = this.doc.createXULElement("toolbarbutton");
|
|
button.setAttribute("id", "allTabsMenu-groupsViewShowMore");
|
|
button.setAttribute("class", "subviewbutton subviewbutton-nav");
|
|
button.setAttribute("closemenu", "none");
|
|
button.setAttribute("flex", "1");
|
|
this.doc.l10n.setAttributes(button, "all-tabs-menu-tab-groups-show-all");
|
|
fragment.appendChild(button);
|
|
}
|
|
|
|
this.containerNode.replaceChildren(fragment);
|
|
this.#setupListeners();
|
|
}
|
|
|
|
/**
|
|
* @param {TabGroupStateData} group
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.isOpen]
|
|
* Set to true if the group is currently open, and false if it's saved
|
|
* @returns {XULElement}
|
|
*/
|
|
#createRow(group, { isOpen = true } = {}) {
|
|
let { doc } = this;
|
|
let row = doc.createXULElement("toolbaritem");
|
|
row.setAttribute("class", "all-tabs-item all-tabs-group-item");
|
|
|
|
row.style.setProperty(
|
|
"--tab-group-color",
|
|
`var(--tab-group-color-${group.color})`
|
|
);
|
|
row.style.setProperty(
|
|
"--tab-group-color-invert",
|
|
`var(--tab-group-color-${group.color}-invert)`
|
|
);
|
|
row.style.setProperty(
|
|
"--tab-group-color-pale",
|
|
`var(--tab-group-color-${group.color}-pale)`
|
|
);
|
|
let button = doc.createXULElement("toolbarbutton");
|
|
button.setAttribute(
|
|
"class",
|
|
"all-tabs-button subviewbutton subviewbutton-iconic all-tabs-group-action-button"
|
|
);
|
|
button.dataset.tabGroupId = group.id;
|
|
if (!isOpen) {
|
|
button.classList.add(
|
|
"all-tabs-group-saved-group",
|
|
"tab-group-icon-closed"
|
|
);
|
|
button.dataset.command = "allTabsGroupView_restoreGroup";
|
|
} else {
|
|
button.classList.add("tab-group-icon");
|
|
button.dataset.command = "allTabsGroupView_selectGroup";
|
|
}
|
|
button.setAttribute("flex", "1");
|
|
button.setAttribute("crop", "end");
|
|
|
|
let setName = tabGroupName => {
|
|
if (group.saved) {
|
|
doc.l10n.setAttributes(button, "tabbrowser-manager-closed-tab-group", {
|
|
tabGroupName,
|
|
});
|
|
} else {
|
|
button.setAttribute("label", tabGroupName);
|
|
button.setAttribute("tooltiptext", tabGroupName);
|
|
}
|
|
};
|
|
|
|
if (group.name) {
|
|
setName(group.name);
|
|
} else {
|
|
doc.l10n
|
|
.formatValues([{ id: "tab-group-name-default" }])
|
|
.then(([msg]) => {
|
|
setName(msg);
|
|
});
|
|
}
|
|
row.appendChild(button);
|
|
return row;
|
|
}
|
|
}
|