Files
tubestation/waterfox/browser/components/sidebar/background/handle-moved-tabs.js
2025-11-06 14:13:52 +00:00

297 lines
9.8 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/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
wait,
isMacOS,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import * as Commands from './commands.js';
import * as NativeTabGroups from './native-tab-groups.js';
import * as TabsGroup from './tabs-group.js';
import * as Tree from './tree.js';
import * as TreeStructure from './tree-structure.js';
function log(...args) {
internalLogger('background/handle-moved-tabs', ...args);
}
function logApiTabs(...args) {
internalLogger('common/api-tabs', ...args);
}
let mMaybeTabMovingByShortcut = false;
Tab.onCreated.addListener((tab, info = {}) => {
if (!info.mayBeReplacedWithContainer &&
(info.duplicated ||
info.restored ||
info.skipFixupTree ||
// do nothing for already attached tabs
(tab.openerTabId &&
tab.$TST.parent == Tab.get(tab.openerTabId)))) {
log('skip to fixup tree for replaced/duplicated/restored tab ', tab, info);
return;
}
// if the tab is opened inside existing tree by someone, we must fixup the tree.
if (!(info.positionedBySelf ||
info.movedBySelfWhileCreation) &&
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab ||
(info.treeForActionDetection?.target &&
(info.treeForActionDetection.target.next ||
info.treeForActionDetection.target.previous)))) {
tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: Tab.getLastTab(tab.windowId).index,
treeForActionDetection: info.treeForActionDetection,
isTabCreating: true
});
}
else {
log('no need to fixup tree for newly created tab ', tab, info);
}
});
Tab.onMoving.addListener((tab, moveInfo) => {
// avoid TabMove produced by browser.tabs.insertRelatedAfterCurrent=true or something.
const win = TabsStore.windows.get(tab.windowId);
const isNewlyOpenedTab = win.openingTabs.has(tab.id);
const positionControlled = configs.insertNewChildAt != Constants.kINSERT_NO_CONTROL;
if (!isNewlyOpenedTab ||
!positionControlled ||
moveInfo.byInternalOperation ||
moveInfo.alreadyMoved ||
!moveInfo.movedInBulk)
return true;
// if there is no valid opener, it can be a restored initial tab in a restored window
// and can be just moved as a part of window restoration process.
if (!tab.$TST.openerTab)
return true;
log('onTabMove for new child tab: move back '+moveInfo.toIndex+' => '+moveInfo.fromIndex);
moveBack(tab, moveInfo);
return false;
});
async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) {
const internalGroupMoveCount = NativeTabGroups.internallyMovingNativeTabGroups.get(tab.groupId);
if (internalGroupMoveCount) {
log('ignore internal move of tab groups ', internalGroupMoveCount);
return;
}
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
...moveInfo,
});
log('tryFixupTreeForInsertedTab ', {
tab: tab.id,
parentTabOperationBehavior,
moveInfo,
childIds: tab.$TST.childIds,
parentId: tab.$TST.parentId,
});
if (!moveInfo.isTabCreating &&
parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
if (tab.$TST.hasChild)
tab.$TST.temporaryMetadata.set('childIdsBeforeMoved', tab.$TST.childIds.slice(0));
tab.$TST.temporaryMetadata.set('parentIdBeforeMoved', tab.$TST.parentId);
const replacedGroupTab = (parentTabOperationBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
await TabsGroup.tryReplaceTabWithGroup(tab, { insertBefore: tab.$TST.firstChild }) :
null;
if (!replacedGroupTab && tab.$TST.hasChild) {
if (tab.$TST.isGroupTab)
await TabsGroup.clearTemporaryState(tab);
await Tree.detachAllChildren(tab, {
behavior: parentTabOperationBehavior,
nearestFollowingRootTab: tab.$TST.firstChild.$TST.nearestFollowingRootTab,
broadcast: true
});
}
if (tab.$TST.parentId)
await Tree.detachTab(tab, {
broadcast: true
});
// Pinned tab is moved at first, so Tab.onPinned handler cannot know tree information
// before the pinned tab was moved. Thus we cache tree information for the handler.
wait(100).then(() => {
tab.$TST.temporaryMetadata.delete('childIdsBeforeMoved');
tab.$TST.temporaryMetadata.delete('parentIdBeforeMoved');
});
}
log('The tab can be placed inside existing tab unexpectedly, so now we are trying to fixup tree.');
const action = Tree.detectTabActionFromNewPosition(tab, {
isMovingByShortcut: mMaybeTabMovingByShortcut,
...moveInfo,
});
log(' => action: ', action);
if (!action.action) {
log('no action');
return;
}
// When multiple tabs are moved at once by outside of TST (e.g. moving of multiselected tabs)
// Tree.detectTabActionFromNewPosition() may be called for other tabs asynchronously
// before this operation finishes. Thus we need to memorize the calculated "parent"
// and Tree.detectTabActionFromNewPosition() will use it.
if (action.parent)
tab.$TST.temporaryMetadata.set('goingToBeAttachedTo', action.parent);
// notify event to helper addons with action and allow or deny
const cache = {};
const allowed = await TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_FIXUP_TREE_ON_TAB_MOVED,
{
tab,
fromIndex: moveInfo.fromIndex,
toIndex: moveInfo.toIndex,
action: action.action,
parent: action.parent,
insertBefore: action.insertBefore,
insertAfter: action.insertAfter,
},
{ tabProperties: ['tab', 'parent', 'insertBefore', 'insertAfter'], cache }
);
TSTAPI.clearCache(cache);
if (!allowed) {
log('no action - canceled by a helper addon');
}
else {
log('action: ', action);
switch (action.action) {
case 'invalid':
moveBack(tab, moveInfo);
break;
default:
log('tryFixupTreeForInsertedTab: apply action for unattached tab: ', tab, action);
await action.apply();
break;
}
}
if (tab.$TST.temporaryMetadata.get('goingToBeAttachedTo') == action.parent)
tab.$TST.temporaryMetadata.delete('goingToBeAttachedTo');
}
function reserveToEnsureRootTabVisible(tab) {
reserveToEnsureRootTabVisible.tabIds.add(tab.id);
if (reserveToEnsureRootTabVisible.reserved)
clearTimeout(reserveToEnsureRootTabVisible.reserved);
reserveToEnsureRootTabVisible.reserved = setTimeout(() => {
delete reserveToEnsureRootTabVisible.reserved;
const tabs = Array.from(reserveToEnsureRootTabVisible.tabIds, Tab.get);
reserveToEnsureRootTabVisible.tabIds.clear();
for (const tab of tabs) {
if (!tab.$TST ||
tab.$TST.parent ||
!tab.$TST.collapsed)
continue;
Tree.collapseExpandTabAndSubtree(tab, {
collapsed: false,
broadcast: true
});
}
}, 150);
}
reserveToEnsureRootTabVisible.tabIds = new Set();
Tab.onMoved.addListener((tab, moveInfo = {}) => {
if (moveInfo.byInternalOperation ||
!moveInfo.movedInBulk ||
tab.$TST.duplicating) {
log('internal move');
tab.$TST.nativeTabGroup?.$TST.reindex();
}
else {
log('process moved tab');
tryFixupTreeForInsertedTab(tab, moveInfo).then(() => {
tab.$TST.nativeTabGroup?.$TST.reindex();
});
}
reserveToEnsureRootTabVisible(tab);
});
function onMessage(message, _sender) {
if (!message ||
typeof message.type != 'string')
return;
//log('onMessage: ', message, sender);
switch (message.type) {
case Constants.kNOTIFY_TAB_MOUSEDOWN:
mMaybeTabMovingByShortcut = false;
break;
case Constants.kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH:
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
mMaybeTabMovingByShortcut = true;
break;
case Constants.kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH:
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
return;
mMaybeTabMovingByShortcut = false;
break;
}
}
browser.runtime.onMessage.addListener(onMessage);
Commands.onMoveUp.addListener(async tab => {
await tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: tab.index + 1,
});
});
Commands.onMoveDown.addListener(async tab => {
await tryFixupTreeForInsertedTab(tab, {
toIndex: tab.index,
fromIndex: tab.index - 1,
});
});
TreeStructure.onTabAttachedFromRestoredInfo.addListener((tab, moveInfo) => { tryFixupTreeForInsertedTab(tab, moveInfo); });
function moveBack(tab, moveInfo) {
log('Move back tab from unexpected move: ', dumpTab(tab), moveInfo);
const id = tab.id;
const win = TabsStore.windows.get(tab.windowId);
const index = moveInfo.fromIndex;
win.internalMovingTabs.set(id, index);
logApiTabs(`handle-moved-tabs:moveBack: browser.tabs.move() `, tab.id, {
windowId: moveInfo.windowId,
index: moveInfo.fromIndex
});
// Because we need to use the raw "fromIndex" directly,
// we cannot use TabsMove.moveTabInternallyBefore/After here.
return browser.tabs.move(tab.id, {
windowId: moveInfo.windowId,
index: moveInfo.fromIndex
}).catch(ApiTabs.createErrorHandler(e => {
if (win.internalMovingTabs.get(id) == index)
win.internalMovingTabs.delete(id);
ApiTabs.handleMissingTabError(e);
}));
}