Bug 1908439 - Drag and drop for moving a group within the window. r=dwalker,tabbrowser-reviewers,sessionstore-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D239494
This commit is contained in:
Dão Gottwald
2025-03-06 22:20:21 +00:00
parent c998fff42f
commit 0fb65f4185
6 changed files with 194 additions and 91 deletions

View File

@@ -682,8 +682,12 @@
}
on_dragstart(event) {
if (this._isCustomizing) {
return;
}
var tab = this._getDragTargetTab(event);
if (!tab || this._isCustomizing) {
if (!tab) {
return;
}
@@ -711,34 +715,36 @@
}
let dataTransferOrderedTabs;
if (!fromTabList) {
if (fromTabList || isTabGroupLabel(tab)) {
// Dragging a group label or an item in the all tabs menu doesn't
// change the currently selected tabs, and it's not possible to select
// multiple tabs from the list, thus handle only the dragged tab in
// this case.
dataTransferOrderedTabs = [tab];
} else {
let selectedTabs = gBrowser.selectedTabs;
let otherSelectedTabs = selectedTabs.filter(
selectedTab => selectedTab != tab
);
dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
} else {
// Dragging an item in the tabs list doesn't change the currently
// selected tabs, and it's not possible to select multiple tabs from
// the list, thus handle only the dragged tab in this case.
dataTransferOrderedTabs = [tab];
}
let dt = event.dataTransfer;
for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
let dtTab = dataTransferOrderedTabs[i];
dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
let dtBrowser = dtTab.linkedBrowser;
if (isTab(dtTab)) {
let dtBrowser = dtTab.linkedBrowser;
// We must not set text/x-moz-url or text/plain data here,
// otherwise trying to detach the tab by dropping it on the desktop
// may result in an "internet shortcut"
dt.mozSetDataAt(
"text/x-moz-text-internal",
dtBrowser.currentURI.spec,
i
);
// We must not set text/x-moz-url or text/plain data here,
// otherwise trying to detach the tab by dropping it on the desktop
// may result in an "internet shortcut"
dt.mozSetDataAt(
"text/x-moz-text-internal",
dtBrowser.currentURI.spec,
i
);
}
}
// Set the cursor to an arrow during tab drags.
@@ -748,8 +754,20 @@
// node to deliver the `dragend` event. See bug 1345473.
dt.addElement(tab);
let expandedTabGroups;
if (tab.multiselected) {
this.#moveTogetherSelectedTabs(tab);
} else if (isTabGroupLabel(tab)) {
expandedTabGroups = gBrowser.tabGroups.filter(
group => !group.collapsed
);
if (expandedTabGroups.length) {
this._lockTabSizing();
this.#keepTabSizeLocked = true;
}
for (let group of expandedTabGroups) {
group.collapsed = true;
}
}
// Create a canvas to which we capture the current tab.
@@ -772,8 +790,10 @@
canvas.height = 90 * scale;
let toDrag = canvas;
let dragImageOffset = -16;
let browser = tab.linkedBrowser;
if (gMultiProcessBrowser) {
let browser = isTab(tab) && tab.linkedBrowser;
if (isTabGroupLabel(tab)) {
toDrag = document.getElementById("tab-drag-empty-feedback");
} else if (gMultiProcessBrowser) {
var context = canvas.getContext("2d");
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
@@ -851,6 +871,7 @@
),
fromTabList,
tabGroupCreationColor: gBrowser.tabGroupMenu.nextUnusedColor,
expandedTabGroups,
};
event.stopPropagation();
@@ -897,7 +918,7 @@
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if (
(effects == "move" || effects == "copy") &&
this == draggedTab.container &&
document == draggedTab.ownerDocument &&
!draggedTab._dragData.fromTabList
) {
ind.hidden = true;
@@ -1117,12 +1138,16 @@
if (shouldTranslate) {
let translationPromises = [];
for (let tab of movingTabs) {
for (let item of movingTabs) {
if (isTabGroupLabel(item)) {
// Shift the `.tab-group-label-container` to shift the label element.
item = item.parentElement;
}
let translationPromise = new Promise(resolve => {
tab.toggleAttribute("tabdrop-samewindow", true);
tab.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`;
item.toggleAttribute("tabdrop-samewindow", true);
item.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`;
let postTransitionCleanup = () => {
tab.removeAttribute("tabdrop-samewindow");
item.removeAttribute("tabdrop-samewindow");
resolve();
};
if (gReduceMotion) {
@@ -1131,15 +1156,15 @@
let onTransitionEnd = transitionendEvent => {
if (
transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != tab
transitionendEvent.originalTarget != item
) {
return;
}
tab.removeEventListener("transitionend", onTransitionEnd);
item.removeEventListener("transitionend", onTransitionEnd);
postTransitionCleanup();
};
tab.addEventListener("transitionend", onTransitionEnd);
item.addEventListener("transitionend", onTransitionEnd);
}
});
translationPromises.push(translationPromise);
@@ -1257,6 +1282,13 @@
}
if (draggedTab) {
if (draggedTab._dragData.expandedTabGroups?.length) {
for (let group of draggedTab._dragData.expandedTabGroups) {
group.collapsed = false;
}
this.#keepTabSizeLocked = true;
this._unlockTabSizing();
}
delete draggedTab._dragData;
}
}
@@ -1861,10 +1893,12 @@
selectedTab._notselectedsinceload = false;
}
#keepTabSizeLocked;
/**
* Try to keep the active tab's close button under the mouse cursor
*/
_lockTabSizing(aTab, aTabWidth) {
_lockTabSizing(aClosingTab, aTabWidth) {
if (this.verticalMode) {
return;
}
@@ -1874,17 +1908,23 @@
return;
}
var isEndTab = aTab._tPos > tabs.at(-1)._tPos;
let numPinned = gBrowser.pinnedTabCount;
let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-1)._tPos;
if (!this._tabDefaultMaxWidth) {
this._tabDefaultMaxWidth = parseFloat(
window.getComputedStyle(aTab).maxWidth
window.getComputedStyle(tabs[numPinned]).maxWidth
);
}
this._lastTabClosedByMouse = true;
this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
this.arrowScrollbox._scrollButtonDown
).width;
if (aTabWidth === undefined) {
aTabWidth = window.windowUtils.getBoundsWithoutFlushing(
tabs[numPinned]
).width;
}
if (this.overflowing) {
// Don't need to do anything if we're in overflow mode and aren't scrolled
@@ -1895,17 +1935,15 @@
// If the tab has an owner that will become the active tab, the owner will
// be to the left of it, so we actually want the left tab to slide over.
// This can't be done as easily in non-overflow mode, so we don't bother.
if (aTab.owner) {
if (aClosingTab?.owner) {
return;
}
this._expandSpacerBy(aTabWidth);
} else {
// non-overflow mode
// Locking is neither in effect nor needed, so let tabs expand normally.
} /* non-overflow mode */ else {
if (isEndTab && !this._hasTabTempMaxWidth) {
// Locking is neither in effect nor needed, so let tabs expand normally.
return;
}
let numPinned = gBrowser.pinnedTabCount;
// Force tabs to stay the same width, unless we're closing the last tab,
// which case we need to let them expand just enough so that the overall
// tabbar width is the same.
@@ -1955,6 +1993,10 @@
}
_unlockTabSizing() {
if (this.#keepTabSizeLocked) {
return;
}
gBrowser.removeEventListener("mousemove", this);
window.removeEventListener("mouseout", this);
@@ -2343,8 +2385,12 @@
let lastBound = endEdge(lastTab) - lastMovingTabScreen;
translate = Math.min(Math.max(translate, firstBound), lastBound);
for (let tab of movingTabs) {
tab.style.transform = `${translateAxis}(${translate}px)`;
for (let item of movingTabs) {
if (isTabGroupLabel(item)) {
// Shift the `.tab-group-label-container` to shift the label element.
item = item.parentElement;
}
item.style.transform = `${translateAxis}(${translate}px)`;
}
dragData.translatePos = translate;
@@ -2562,7 +2608,7 @@
}
}
if (gBrowser._tabGroupsEnabled && !isPinned) {
if (gBrowser._tabGroupsEnabled && isTab(draggedTab) && !isPinned) {
let dragOverGroupingThreshold = 1 - moveOverThreshold;
// When dragging tab(s) over an ungrouped tab, signal to the user
@@ -2902,7 +2948,10 @@
}
// fall through
case "mousemove":
if (document.getElementById("tabContextMenu").state != "open") {
if (
document.getElementById("tabContextMenu").state != "open" &&
!this.hasAttribute("movingtab")
) {
this._unlockTabSizing();
}
break;
@@ -3027,28 +3076,30 @@
*/
_getDragTargetTab(event, { ignoreTabSides = false } = {}) {
let { target } = event;
if (target.nodeType != Node.ELEMENT_NODE) {
target = target.parentElement;
while (target) {
if (isTab(target) || isTabGroupLabel(target)) {
break;
}
target = target.parentNode;
}
let tab = target?.closest("tab");
if (tab && ignoreTabSides) {
let { width, height } = tab.getBoundingClientRect();
if (target && ignoreTabSides) {
let { width, height } = target.getBoundingClientRect();
if (
event.screenX < tab.screenX + width * 0.25 ||
event.screenX > tab.screenX + width * 0.75 ||
((event.screenY < tab.screenY + height * 0.25 ||
event.screenY > tab.screenY + height * 0.75) &&
event.screenX < target.screenX + width * 0.25 ||
event.screenX > target.screenX + width * 0.75 ||
((event.screenY < target.screenY + height * 0.25 ||
event.screenY > target.screenY + height * 0.75) &&
this.verticalMode)
) {
return null;
}
}
return tab;
return target;
}
_getDropIndex(event) {
let tab = this._getDragTargetTab(event);
if (!tab) {
if (!isTab(tab)) {
return this.allTabs.length;
}
let isBeforeMiddle;
@@ -3080,12 +3131,10 @@
if (isMovingTabs) {
let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if (
XULElement.isInstance(sourceNode) &&
sourceNode.localName == "tab" &&
(isTab(sourceNode) || isTabGroupLabel(sourceNode)) &&
sourceNode.ownerGlobal.isChromeWindow &&
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
"navigator:browser" &&
sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
"navigator:browser"
) {
// Do not allow transfering a private tab to a non-private window
// and vice versa.