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

692 lines
26 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 EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
dumpTab,
toLines,
configs,
wait,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as UserOperationBlocker from '/common/user-operation-blocker.js';
import MetricsData from '/common/MetricsData.js';
import { Tab } from '/common/TreeItem.js';
import * as Commands from './commands.js';
import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/tree-structure', ...args);
}
export const onTabAttachedFromRestoredInfo = new EventListenerManager();
let mRecentlyClosedTabs = [];
let mRecentlyClosedTabsTreeStructure = [];
export function startTracking() {
Tab.onCreated.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tab.onRemoved.addListener((tab, info) => {
if (!info.isWindowClosing)
reserveToSaveTreeStructure(tab.windowId);
});
Tab.onMoved.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tab.onUpdated.addListener((tab, info) => {
if ('openerTabId' in info)
reserveToSaveTreeStructure(tab.windowId);
});
Tree.onAttached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tree.onDetached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
Tree.onSubtreeCollapsedStateChanging.addListener(tab => { reserveToSaveTreeStructure(tab.windowId); });
}
export function reserveToSaveTreeStructure(windowId) {
const win = TabsStore.windows.get(windowId);
if (!win)
return;
if (win.waitingToSaveTreeStructure)
clearTimeout(win.waitingToSaveTreeStructure);
win.waitingToSaveTreeStructure = setTimeout(() => {
saveTreeStructure(windowId);
}, 150);
}
async function saveTreeStructure(windowId) {
const win = TabsStore.windows.get(windowId);
if (!win)
return;
const structure = TreeBehavior.getTreeStructureFromTabs(Tab.getAllTabs(windowId));
browser.sessions.setWindowValue(
windowId,
Constants.kWINDOW_STATE_TREE_STRUCTURE,
structure
).catch(ApiTabs.createErrorSuppressor());
}
export async function loadTreeStructure(windows, restoredFromCacheResults) {
log('loadTreeStructure');
MetricsData.add('loadTreeStructure: start');
return MetricsData.addAsync('loadTreeStructure: restoration for windows', Promise.all(windows.map(async win => {
if (restoredFromCacheResults &&
restoredFromCacheResults.get(win.id)) {
log(`skip tree structure restoration for window ${win.id} (restored from cache)`);
return;
}
const tabs = Tab.getAllTabs(win.id);
let windowStateCompletelyApplied = false;
try {
const structure = await browser.sessions.getWindowValue(win.id, Constants.kWINDOW_STATE_TREE_STRUCTURE).catch(ApiTabs.createErrorHandler());
let uniqueIds = tabs.map(tab => tab.$TST.uniqueId || '?');
MetricsData.add('loadTreeStructure: read stored data');
if (structure &&
structure.length > 0 &&
structure.length <= tabs.length) {
uniqueIds = uniqueIds.map(id => id.id);
let tabsOffset;
if (structure[0].id) {
const structureSignature = toLines(structure, item => item.id);
tabsOffset = uniqueIds.join('\n').indexOf(structureSignature);
windowStateCompletelyApplied = tabsOffset > -1;
}
else {
tabsOffset = 0;
windowStateCompletelyApplied = structure.length == tabs.length;
}
if (tabsOffset > -1) {
const structureRestoreTabs = tabs.slice(tabsOffset);
await Tree.applyTreeStructureToTabs(structureRestoreTabs, structure);
for (const tab of structureRestoreTabs) {
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
}
MetricsData.add('loadTreeStructure: Tree.applyTreeStructureToTabs');
}
else {
MetricsData.add('loadTreeStructure: mismatched signature');
}
}
else {
MetricsData.add('loadTreeStructure: no valid structure information');
}
}
catch(error) {
console.log(`TreeStructure.loadTreeStructure: Fatal error, ${error}`, error.stack);
MetricsData.add('loadTreeStructure: failed to apply tree structure');
}
if (!windowStateCompletelyApplied) {
log(`Tree information for the window ${win.id} is not same to actual state. Fallback to restoration from tab relations.`);
MetricsData.add('loadTreeStructure: fallback to reserveToAttachTabFromRestoredInfo');
const unattachedTabs = new Set(tabs);
for (const tab of tabs) {
reserveToAttachTabFromRestoredInfo(tab, {
keepCurrentTree: true,
canCollapse: true
}).then(attached => {
if (attached ||
tab.$TST.parent ||
tab.$TST.hasChild)
unattachedTabs.delete(tab);
});
}
await reserveToAttachTabFromRestoredInfo.promisedDone;
MetricsData.add('loadTreeStructure: attachTabFromRestoredInfo finish');
// unknown tabs may appear inside tree, so we need to fixup tree based on their position.
for (const tab of unattachedTabs) {
const action = Tree.detectTabActionFromNewPosition(tab, {
fromIndex: tabs.length - 1,
toIndex: tab.index,
isTabCreating: true,
});
switch (action.action) {
default:
break;
case 'attach':
case 'detach':
log('loadTreeStructure: apply action for unattached tab: ', tab, action);
await action.applyIfNeeded();
break;
}
}
MetricsData.add('loadTreeStructure: finish to fixup tree structure');
}
Tab.dumpAll();
})));
}
async function reserveToAttachTabFromRestoredInfo(tab, options = {}) {
if (reserveToAttachTabFromRestoredInfo.waiting)
clearTimeout(reserveToAttachTabFromRestoredInfo.waiting);
reserveToAttachTabFromRestoredInfo.tasks.push({ tab, options: options });
if (!reserveToAttachTabFromRestoredInfo.promisedDone) {
reserveToAttachTabFromRestoredInfo.promisedDone = new Promise((resolve, _reject) => {
reserveToAttachTabFromRestoredInfo.onDone = resolve;
});
}
reserveToAttachTabFromRestoredInfo.waiting = setTimeout(async () => {
reserveToAttachTabFromRestoredInfo.waiting = null;
const tasks = reserveToAttachTabFromRestoredInfo.tasks.slice(0);
reserveToAttachTabFromRestoredInfo.tasks = [];
const uniqueIds = tasks.map(task => task.tab.$TST.uniqueId);
const bulk = tasks.length > 1;
const attachedResults = await Promise.all(uniqueIds.map((uniqueId, index) => {
const task = tasks[index];
return attachTabFromRestoredInfo(task.tab, {
...task.options,
uniqueId,
bulk
}).catch(error => {
console.log(`TreeStructure.reserveToAttachTabFromRestoredInfo: Fatal error on processing task ${index}, ${error}`, error.stack);
return false;
});
}));
if (typeof reserveToAttachTabFromRestoredInfo.onDone == 'function')
reserveToAttachTabFromRestoredInfo.onDone(attachedResults.every(attached => !!attached));
delete reserveToAttachTabFromRestoredInfo.onDone;
delete reserveToAttachTabFromRestoredInfo.promisedDone;
Tab.dumpAll();
}, 100);
return reserveToAttachTabFromRestoredInfo.promisedDone;
}
reserveToAttachTabFromRestoredInfo.waiting = null;
reserveToAttachTabFromRestoredInfo.tasks = [];
reserveToAttachTabFromRestoredInfo.promisedDone = null;
async function attachTabFromRestoredInfo(tab, options = {}) {
log('attachTabFromRestoredInfo ', tab, options);
if (tab.$TST.temporaryMetadata.has('treeStructureAlreadyRestoredFromSessionData')) {
log(' => already restored ', tab.id);
return;
}
let uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed /* for backward compatibility */;
// eslint-disable-next-line prefer-const
[uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed] = await Promise.all([
options.uniqueId || tab.$TST.uniqueId || tab.$TST.promisedUniqueId,
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_BEFORE).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER).catch(ApiTabs.createErrorHandler()),
// This legacy should be removed after legacy data are cleared enough, maybe after Firefox 128 is released.
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER_LEGACY).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ANCESTORS).catch(ApiTabs.createErrorHandler()),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_CHILDREN).catch(ApiTabs.createErrorHandler()),
tab.$TST.getPermanentStates(),
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorHandler()) // for backward compatibility
]);
ancestors = ancestors || [];
children = children || [];
log(`persistent references for ${dumpTab(tab)} (${uniqueId.id}): `, {
insertBefore, insertAfter,
insertAfterLegacy,
ancestors: ancestors.join(', '),
children: children.join(', '),
states,
collapsed
});
if (collapsed && !states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED)) {
// migration
states.push(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
browser.sessions.removeTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorSuppressor());
}
insertBefore = Tab.getByUniqueId(insertBefore);
insertAfter = Tab.getByUniqueId(insertAfter || insertAfterLegacy);
ancestors = ancestors.map(Tab.getByUniqueId);
children = children.map(Tab.getByUniqueId);
log(' => references: ', tab.id, () => ({
insertBefore: dumpTab(insertBefore),
insertAfter: dumpTab(insertAfter),
ancestors: ancestors.map(dumpTab).join(', '),
children: children.map(dumpTab).join(', ')
}));
if (configs.fixupTreeOnTabVisibilityChanged) {
ancestors = ancestors.filter(ancestor => ancestor && (ancestor.hidden == tab.hidden));
children = children.filter(child => child && (child.hidden == tab.hidden));
log(' ==> references: ', tab.id, () => ({
ancestors: ancestors.map(dumpTab).join(', '),
children: children.map(dumpTab).join(', ')
}));
}
// clear wrong positioning information
if (tab.pinned ||
insertBefore?.pinned)
insertBefore = null;
const nextOfInsertAfter = insertAfter?.$TST.nextTab;
if (nextOfInsertAfter &&
nextOfInsertAfter.pinned)
insertAfter = null;
let attached = false;
const active = tab.active;
const promises = [];
for (const ancestor of ancestors) {
if (!ancestor)
continue;
log(' attach to old ancestor: ', tab.id, { child: tab, parent: ancestor });
const promisedDone = Tree.attachTabTo(tab, ancestor, {
insertBefore,
insertAfter,
dontExpand: !active,
forceExpand: active,
broadcast: true
});
if (options.bulk)
promises.push(promisedDone);
else
await promisedDone;
attached = true;
break;
}
if (!attached) {
const opener = tab.$TST.openerTab;
if (opener &&
configs.syncParentTabAndOpenerTab) {
log(' attach to opener: ', tab.id, { child: tab, parent: opener });
const promisedDone = Tree.attachTabTo(tab, opener, {
dontExpand: !active,
forceExpand: active,
broadcast: true,
insertAt: Constants.kINSERT_NEAREST
});
if (options.bulk)
promises.push(promisedDone);
else
await promisedDone;
}
else if (!options.bulk &&
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab)) {
log(' attach from position: ', tab.id);
onTabAttachedFromRestoredInfo.dispatch(tab, {
toIndex: tab.index,
fromIndex: Tab.getLastTab(tab.windowId).index
});
}
}
if (!options.keepCurrentTree &&
// the restored tab is a roo tab
ancestors.length == 0 &&
// but attached to any parent based on its restored position
tab.$TST.parent &&
// when not in-middle position of existing tree (safely detachable position)
!tab.$TST.nextSiblingTab) {
Tree.detachTab(tab, {
broadcast: true
});
}
if (options.children && !options.bulk) {
let firstInTree = tab.$TST.firstChild || tab;
let lastInTree = tab.$TST.lastDescendant || tab;
for (const child of children) {
if (!child)
continue;
await Tree.attachTabTo(child, tab, {
dontExpand: !child.active,
forceExpand: active,
insertAt: Constants.kINSERT_NEAREST,
dontMove: child.index >= firstInTree.index && child.index <= lastInTree.index + 1,
broadcast: true
});
if (child.index < firstInTree.index)
firstInTree = child;
else if (child.index > lastInTree.index)
lastInTree = child;
}
}
const subtreeCollapsed = states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
log('restore subtree collapsed state: ', tab.id, { current: tab.$TST.subtreeCollapsed, expected: subtreeCollapsed, ...options });
if ((options.canCollapse || options.bulk) &&
tab.$TST.subtreeCollapsed != subtreeCollapsed) {
const promisedDone = Tree.collapseExpandSubtree(tab, {
broadcast: true,
collapsed: subtreeCollapsed,
justNow: true
});
promises.push(promisedDone);
}
const updateCollapsedState = () => {
const shouldBeCollapsed = tab.$TST.ancestors.some(ancestor => ancestor.$TST.collapsed || ancestor.$TST.subtreeCollapsed);
log('update collapsed state: ', tab.id, { current: tab.$TST.collapsed, expected: shouldBeCollapsed });
if ((options.canCollapse || options.bulk) &&
tab.$TST.collapsed != shouldBeCollapsed) {
Tree.collapseExpandTabAndSubtree(tab, {
broadcast: true,
collapsed: !tab.$TST.collapsed,
justNow: true
});
}
};
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
if (options.bulk)
await Promise.all(promises).then(updateCollapsedState);
else
updateCollapsedState();
return attached;
}
const mRestoringTabs = new Map();
const mMaxRestoringTabs = new Map();
const mRestoredTabIds = new Set();
const mProcessingTabRestorations = [];
Tab.onRestored.addListener(tab => {
log('onTabRestored ', dumpTab(tab));
mProcessingTabRestorations.push(async () => {
try {
const count = mRestoringTabs.get(tab.windowId) || 0;
if (count == 0) {
setTimeout(() => {
const count = mRestoringTabs.get(tab.windowId) || 0;
if (count > 0) {
UserOperationBlocker.blockIn(tab.windowId, { throbber: true });
UserOperationBlocker.setProgress(0, tab.windowId);
}
}, configs.delayToBlockUserOperationForTabsRestoration);
}
mRestoringTabs.set(tab.windowId, count + 1);
const maxCount = mMaxRestoringTabs.get(tab.windowId) || 0;
mMaxRestoringTabs.set(tab.windowId, Math.max(count, maxCount));
const uniqueId = await tab.$TST.promisedUniqueId;
mRestoredTabIds.add(uniqueId.id);
if (count == 0) {
// Force restore recycled active tab.
// See also: https://github.com/piroor/treestyletab/issues/2191#issuecomment-489271889
const activeTab = Tab.getActiveTab(tab.windowId);
const [uniqueId, restoredUniqueId] = await Promise.all([
activeTab.$TST.promisedUniqueId,
browser.sessions.getTabValue(activeTab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler())
]);
if (restoredUniqueId?.id != uniqueId.id) {
activeTab.$TST.updateUniqueId({ id: restoredUniqueId.id });
reserveToAttachTabFromRestoredInfo(activeTab, {
children: true
});
}
}
reserveToAttachTabFromRestoredInfo(tab, {
children: true
});
reserveToAttachTabFromRestoredInfo.promisedDone.then(() => {
Tree.fixupSubtreeCollapsedState(tab, {
justNow: true,
broadcast: true
});
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_RESTORED,
tabId: tab.id,
windowId: tab.windowId
});
let count = mRestoringTabs.get(tab.windowId) || 0;
count--;
if (count == 0) {
mRestoringTabs.delete(tab.windowId);
mMaxRestoringTabs.delete(tab.windowId);
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
// unblock in the next event loop, after other asynchronous operations are finished
UserOperationBlocker.unblockIn(tab.windowId, { throbber: true });
}, 0);
const countToBeRestored = mRecentlyClosedTabs.filter(tab => !mRestoredTabIds.has(tab.uniqueId));
log('countToBeRestored: ', countToBeRestored);
if (countToBeRestored > 0)
tryRestoreClosedSetFor(tab, countToBeRestored);
mRestoredTabIds.clear();
}
else {
mRestoringTabs.set(tab.windowId, count);
const maxCount = mMaxRestoringTabs.get(tab.windowId);
UserOperationBlocker.setProgress(Math.round(maxCount - count / maxCount * 100), tab.windowId);
}
});
}
catch(_e) {
}
mProcessingTabRestorations.shift();
if (mProcessingTabRestorations.length > 0)
mProcessingTabRestorations[0]();
});
if (mProcessingTabRestorations.length == 1)
mProcessingTabRestorations[0]();
});
// Implementation for the "Undo Close Tab*s*" feature
// https://github.com/piroor/treestyletab/issues/2627
const mPendingRecentlyClosedTabsInfo = {
tabs: [],
structure: []
};
Tab.onRemoved.addListener((_tab, _info) => {
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
mRecentlyClosedTabs = [];
mRecentlyClosedTabsTreeStructure = [];
const newlyRestorable = mRecentlyClosedTabs.length > 1;
if (currentlyRestorable != newlyRestorable)
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
});
Tab.onMultipleTabsRemoving.addListener((tabs, { triggerTab, originalStructure } = {}) => {
if (triggerTab)
tabs = [triggerTab, ...tabs];
mPendingRecentlyClosedTabsInfo.tabs = tabs.map(tab => ({
originalId: tab.id,
uniqueId: tab.$TST.uniqueId.id,
windowId: tab.windowId,
title: tab.title,
url: tab.url,
cookieStoreId: tab.cookieStoreId
}));
mPendingRecentlyClosedTabsInfo.structure = originalStructure || TreeBehavior.getTreeStructureFromTabs(tabs, {
full: true,
keepParentOfRootTabs: true
});
log('mPendingRecentlyClosedTabsInfo.tabs = ', mPendingRecentlyClosedTabsInfo.tabs);
log('mPendingRecentlyClosedTabsInfo.structure = ', mPendingRecentlyClosedTabsInfo.structure);
});
Tab.onMultipleTabsRemoved.addListener((tabs, { triggerTab } = {}) => {
log('multiple tabs are removed');
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
if (triggerTab)
tabs = [triggerTab, ...tabs];
const tabIds = new Set(tabs.map(tab => tab.id));
mRecentlyClosedTabs = mPendingRecentlyClosedTabsInfo.tabs.filter(info => tabIds.has(info.originalId));
mRecentlyClosedTabsTreeStructure = mPendingRecentlyClosedTabsInfo.structure.filter(structure => tabIds.has(structure.originalId));
log(' structure: ', mRecentlyClosedTabsTreeStructure);
const newlyRestorable = mRecentlyClosedTabs.length > 1;
if (currentlyRestorable != newlyRestorable)
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
mPendingRecentlyClosedTabsInfo.tabs = [];
mPendingRecentlyClosedTabsInfo.structure = [];
});
let mToBeActivatedRestoredTabId = null;
function onRestoredTabActivated(activeInfo) {
if (mToBeActivatedRestoredTabId &&
activeInfo.id != mToBeActivatedRestoredTabId) {
TabsInternalOperation.activateTab(mToBeActivatedRestoredTabId);
}
mToBeActivatedRestoredTabId = null;
}
browser.tabs.onActivated.addListener(onRestoredTabActivated);
async function tryRestoreClosedSetFor(tab, countToBeRestored) {
const lastRecentlyClosedTabs = mRecentlyClosedTabs;
const lastRecentlyClosedTabsTreeStructure = mRecentlyClosedTabsTreeStructure;
mRecentlyClosedTabs = [];
mRecentlyClosedTabsTreeStructure = [];
if (lastRecentlyClosedTabs.length > 1)
Tab.onChangeMultipleTabsRestorability.dispatch(false);
if (!configs.undoMultipleTabsClose)
return;
const alreadRestoredIndex = lastRecentlyClosedTabs.findIndex(info => info.uniqueId == tab.$TST.uniqueId.id && info.windowId == tab.windowId);
log('tryRestoreClosedSetFor ', tab, lastRecentlyClosedTabs, lastRecentlyClosedTabsTreeStructure);
if (alreadRestoredIndex < 0) {
log(' => not a member of restorable tab set.');
return;
}
const toBeRestoredTabsCount = Math.min(
typeof countToBeRestored == 'number' ? countToBeRestored : Number.MAX_SAFE_INTEGER,
lastRecentlyClosedTabs.filter(tabInfo => tabInfo.uniqueId != tab.$TST.uniqueId.id).length
);
if (toBeRestoredTabsCount == 0) {
log(' => no more tab to be restored.');
return;
}
const sessions = (await browser.sessions.getRecentlyClosed({
maxResults: browser.sessions.MAX_SESSION_RESULTS
}).catch(ApiTabs.createErrorHandler())).filter(session => session.tab);
const canRestoreWithSession = toBeRestoredTabsCount <= sessions.length;
let restoredTabs;
if (canRestoreWithSession) {
log(`tryRestoreClosedSetFor: restore ${toBeRestoredTabsCount} tabs with the sessions API`);
const unsortedRestoredTabs = await Commands.restoreTabs(toBeRestoredTabsCount);
unsortedRestoredTabs.push(tab);
// tabs can be restored in different order, then we need to rearrange them manually
const tabsByUniqueId = new Map();
for (const tab of unsortedRestoredTabs) {
tabsByUniqueId.set(tab.$TST.uniqueId.id, tab);
}
restoredTabs = await Promise.all(lastRecentlyClosedTabsTreeStructure.map(tabInfo => {
const restoredTab = tabsByUniqueId.get(tabInfo.id);
if (restoredTab)
return restoredTab;
log('tryRestoreClosedSetFor: recreate tab for ', tabInfo);
return TabsOpen.openURIInTab({
title: tabInfo.title,
url: tabInfo.url,
cookieStoreId: tabInfo.cookieStoreId
}, {
windowId: tab.windowId,
isOrphan: true,
inBackground: true,
discarded: true,
fixPositions: true
});
}));
let lastTab;
for (const tab of restoredTabs) {
if (lastTab && tab.$TST.previousTab != lastTab)
await TabsMove.moveTabAfter(
tab,
lastTab,
{ broadcast: true }
);
lastTab = tab;
}
}
else {
log('tryRestoreClosedSetFor: recreate tabs');
const tabOpenOptions = {
windowId: lastRecentlyClosedTabs[0].windowId,
isOrphan: true,
inBackground: true,
discarded: true,
fixPositions: true
};
const beforeTabs = await TabsOpen.openURIsInTabs(
lastRecentlyClosedTabs.slice(0, alreadRestoredIndex).map(info => ({
title: info.title,
url: info.url,
cookieStoreId: info.cookieStoreId
})),
tabOpenOptions
);
const afterTabs = await TabsOpen.openURIsInTabs(
lastRecentlyClosedTabs.slice(alreadRestoredIndex + 1).map(info => ({
title: info.title,
url: info.url,
cookieStoreId: info.cookieStoreId
})),
tabOpenOptions
);
// We need to move tabs after they are opened instead of
// specifying the "index" option for TabsOpen.openURIsInTabs(),
// because the given restored tab can be moved while this
// operation.
if (beforeTabs.length > 0)
await TabsMove.moveTabsBefore(
beforeTabs,
tab,
{ broadcast: true }
);
if (afterTabs.length > 0)
await TabsMove.moveTabsAfter(
afterTabs,
tab,
{ broadcast: true }
);
restoredTabs = [...beforeTabs, tab, ...afterTabs];
await TabsInternalOperation.activateTab(restoredTabs[0]);
}
const rootTabs = restoredTabs.filter((tab, index) => lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_KEEP_PARENT || lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_NO_PARENT);
log(`tryRestoreClosedSetFor: rootTabs, restoredTabs = `, rootTabs, restoredTabs);
for (const rootTab of rootTabs) {
const referenceTabs = TreeBehavior.calculateReferenceItemsFromInsertionPosition(rootTab, {
context: Constants.kINSERTION_CONTEXT_MOVED,
insertAfter: rootTab.$TST.previousTab,
insertBefore: restoredTabs[restoredTabs.length - 1].$TST.nextTab
});
log(`tryRestoreClosedSetFor: referenceTabs for ${rootTab.id} => `, referenceTabs);
if (referenceTabs.parent)
await Tree.attachTabTo(rootTab, referenceTabs.parent, {
dontExpand: true,
insertAfter: referenceTabs.insertAfter,
dontMove: true,
broadcast: true
});
}
await Tree.applyTreeStructureToTabs(
restoredTabs,
lastRecentlyClosedTabsTreeStructure
);
// Firefox itself activates the initially restored tab with delay,
// so we need to activate the first tab of the restored tabs again.
mToBeActivatedRestoredTabId = restoredTabs[0].id;
wait(100).then(() => onRestoredTabActivated({ id: -1 })); // failsafe
}