From 94020c21bf48d45fd571db71ec3de8e6c56f8349 Mon Sep 17 00:00:00 2001 From: Alex Kontos Date: Mon, 23 Sep 2024 14:58:39 +0100 Subject: [PATCH] Update sidebar to 1.0.3. * Fix "Autohide" behavior of the Waterfox Sidebar for full screen mode It was caused by the difference of the spec of -moz-bool-pref between ESR115 and ESR128. Now codes have been renewed for ESR128. * Fix an error opening option page It failed to run the initializer function on some cases. Now the initializer is retried with delay. * Fix "Bookmark Selected Tabs" command It was caused by the difference of the Places modules between ESR115 and ESR128. Now codes have been renewed for ESR128. * Fix Tab preview feature with browser.tabs.hoverPreview.enabled=true It was largely reconstructured on Firefox, thus I've updated codes for the current implementation. * Fix behavior of F1 key not to toggle the Waterfox Sidebar It happened when the "key" XUL element is re-inserted dynamically. Now the first inserted element is reused after reinstallation of the sidebar module. * Fix the behavior of "Reset to default" operation in the toolbar customization It was caused by an unhandled error in Firefox's toolbar customization module. I've implemented a mechanism to reset toolbar customizations until insertion of the tabs sidebar button succeed. --- .../sidebar/_locales/en/messages.json | 3 + .../sidebar/_locales/ja/messages.json | 3 + .../sidebar/background/api-tabs-listener.js | 89 +-- .../sidebar/background/background-cache.js | 6 +- .../sidebar/background/background.js | 15 +- .../sidebar/background/browser-action-menu.js | 28 +- .../sidebar/background/handle-misc.js | 32 +- .../sidebar/background/handle-moved-tabs.js | 92 +-- .../sidebar/background/handle-tab-bunches.js | 11 +- .../components/sidebar/background/prefs.js | 15 - .../sidebar/background/successor-tab.js | 53 +- .../sidebar/background/tab-context-menu.js | 31 + .../sidebar/background/tabs-group.js | 40 +- .../sidebar/background/tabs-move.js | 22 +- .../sidebar/background/tabs-open.js | 3 +- .../components/sidebar/background/tree.js | 306 ++++++---- .../browser/components/sidebar/common/Tab.js | 65 +- .../components/sidebar/common/Window.js | 11 +- .../components/sidebar/common/api-tabs.js | 49 -- .../components/sidebar/common/bookmark.js | 90 +-- .../sidebar/common/cache-storage.js | 127 ++-- .../components/sidebar/common/common.js | 23 +- .../components/sidebar/common/constants.js | 3 + .../components/sidebar/common/permissions.js | 19 +- .../components/sidebar/common/retrieve-url.js | 7 +- .../sidebar/common/tabs-internal-operation.js | 85 ++- .../sidebar/common/tree-behavior.js | 17 +- .../components/sidebar/common/tst-api.js | 6 + .../components/sidebar/experiments/prefs.js | 26 +- .../sidebar/experiments/waterfoxBridge.js | 236 +++++--- .../sidebar/experiments/waterfoxBridge.json | 11 - .../browser/components/sidebar/manifest.json | 559 +++++++++--------- .../components/sidebar/options/init.js | 31 +- .../components/sidebar/options/options.css | 2 +- .../components/sidebar/options/options.html | 16 + .../components/sidebar/resources/group-tab.js | 74 ++- .../components/sidebar/resources/ui-color.css | 36 +- .../sidebar/sidebar/components/TabElement.js | 58 +- .../sidebar/components/TabLabelElement.js | 5 + .../sidebar/sidebar/drag-and-drop.js | 21 +- .../components/sidebar/sidebar/index-ws.js | 12 + .../sidebar/sidebar/mouse-event-listener.js | 71 ++- .../components/sidebar/sidebar/scroll.js | 169 ++++-- .../sidebar/sidebar/sidebar-tabs.js | 4 +- .../components/sidebar/sidebar/sidebar.js | 16 +- .../sidebar/sidebar/styles/photon/base.css | 7 +- .../sidebar/styles/photon/highcontrast.css | 4 +- .../sidebar/sidebar/styles/photon/photon.css | 8 +- .../sidebar/sidebar/styles/proton/proton.css | 32 +- .../sidebar/sidebar/tab-context-menu.js | 41 +- .../components/sidebar/sidebar/tab-preview.js | 2 - .../sidebar/sidebar/tst-api-frontend.js | 7 +- 52 files changed, 1653 insertions(+), 1046 deletions(-) diff --git a/waterfox/browser/components/sidebar/_locales/en/messages.json b/waterfox/browser/components/sidebar/_locales/en/messages.json index c902600bec85d..c9071c5bc71fe 100644 --- a/waterfox/browser/components/sidebar/_locales/en/messages.json +++ b/waterfox/browser/components/sidebar/_locales/en/messages.json @@ -614,6 +614,7 @@ "config_groupTabTemporaryStateForNewTabsFromBookmarks_label": { "message": "For tabs opened from bookmarks:" }, "config_groupTabTemporaryStateForNewTabsFromOthers_label": { "message": "For tabs opened at a time from non-bookmark triggers:" }, "config_groupTabTemporaryStateForOrphanedTabs_label": { "message": "Opened to replace a closed parent tab:" }, + "config_groupTabTemporaryStateForAPI_label": { "message": "Opened to group tabs from other addons via API:" }, "config_inheritContextualIdentityToChildTabMode_label": { "message": "Container:" }, "config_inheritContextualIdentityToChildTabMode_default": { "message": "(no control)" }, @@ -730,6 +731,7 @@ "config_insertDroppedTabsAt_first": { "message": "The top of the tree (next to the parent)" }, "config_insertDroppedTabsAt_end": { "message": "The end of the tree" }, + "config_autoCreateFolderForBookmarksFromTree_label": { "message": "Auto-group bookmark items under a folder when they are guessed to be created from multiple tree item tabs (ex. drag and drop of multiselected tabs)" }, "config_autoExpandOnLongHoverDelay_before": { "message": "Expand collapsed tree with hovering over " }, "config_autoExpandOnLongHoverDelay_after": { "message": "msec while dragging" }, "config_autoExpandOnLongHoverRestoreIniitalState_label": { "message": "Collapse such trees automatically after a drag action is finished" }, @@ -892,6 +894,7 @@ "tabContextMenu_shareTabURL_more_label": { "message": "More…" }, + "tabContextMenu_closeDuplicatedTabs_label": { "message": "Close D&uplicate Tabs" }, "tabContextMenu_closeMultipleTabs_label": { "message": "Close &Multiple Tabs" }, "tabContextMenu_closeTabsToTop_label": { "message": "Close Tabs to the &Top" }, "tabContextMenu_closeTabsToBottom_label": { "message": "Close Tabs to the &Bottom" }, diff --git a/waterfox/browser/components/sidebar/_locales/ja/messages.json b/waterfox/browser/components/sidebar/_locales/ja/messages.json index af98063c97a27..b923dca2c73aa 100644 --- a/waterfox/browser/components/sidebar/_locales/ja/messages.json +++ b/waterfox/browser/components/sidebar/_locales/ja/messages.json @@ -611,6 +611,7 @@ "config_groupTabTemporaryStateForNewTabsFromBookmarks_label": { "message": "ブックマークから開かれたタブをグループ化した場合:" }, "config_groupTabTemporaryStateForNewTabsFromOthers_label": { "message": "ブックマーク以外から連続して開かれたタブをグループ化した場合:" }, "config_groupTabTemporaryStateForOrphanedTabs_label": { "message": "閉じられた親タブを置き換える形で開かれた場合:" }, + "config_groupTabTemporaryStateForAPI_label": { "message": "他の拡張機能からAPI経由でタブをグループ化した場合:" }, "config_inheritContextualIdentityToChildTabMode_label": { "message": "コンテナー:" }, "config_inheritContextualIdentityToChildTabMode_default": { "message": "(制御しない)" }, @@ -727,6 +728,7 @@ "config_insertDroppedTabsAt_first": { "message": "ツリーの先頭(親タブの隣)" }, "config_insertDroppedTabsAt_end": { "message": "ツリーの末尾" }, + "config_autoCreateFolderForBookmarksFromTree_label": { "message": "ツリー構造を持ったタブを複数選択してのドラッグ&ドロップなどの操作で複数のブックマークが作成されたと推測できる場合に、作成されたブックマークを自動的に1つのフォルダーにまとめる" }, "config_autoExpandOnLongHoverDelay_before": { "message": "ドラッグ操作中、折りたたまれたツリーの上で" }, "config_autoExpandOnLongHoverDelay_after": { "message": "ミリ秒以上経過したらツリーを展開する" }, "config_autoExpandOnLongHoverRestoreIniitalState_label": { "message": "ドラッグ操作の終了後、ツリーを折りたたんだ状態に自動的に戻す" }, @@ -888,6 +890,7 @@ "tabContextMenu_shareTabURL_label": { "message": "共有(&H)" }, "tabContextMenu_shareTabURL_more_label": { "message": "その他..." }, + "tabContextMenu_closeDuplicatedTabs_label": { "message": "重複タブを閉じる(&U)" }, "tabContextMenu_closeMultipleTabs_label": { "message": "複数のタブを閉じる(&M)" }, "tabContextMenu_closeTabsToTop_label": { "message": "上のタブをすべて閉じる(&T)" }, "tabContextMenu_closeTabsToBottom_label": { "message": "下のタブをすべて閉じる(&B)" }, diff --git a/waterfox/browser/components/sidebar/background/api-tabs-listener.js b/waterfox/browser/components/sidebar/background/api-tabs-listener.js index 8910e4241d8e0..b7a539d522392 100644 --- a/waterfox/browser/components/sidebar/background/api-tabs-listener.js +++ b/waterfox/browser/components/sidebar/background/api-tabs-listener.js @@ -138,15 +138,12 @@ async function onActivated(activeInfo) { try { const win = Window.init(activeInfo.windowId); - const byInternalOperation = win.internalFocusCount > 0; - if (byInternalOperation) - win.internalFocusCount--; - const byMouseOperation = win.internalByMouseFocusCount > 0; - if (byMouseOperation) - win.internalByMouseFocusCount--; - const silently = win.internalSilentlyFocusCount > 0; - if (silently) - win.internalSilentlyFocusCount--; + const byInternalOperation = win.internallyFocusingTabs.has(activeInfo.tabId); + win.internallyFocusingTabs.delete(activeInfo.tabId); + const byMouseOperation = win.internallyFocusingByMouseTabs.has(activeInfo.tabId); + win.internallyFocusingByMouseTabs.delete(activeInfo.tabId); + const silently = win.internallyFocusingSilentlyTabs.has(activeInfo.tabId); + win.internallyFocusingSilentlyTabs.delete(activeInfo.tabId); const byTabDuplication = parseInt(win.duplicatingTabsCount) > 0; if (!Tab.isTracked(activeInfo.tabId)) @@ -272,6 +269,7 @@ async function onUpdated(tabId, changeInfo, tab) { } } /* + Workaround for Firefox 130 and olders. Updated openerTabId is not notified via tabs.onUpdated due to https://bugzilla.mozilla.org/show_bug.cgi?id=1409262 , so it can be notified with delay as a part of the complete tabs.Tab object, @@ -284,8 +282,11 @@ async function onUpdated(tabId, changeInfo, tab) { continue; if ('key' in updatedTab) oldState[key] = updatedTab[key]; - if (key == 'openerTabId') + if (key == 'openerTabId') { + if (changeInfo.openerTabId == updatedTab.openerTabId) // already processed + continue; log(`openerTabId of ${tabId} is changed by someone (notified via changeInfo)!: ${updatedTab.openerTabId} (original) => ${changeInfo[key]} (changed by someone)`, configs.debug && new Error().stack); + } updatedTab[key] = changeInfo[key]; } if (changeInfo.url || @@ -788,10 +789,8 @@ async function onNewTabTracked(tab, info) { } tab.$TST.memorizeNeighbors('newly tracked'); - if (tab.$TST.unsafePreviousTab) - tab.$TST.unsafePreviousTab.$TST.memorizeNeighbors('unsafePreviousTab'); - if (tab.$TST.unsafeNextTab) - tab.$TST.unsafeNextTab.$TST.memorizeNeighbors('unsafeNextTab'); + tab.$TST.unsafePreviousTab?.$TST?.memorizeNeighbors('unsafePreviousTab'); + tab.$TST.unsafeNextTab?.$TST?.memorizeNeighbors('unsafeNextTab'); Tree.onAttached.removeListener(onTreeModified); metric.add('Tree.onAttached proceeded'); @@ -839,11 +838,18 @@ async function onRemoved(tabId, removeInfo) { log('tabs.onRemoved: ', tabId, removeInfo); const win = Window.init(removeInfo.windowId); const byInternalOperation = win.internalClosingTabs.has(tabId); - if (byInternalOperation) - win.internalClosingTabs.delete(tabId); const preventEntireTreeBehavior = win.keepDescendantsTabs.has(tabId); - if (preventEntireTreeBehavior) - win.keepDescendantsTabs.delete(tabId); + + win.internalMovingTabs.delete(tabId); + win.alreadyMovedTabs.delete(tabId); + win.internalClosingTabs.delete(tabId); + win.keepDescendantsTabs.delete(tabId); + win.highlightingTabs.delete(tabId); + win.tabsToBeHighlightedAlone.delete(tabId); + + win.internallyFocusingTabs.delete(tabId); + win.internallyFocusingByMouseTabs.delete(tabId); + win.internallyFocusingSilentlyTabs.delete(tabId); if (Tab.needToWaitTracked(removeInfo.windowId)) await Tab.waitUntilTrackedAll(removeInfo.windowId); @@ -925,9 +931,7 @@ async function onRemoved(tabId, removeInfo) { oldTab.$TST.destroy(); for (const tab of nearestTabs) { - if (!tab || !tab.$TST) - continue; - tab.$TST.memorizeNeighbors('neighbor of closed tab'); + tab?.$TST?.memorizeNeighbors('neighbor of closed tab'); } onCompleted(); @@ -956,7 +960,10 @@ async function onMoved(tabId, moveInfo) { // and other fixup operations around tabs moved by foreign triggers, on such // cases. Don't mind, the tab will be rearranged again by delayed // TabsMove.syncTabsPositionToApiTabs() anyway! - const maybeInternalOperation = win.internalMovingTabs.has(tabId); + const internalExpectedIndex = win.internalMovingTabs.get(tabId); + const maybeInternalOperation = internalExpectedIndex < 0 || internalExpectedIndex == moveInfo.toIndex; + if (maybeInternalOperation) + log(`tabs.onMoved: ${tabId} is detected as moved internally`); if (!Tab.isTracked(tabId)) await Tab.waitUntilTracked(tabId); @@ -979,7 +986,7 @@ async function onMoved(tabId, moveInfo) { do following processes after the tab is completely pinned. */ const movedTab = Tab.get(tabId); if (!movedTab) { - if (maybeInternalOperation) + if (win.internalMovingTabs.has(tabId)) win.internalMovingTabs.delete(tabId); completelyMoved(); warnTabDestroyedWhileWaiting(tabId, movedTab); @@ -989,22 +996,21 @@ async function onMoved(tabId, moveInfo) { let oldPreviousTab = movedTab.hidden ? movedTab.$TST.unsafePreviousTab : movedTab.$TST.previousTab; let oldNextTab = movedTab.hidden ? movedTab.$TST.unsafeNextTab : movedTab.$TST.nextTab; if (movedTab.index != moveInfo.toIndex || - (oldPreviousTab && oldPreviousTab.index == movedTab.index - 1) || - (oldNextTab && oldNextTab.index == movedTab.index + 1)) { + (oldPreviousTab?.index == movedTab.index - 1) || + (oldNextTab?.index == movedTab.index + 1)) { // already moved oldPreviousTab = Tab.getTabAt(moveInfo.windowId, moveInfo.toIndex < moveInfo.fromIndex ? moveInfo.fromIndex : moveInfo.fromIndex - 1); oldNextTab = Tab.getTabAt(moveInfo.windowId, moveInfo.toIndex < moveInfo.fromIndex ? moveInfo.fromIndex + 1 : moveInfo.fromIndex); - if (oldPreviousTab && oldPreviousTab.id == movedTab.id) + if (oldPreviousTab?.id == movedTab.id) oldPreviousTab = Tab.getTabAt(moveInfo.windowId, moveInfo.toIndex < moveInfo.fromIndex ? moveInfo.fromIndex - 1 : moveInfo.fromIndex - 2); - if (oldNextTab && oldNextTab.id == movedTab.id) + if (oldNextTab?.id == movedTab.id) oldNextTab = Tab.getTabAt(moveInfo.windowId, moveInfo.toIndex < moveInfo.fromIndex ? moveInfo.fromIndex : moveInfo.fromIndex - 1); } - let alreadyMoved = false; - if (win.alreadyMovedTabs.has(tabId)) { + const expectedIndex = win.alreadyMovedTabs.get(tabId); + const alreadyMoved = expectedIndex < 0 || expectedIndex == moveInfo.toIndex; + if (win.alreadyMovedTabs.has(tabId)) win.alreadyMovedTabs.delete(tabId); - alreadyMoved = true; - } const extendedMoveInfo = { ...moveInfo, @@ -1012,7 +1018,10 @@ async function onMoved(tabId, moveInfo) { alreadyMoved, oldPreviousTab, oldNextTab, - isSubstantiallyMoved: movedTab.$TST.isSubstantiallyMoved + // Multiselected tabs can be moved together in bulk, by drag and drop + // in the horizontal tab bar, or addons like + // https://addons.mozilla.org/firefox/addon/move-tab-hotkeys/ + movedInBulk: !maybeInternalOperation && (movedTab.$TST.multiselected || movedTab.$TST.movedInBulk), }; log('tabs.onMoved: ', movedTab, extendedMoveInfo); @@ -1060,20 +1069,16 @@ async function onMoved(tabId, moveInfo) { nextTabId: nextTab && nextTab.id }); } - if (maybeInternalOperation) + if (win.internalMovingTabs.has(tabId)) win.internalMovingTabs.delete(tabId); completelyMoved(); movedTab.$TST.memorizeNeighbors('moved'); - if (movedTab.$TST.unsafePreviousTab) - movedTab.$TST.unsafePreviousTab.$TST.memorizeNeighbors('unsafePreviousTab'); - if (movedTab.$TST.unsafeNextTab) - movedTab.$TST.unsafeNextTab.$TST.memorizeNeighbors('unsafeNextTab'); - - if (oldPreviousTab) - oldPreviousTab.$TST.memorizeNeighbors('oldPreviousTab'); - if (oldNextTab) - oldNextTab.$TST.memorizeNeighbors('oldNextTab'); + movedTab.$TST.unsafePreviousTab?.$TST?.memorizeNeighbors('unsafePreviousTab'); + movedTab.$TST.unsafeNextTab?.$TST?.memorizeNeighbors('unsafeNextTab'); + + oldPreviousTab?.$TST?.memorizeNeighbors('oldPreviousTab'); + oldNextTab?.$TST?.memorizeNeighbors('oldNextTab'); } catch(e) { console.log(e); diff --git a/waterfox/browser/components/sidebar/background/background-cache.js b/waterfox/browser/components/sidebar/background/background-cache.js index bbbf21c92c765..1561ce5eff62b 100644 --- a/waterfox/browser/components/sidebar/background/background-cache.js +++ b/waterfox/browser/components/sidebar/background/background-cache.js @@ -339,7 +339,8 @@ async function updateWindowCache(owner, key, value) { }); return; } - catch(_error) { + catch(error) { + console.log(`BackgroundCache.updateWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error); } } @@ -372,7 +373,8 @@ async function getWindowCache(owner, key) { }); return value; } - catch(_error) { + catch(error) { + console.log(`BackgroundCache.getWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error); } } diff --git a/waterfox/browser/components/sidebar/background/background.js b/waterfox/browser/components/sidebar/background/background.js index 52bca30f1033d..419cd6d4ab7f9 100644 --- a/waterfox/browser/components/sidebar/background/background.js +++ b/waterfox/browser/components/sidebar/background/background.js @@ -310,12 +310,23 @@ async function rebuildAll(windows) { try { log(`build tabs for ${win.id} from scratch`); Window.init(win.id); + const promises = []; for (let tab of win.tabs) { tab = Tab.get(tab.id); tab.$TST.clear(); // clear dirty restored states - TabsUpdate.updateTab(tab, tab, { forceApply: true }); + promises.push( + tab.$TST.getPermanentStates() + .then(states => { + tab.$TST.states = new Set(states); + }) + .catch(console.error) + .then(() => { + TabsUpdate.updateTab(tab, tab, { forceApply: true }); + }) + ); tryStartHandleAccelKeyOnTab(tab); } + await Promise.all(promises); } catch(e) { log(`failed to build tabs for ${win.id}`, e); @@ -774,7 +785,7 @@ Tab.onTabInternallyMoved.addListener((tab, info = {}) => { }); Tab.onMoved.addListener((tab, moveInfo) => { - if (!moveInfo.isSubstantiallyMoved) + if (moveInfo.movedInBulk) return; reserveToUpdateInsertionPosition([ tab, diff --git a/waterfox/browser/components/sidebar/background/browser-action-menu.js b/waterfox/browser/components/sidebar/background/browser-action-menu.js index 72cf03cbdfbe4..af224b7f61135 100644 --- a/waterfox/browser/components/sidebar/background/browser-action-menu.js +++ b/waterfox/browser/components/sidebar/background/browser-action-menu.js @@ -1298,8 +1298,32 @@ const mItems = [ type: 'radio' } ] - } - ] + }, + { + title: indent() + browser.i18n.getMessage('config_groupTabTemporaryStateForAPI_label'), + expert: true, + children: [ + { + title: browser.i18n.getMessage('config_groupTabTemporaryState_option_default'), + key: 'groupTabTemporaryStateForAPI', + value: Constants.kGROUP_TAB_TEMPORARY_STATE_NOTHING, + type: 'radio' + }, + { + title: `${browser.i18n.getMessage('config_groupTabTemporaryState_option_checked_before')}${browser.i18n.getMessage('groupTab_temporary_label')}${browser.i18n.getMessage('config_groupTabTemporaryState_option_checked_after')}`, + key: 'groupTabTemporaryStateForAPI', + value: Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE, + type: 'radio' + }, + { + title: `${browser.i18n.getMessage('config_groupTabTemporaryState_option_checked_before')}${browser.i18n.getMessage('groupTab_temporaryAggressive_label')}${browser.i18n.getMessage('config_groupTabTemporaryState_option_checked_after')}`, + key: 'groupTabTemporaryStateForAPI', + value: Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE, + type: 'radio' + }, + ], + }, + ], }, { title: browser.i18n.getMessage('config_treeBehavior_caption'), diff --git a/waterfox/browser/components/sidebar/background/handle-misc.js b/waterfox/browser/components/sidebar/background/handle-misc.js index 77e8f8eff7ab9..b0de426fde4cb 100644 --- a/waterfox/browser/components/sidebar/background/handle-misc.js +++ b/waterfox/browser/components/sidebar/background/handle-misc.js @@ -264,7 +264,7 @@ async function onShortcutCommand(command) { return; case 'simulateUpOnTree': - if (SidebarConnection.isSidebarOpen(activeTab.windowId)) { + if (SidebarConnection.isOpen(activeTab.windowId)) { if (configs.faviconizePinnedTabs && (activeTab.pinned || activeTab == Tab.getFirstNormalTab(activeTab.windowId))) { @@ -291,7 +291,7 @@ async function onShortcutCommand(command) { } return; case 'simulateDownOnTree': - if (SidebarConnection.isSidebarOpen(activeTab.windowId)) { + if (SidebarConnection.isOpen(activeTab.windowId)) { if (configs.faviconizePinnedTabs && activeTab.pinned) { const nextActiveId = await browser.runtime.sendMessage({ @@ -317,7 +317,7 @@ async function onShortcutCommand(command) { } return; case 'simulateLeftOnTree': - if (SidebarConnection.isSidebarOpen(activeTab.windowId)) { + if (SidebarConnection.isOpen(activeTab.windowId)) { if (configs.faviconizePinnedTabs && activeTab.pinned) { const nextActiveId = await browser.runtime.sendMessage({ @@ -342,7 +342,7 @@ async function onShortcutCommand(command) { } return; case 'simulateRightOnTree': - if (SidebarConnection.isSidebarOpen(activeTab.windowId)) { + if (SidebarConnection.isOpen(activeTab.windowId)) { if (configs.faviconizePinnedTabs && activeTab.pinned) { const nextActiveId = await browser.runtime.sendMessage({ @@ -453,12 +453,14 @@ async function onShortcutCommand(command) { function focusPrevious(activeTab) { const nextActive = activeTab.$TST.nearestVisiblePrecedingTab || + (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) || Tab.getLastVisibleTab(activeTab.windowId); TabsInternalOperation.activateTab(nextActive); } function focusPreviousSilently(activeTab) { const nextActive = activeTab.$TST.nearestVisiblePrecedingTab || + (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) || Tab.getLastVisibleTab(activeTab.windowId); TabsInternalOperation.activateTab(nextActive, { silently: true, @@ -466,13 +468,15 @@ function focusPreviousSilently(activeTab) { } function focusNext(activeTab) { - const nextActive = activeTab.$TST.nextVisibleTab || + const nextActive = activeTab.$TST.nearestVisibleFollowingTab || + (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) || Tab.getFirstVisibleTab(activeTab.windowId); TabsInternalOperation.activateTab(nextActive); } function focusNextSilently(activeTab) { const nextActive = activeTab.$TST.nearestVisibleFollowingTab || + (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) || Tab.getFirstVisibleTab(activeTab.windowId); TabsInternalOperation.activateTab(nextActive, { silently: true, @@ -953,9 +957,27 @@ function onMessageExternal(message, sender) { case TSTAPI.kGROUP_TABS: return (async () => { const tabs = await TSTAPI.getTargetTabs(message, sender); + const temporaryStateParams = (message.temporary && !message.temporaryAggressive) ? + { + temporary: true, + temporaryAggressive: false, + } : + (!message.temporary && message.temporaryAggressive) ? + { + temporary: false, + temporaryAggressive: true, + } : + (message.temporaryAggressive === false && message.temporary === false) ? + { + temporary: false, + temporaryAggressive: false, + } : + {}; const tab = await TabsGroup.groupTabs(Array.from(tabs), { title: message.title, broadcast: true, + ...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForAPI), + ...temporaryStateParams, }); if (!tab) return null; diff --git a/waterfox/browser/components/sidebar/background/handle-moved-tabs.js b/waterfox/browser/components/sidebar/background/handle-moved-tabs.js index b819f6134c693..9060dc748ee64 100644 --- a/waterfox/browser/components/sidebar/background/handle-moved-tabs.js +++ b/waterfox/browser/components/sidebar/background/handle-moved-tabs.js @@ -52,8 +52,7 @@ Tab.onCreated.addListener((tab, info = {}) => { info.movedBySelfWhileCreation) && (tab.$TST.nearestCompletelyOpenedNormalFollowingTab || tab.$TST.nearestCompletelyOpenedNormalPrecedingTab || - (info.treeForActionDetection && - info.treeForActionDetection.target && + (info.treeForActionDetection?.target && (info.treeForActionDetection.target.next || info.treeForActionDetection.target.previous)))) { tryFixupTreeForInsertedTab(tab, { @@ -73,21 +72,21 @@ Tab.onMoving.addListener((tab, moveInfo) => { const win = TabsStore.windows.get(tab.windowId); const isNewlyOpenedTab = win.openingTabs.has(tab.id); const positionControlled = configs.insertNewChildAt != Constants.kINSERT_NO_CONTROL; - if (isNewlyOpenedTab && - !moveInfo.byInternalOperation && - !moveInfo.alreadyMoved && - !moveInfo.isSubstantiallyMoved && - positionControlled) { - const opener = tab.$TST.openerTab; - // 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 (opener) { - log('onTabMove for new child tab: move back '+moveInfo.toIndex+' => '+moveInfo.fromIndex); - moveBack(tab, moveInfo); - return false; - } - } - return true; + 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 = {}) { @@ -96,6 +95,7 @@ async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) { ...moveInfo, }); log('tryFixupTreeForInsertedTab ', { + tab: tab.id, parentTabOperationBehavior, moveInfo, childIds: tab.$TST.childIds, @@ -118,9 +118,10 @@ async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) { broadcast: true }); } - await Tree.detachTab(tab, { - 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(() => { @@ -134,11 +135,19 @@ async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) { 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( @@ -155,22 +164,26 @@ async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) { { tabProperties: ['tab', 'parent', 'insertBefore', 'insertAfter'], cache } ); TSTAPI.clearCache(cache); + if (!allowed) { log('no action - canceled by a helper addon'); - return; } + else { + log('action: ', action); + switch (action.action) { + case 'invalid': + moveBack(tab, moveInfo); + break; - log('action: ', action); - switch (action.action) { - case 'invalid': - moveBack(tab, moveInfo); - return; - - default: - log('tryFixupTreeForInsertedTab: apply action for unattached tab: ', tab, action); - await action.apply(); - return; + 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) { @@ -196,14 +209,14 @@ function reserveToEnsureRootTabVisible(tab) { reserveToEnsureRootTabVisible.tabIds = new Set(); Tab.onMoved.addListener((tab, moveInfo = {}) => { - if (!moveInfo.byInternalOperation && - !moveInfo.isSubstantiallyMoved && - !tab.$TST.duplicating) { - log('process moved tab'); - tryFixupTreeForInsertedTab(tab, moveInfo); + if (moveInfo.byInternalOperation || + !moveInfo.movedInBulk || + tab.$TST.duplicating) { + log('internal move'); } else { - log('internal move'); + log('process moved tab'); + tryFixupTreeForInsertedTab(tab, moveInfo); } reserveToEnsureRootTabVisible(tab); }); @@ -255,7 +268,8 @@ 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); - win.internalMovingTabs.add(id); + 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 @@ -266,7 +280,7 @@ function moveBack(tab, moveInfo) { windowId: moveInfo.windowId, index: moveInfo.fromIndex }).catch(ApiTabs.createErrorHandler(e => { - if (win.internalMovingTabs.has(id)) + if (win.internalMovingTabs.get(id) == index) win.internalMovingTabs.delete(id); ApiTabs.handleMissingTabError(e); })); diff --git a/waterfox/browser/components/sidebar/background/handle-tab-bunches.js b/waterfox/browser/components/sidebar/background/handle-tab-bunches.js index edb58d022b7dd..52483a8fc0a37 100644 --- a/waterfox/browser/components/sidebar/background/handle-tab-bunches.js +++ b/waterfox/browser/components/sidebar/background/handle-tab-bunches.js @@ -104,9 +104,14 @@ async function tryDetectTabBunches(win) { } if (tabReferences.length > 1) { - for (const tabReference of tabReferences) { - Tab.get(tabReference.id).$TST.temporaryMetadata.set('openedWithOthers', true); - } + await Promise.all(tabReferences.map(tabReference => { + const tab = Tab.get(tabReference.id); + tab.$TST.temporaryMetadata.set('openedWithOthers', true); + // We need to wait until all tabs are handlede completely. + // Otherwise `tab.$TST.needToBeGroupedSiblings` may contain unrelated tabs + // (tabs opened from any other parent tab) unexpectedly. + return tab.$TST.opened; + })); } if (areTabsFromOtherDeviceWithInsertAfterCurrent(tabReferences) && diff --git a/waterfox/browser/components/sidebar/background/prefs.js b/waterfox/browser/components/sidebar/background/prefs.js index 780dccd36de83..7ae0b866800aa 100644 --- a/waterfox/browser/components/sidebar/background/prefs.js +++ b/waterfox/browser/components/sidebar/background/prefs.js @@ -11,8 +11,6 @@ import { } from '/common/common.js'; import * as Constants from '/common/constants.js'; -import Tab from '/common/Tab.js'; - export const onChanged = new EventListenerManager(); if (Constants.IS_BACKGROUND) { @@ -100,16 +98,3 @@ configs.$addObserver(async name => { mNamesSyncToChrome.delete(name); }); }); - -browser.waterfoxBridge.onHoverPreviewChanged.addListener(enabled => { - if (enabled) { - for (const tab of Tab.getAllTabs()) { - tab.$TST.registerTooltipText(browser.runtime.id, '', true); - } - } - else { - for (const tab of Tab.getAllTabs()) { - tab.$TST.unregisterTooltipText(browser.runtime.id); - } - } -}); diff --git a/waterfox/browser/components/sidebar/background/successor-tab.js b/waterfox/browser/components/sidebar/background/successor-tab.js index 3b80bab5d50d7..b931a5a735cc3 100644 --- a/waterfox/browser/components/sidebar/background/successor-tab.js +++ b/waterfox/browser/components/sidebar/background/successor-tab.js @@ -14,6 +14,7 @@ import { 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'; @@ -42,6 +43,17 @@ browser.tabs.onUpdated.addListener((tabId, updateInfo, _tab) => { // properties: ['successorTabId'], }); +TabsInternalOperation.onBeforeTabsRemove.addListener(async tabs => { + let activeTab = null; + const tabIds = tabs.map(tab => { + if (tab.active) + activeTab = tab; + return tab.id; + }); + if (activeTab) + await updateInternal(activeTab.id, tabIds); +}); + function setSuccessor(tabId, successorTabId = -1) { const tab = Tab.get(tabId); const successorTab = Tab.get(successorTabId); @@ -133,7 +145,7 @@ function update(tabId) { } }, 0); } -async function updateInternal(tabId) { +async function updateInternal(tabId, excludeTabIds = []) { // tabs.onActivated can be notified before the tab is completely tracked... await Tab.waitUntilTracked(tabId); const tab = Tab.get(tabId); @@ -190,6 +202,15 @@ async function updateInternal(tabId) { let successor = null; if (renewedTab.active) { log('it is active, so reset successor'); + const excludeTabIdsSet = new Set(excludeTabIds); + const findSuccessor = (...candidates) => { + for (const candidate of candidates) { + if (!excludeTabIdsSet.has(candidate?.id) && + candidate) + return candidate; + } + return null; + }; if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_IN_TREE) { const closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, { context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE, @@ -203,7 +224,11 @@ async function updateInternal(tabId) { const firstChild = collapsedChildSuccessorAllowed ? tab.$TST.firstChild : tab.$TST.firstVisibleChild; const nextVisibleSibling = tab.$TST.nextVisibleSiblingTab; const nearestVisiblePreceding = tab.$TST.nearestVisiblePrecedingTab; - successor = firstChild || nextVisibleSibling || nearestVisiblePreceding; + successor = findSuccessor( + firstChild, + nextVisibleSibling, + nearestVisiblePreceding + ); log(` possible successor: ${dumpTab(tab)}: `, successor, { closeParentBehavior, collapsedChildSuccessorAllowed, @@ -216,22 +241,29 @@ async function updateInternal(tabId) { successor.discarded && configs.avoidDiscardedTabToBeActivatedIfPossible) { log(` ${dumpTab(successor)} is discarded.`); - successor = tab.$TST.nearestLoadedSiblingTab || - tab.$TST.nearestLoadedTabInTree || - tab.$TST.nearestLoadedTab || - successor; + successor = findSuccessor( + tab.$TST.nearestLoadedSiblingTab, + tab.$TST.nearestLoadedTabInTree, + tab.$TST.nearestLoadedTab, + successor + ); log(` => redirected successor is: ${dumpTab(successor)}`); } } else { - successor = tab.$TST.nearestVisibleFollowingTab || tab.$TST.nearestVisiblePrecedingTab; + successor = findSuccessor( + tab.$TST.nearestVisibleFollowingTab, + tab.$TST.nearestVisiblePrecedingTab + ); log(` possible successor: ${dumpTab(tab)}`); if (successor && successor.discarded && configs.avoidDiscardedTabToBeActivatedIfPossible) { log(` ${dumpTab(successor)} is discarded.`); - successor = tab.$TST.nearestLoadedTab || - successor; + successor = findSuccessor( + tab.$TST.nearestLoadedTab, + successor + ); log(` => redirected successor is: ${dumpTab(successor)}`); } } @@ -243,8 +275,7 @@ async function updateInternal(tabId) { } else { log(` ${dumpTab(tab)} is out of control.`, { - active: renewedTab.active, - successor: successor && successor.id + active: renewedTab.active, }); clearSuccessor(renewedTab.id); } diff --git a/waterfox/browser/components/sidebar/background/tab-context-menu.js b/waterfox/browser/components/sidebar/background/tab-context-menu.js index 33ae764db4a1f..faeb564c9afd4 100644 --- a/waterfox/browser/components/sidebar/background/tab-context-menu.js +++ b/waterfox/browser/components/sidebar/background/tab-context-menu.js @@ -197,6 +197,9 @@ const mItemsById = { title: browser.i18n.getMessage('tabContextMenu_close_label'), titleMultiselected: browser.i18n.getMessage('tabContextMenu_close_label_multiselected') }, + 'context_closeDuplicatedTabs': { + title: browser.i18n.getMessage('tabContextMenu_closeDuplicatedTabs_label') + }, 'context_closeMultipleTabs': { title: browser.i18n.getMessage('tabContextMenu_closeMultipleTabs_label') }, @@ -694,6 +697,7 @@ async function onShown(info, contextTab) { const previousSiblingTab = contextTab && contextTab.$TST.previousSiblingTab; const nextTab = contextTab && contextTab.$TST.nextTab; const nextSiblingTab = contextTab && contextTab.$TST.nextSiblingTab; + const hasDuplicatedTabs = Tab.hasDuplicatedTabs(windowId); const hasMultipleTabs = Tab.hasMultipleTabs(windowId); const hasMultipleNormalTabs = Tab.hasMultipleTabs(windowId, { normal: true }); const multiselected = contextTab && contextTab.$TST.multiselected; @@ -908,6 +912,11 @@ async function onShown(info, contextTab) { multiselected }) && modifiedItemsCount++; + updateItem('context_closeDuplicatedTabs', { + visible: emulate && !!contextTab, + enabled: hasDuplicatedTabs, + multiselected + }) && modifiedItemsCount++; updateItem('context_closeMultipleTabs', { visible: emulate && !!contextTab, enabled: hasMultipleNormalTabs, @@ -1251,6 +1260,28 @@ async function onClick(info, contextTab) { case 'context_bookmarkSelected': Commands.bookmarkTab(contextTab || activeTab); break; + case 'context_closeDuplicatedTabs': { + const tabs = await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler()); + tabs.sort((a, b) => b.lastAccessed - a.lastAccessed); + const tabKeys = new Set(); + const closeTabs = []; + for (const tab of tabs) { + const key = `${tab.cookieStoreId}\n${tab.url}`; + if (tabKeys.has(key)) { + closeTabs.push(Tab.get(tab.id)); + continue; + } + tabKeys.add(key); + } + const canceled = (await browser.runtime.sendMessage({ + type: Constants.kCOMMAND_NOTIFY_TABS_CLOSING, + tabs: closeTabs.map(tab => tab.$TST.sanitized), + windowId, + }).catch(ApiTabs.createErrorHandler())) === false; + if (canceled) + break; + TabsInternalOperation.removeTabs(closeTabs); + } break; case 'context_closeTabsToTheStart': { const tabs = await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler()); const closeTabs = []; diff --git a/waterfox/browser/components/sidebar/background/tabs-group.js b/waterfox/browser/components/sidebar/background/tabs-group.js index 977594b7075db..cfdfc0601e2a3 100644 --- a/waterfox/browser/components/sidebar/background/tabs-group.js +++ b/waterfox/browser/components/sidebar/background/tabs-group.js @@ -30,7 +30,7 @@ function log(...args) { internalLogger('background/tabs-group', ...args); } -export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId } = {}) { +export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId, replacedParentCount } = {}) { const url = new URL(Constants.kGROUP_TAB_URI); if (title) @@ -47,19 +47,31 @@ export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerT if (aliasTabId) url.searchParams.set('aliasTabId', aliasTabId); + if (replacedParentCount) + url.searchParams.set('replacedParentCount', replacedParentCount); + return url.href; } export function temporaryStateParams(state) { switch (state) { case Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE: - return { temporary: true }; + return { + temporary: true, + temporaryAggressive: false, + }; case Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE: - return { temporaryAggressive: true }; + return { + temporary: false, + temporaryAggressive: true, + }; default: break; } - return {}; + return { + temporary: false, + temporaryAggressive: false, + }; } export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...groupTabOptions } = {}) { @@ -67,7 +79,7 @@ export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...g if (rootTabs.length <= 0) return null; - log('groupTabs: ', () => tabs.map(dumpTab)); + log('groupTabs: ', () => tabs.map(dumpTab), { broadcast, parent, withDescendants }); const uri = makeGroupTabURI({ title: browser.i18n.getMessage('groupTab_label', rootTabs[0].title), @@ -81,10 +93,19 @@ export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...g inBackground: true }); - if (!withDescendants) + if (!withDescendants) { + const structure = TreeBehavior.getTreeStructureFromTabs(tabs); + await Tree.detachTabsFromTree(tabs, { - broadcast: !!broadcast + broadcast: !!broadcast, }); + + log('structure: ', structure); + await Tree.applyTreeStructureToTabs(tabs, structure, { + broadcast: !!broadcast, + }); + } + await TabsMove.moveTabsAfter(tabs.slice(1), tabs[0], { broadcast: !!broadcast }); @@ -92,7 +113,7 @@ export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...g await Tree.attachTabTo(tab, groupTab, { forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree dontMove: true, - broadcast: !!broadcast + broadcast: !!broadcast, }); } return groupTab; @@ -163,7 +184,8 @@ export async function tryReplaceTabWithGroup(tab, { windowId, parent, children, const firstChild = children[0]; const uri = makeGroupTabURI({ title: browser.i18n.getMessage('groupTab_label', firstChild.title), - ...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs) + ...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs), + replacedParentCount: (tab?.$TST?.replacedParentGroupTabCount || 0) + 1, }); const win = TabsStore.windows.get(windowId); win.toBeOpenedTabsWithPositions++; diff --git a/waterfox/browser/components/sidebar/background/tabs-move.js b/waterfox/browser/components/sidebar/background/tabs-move.js index 1edb53a9c6432..1de08e9d9c119 100644 --- a/waterfox/browser/components/sidebar/background/tabs-move.js +++ b/waterfox/browser/components/sidebar/background/tabs-move.js @@ -14,7 +14,7 @@ * The Original Code is the Tree Style Tab. * * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi. - * Portions created by the Initial Developer are Copyright (C) 2011-2023 + * Portions created by the Initial Developer are Copyright (C) 2011-2024 * the Initial Developer. All Rights Reserved. * * Contributor(s): YUKI "Piro" Hiroshi @@ -101,15 +101,15 @@ async function moveTabsInternallyBefore(tabs, referenceTab, options = {}) { const oldNextTab = tab.$TST.unsafeNextTab; if (oldNextTab && oldNextTab.id == referenceTab.id) // no move case continue; - if (SidebarConnection.isInitialized()) { // only on the background page - win.internalMovingTabs.add(tab.id); - win.alreadyMovedTabs.add(tab.id); - } const fromIndex = tab.index; if (referenceTab.index > tab.index) tab.index = referenceTab.index - 1; else tab.index = referenceTab.index; + if (SidebarConnection.isInitialized()) { // only on the background page + win.internalMovingTabs.set(tab.id, tab.index); + win.alreadyMovedTabs.set(tab.id, tab.index); + } tab.reindexedBy = `moveTabsInternallyBefore (${tab.index})`; Tab.track(tab); movedTabs.push(tab); @@ -211,10 +211,6 @@ async function moveTabsInternallyAfter(tabs, referenceTab, options = {}) { if ((!oldNextTab && !nextTab) || (oldNextTab && nextTab && oldNextTab.id == nextTab.id)) // no move case continue; - if (SidebarConnection.isInitialized()) { // only on the background page - win.internalMovingTabs.add(tab.id); - win.alreadyMovedTabs.add(tab.id); - } const fromIndex = tab.index; if (nextTab) { if (nextTab.index > tab.index) @@ -225,6 +221,10 @@ async function moveTabsInternallyAfter(tabs, referenceTab, options = {}) { else { tab.index = win.tabs.size - 1 } + if (SidebarConnection.isInitialized()) { // only on the background page + win.internalMovingTabs.set(tab.id, tab.index); + win.alreadyMovedTabs.set(tab.id, tab.index); + } tab.reindexedBy = `moveTabsInternallyAfter (${tab.index})`; Tab.track(tab); movedTabs.push(tab); @@ -363,8 +363,8 @@ async function syncToNativeTabsInternal(windowId) { toIndex--; log(`syncToNativeTabs(${windowId}): step1, move ${moveTabIds.join(',')} before ${referenceId} / from = ${fromIndex}, to = ${toIndex}`); for (const movedId of moveTabIds) { - win.internalMovingTabs.add(movedId); - win.alreadyMovedTabs.add(movedId); + win.internalMovingTabs.set(movedId, -1); + win.alreadyMovedTabs.set(movedId, -1); movedTabs.add(movedId); } logApiTabs(`tabs-move:syncToNativeTabs(${windowId}): step1, browser.tabs.move() `, moveTabIds, { diff --git a/waterfox/browser/components/sidebar/background/tabs-open.js b/waterfox/browser/components/sidebar/background/tabs-open.js index 5ab817910df55..22f2197b75bba 100644 --- a/waterfox/browser/components/sidebar/background/tabs-open.js +++ b/waterfox/browser/components/sidebar/background/tabs-open.js @@ -299,7 +299,8 @@ function onMessage(message, openerTab) { windowId: message.windowId, parent: Tab.get(message.parentId), insertBefore: Tab.get(message.insertBeforeId), - insertAfter: Tab.get(message.insertAfterId) + insertAfter: Tab.get(message.insertAfterId), + active: !!message.active, }); }); break; diff --git a/waterfox/browser/components/sidebar/background/tree.js b/waterfox/browser/components/sidebar/background/tree.js index fd12819d6c10e..494b732546ba4 100644 --- a/waterfox/browser/components/sidebar/background/tree.js +++ b/waterfox/browser/components/sidebar/background/tree.js @@ -455,11 +455,11 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab case Constants.kINSERT_END: default: insertAfter = lastDescendant; - log(` insert ${child && child.id} after lastDescendant ${insertAfter && insertAfter.id} (insertAt=kINSERT_END)`); + log(` insert ${child?.id} after lastDescendant ${insertAfter?.id} (insertAt=kINSERT_END)`); break; case Constants.kINSERT_TOP: insertBefore = firstChild; - log(` insert ${child && child.id} before firstChild ${insertBefore && insertBefore.id} (insertAt=kINSERT_TOP)`); + log(` insert ${child?.id} before firstChild ${insertBefore?.id} (insertAt=kINSERT_TOP)`); break; case Constants.kINSERT_NEAREST: { const allTabs = Tab.getOtherTabs((child || parent).windowId, ignoreTabs); @@ -468,11 +468,11 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab if (index < allTabs.indexOf(firstChild)) { insertBefore = firstChild; insertAfter = parent; - log(` insert ${child && child.id} between parent ${insertAfter && insertAfter.id} and firstChild ${insertBefore && insertBefore.id} (insertAt=kINSERT_NEAREST)`); + log(` insert ${child?.id} between parent ${insertAfter?.id} and firstChild ${insertBefore?.id} (insertAt=kINSERT_NEAREST)`); } else if (index > allTabs.indexOf(lastDescendant)) { insertAfter = lastDescendant; - log(` insert ${child && child.id} after lastDescendant ${insertAfter && insertAfter.id} (insertAt=kINSERT_NEAREST)`); + log(` insert ${child?.id} after lastDescendant ${insertAfter?.id} (insertAt=kINSERT_NEAREST)`); } else { // inside the tree if (parent && !children) @@ -483,12 +483,12 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab if (index > allTabs.indexOf(child)) continue; insertBefore = child; - log(` insert ${child && child.id} before nearest following child ${insertBefore && insertBefore.id} (insertAt=kINSERT_NEAREST)`); + log(` insert ${child?.id} before nearest following child ${insertBefore?.id} (insertAt=kINSERT_NEAREST)`); break; } if (!insertBefore) { insertAfter = lastDescendant; - log(` insert ${child && child.id} after lastDescendant ${insertAfter && insertAfter.id} (insertAt=kINSERT_NEAREST)`); + log(` insert ${child?.id} after lastDescendant ${insertAfter?.id} (insertAt=kINSERT_NEAREST)`); } } }; break; @@ -500,11 +500,11 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab lastRelatedTab = child && parent.$TST.lastRelatedTabId == child.id ? parent.$TST.previousLastRelatedTab : parent.$TST.lastRelatedTab; // it could be updated already... if (lastRelatedTab) { insertAfter = lastRelatedTab.$TST.lastDescendant || lastRelatedTab; - log(` insert ${child && child.id} after lastRelatedTab ${lastRelatedTab.id} (insertAt=kINSERT_NEXT_TO_LAST_RELATED_TAB)`); + log(` insert ${child?.id} after lastRelatedTab ${lastRelatedTab.id} (insertAt=kINSERT_NEXT_TO_LAST_RELATED_TAB)`); } else { insertBefore = firstChild; - log(` insert ${child && child.id} before firstChild (insertAt=kINSERT_NEXT_TO_LAST_RELATED_TAB)`); + log(` insert ${child?.id} before firstChild (insertAt=kINSERT_NEXT_TO_LAST_RELATED_TAB)`); } }; break; case Constants.kINSERT_NO_CONTROL: @@ -513,23 +513,23 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab } else { insertAfter = parent; - log(` insert ${child && child.id} after parent`); + log(` insert ${child?.id} after parent`); } if (insertBefore == child) { // Return unsafe tab, to avoid placing the child after hidden tabs // (too far from the place it should be.) - insertBefore = insertBefore && insertBefore.$TST.unsafeNextTab; - log(` => insert ${child && child.id} before next tab ${insertBefore && insertBefore.id} of the child tab itelf`); + insertBefore = insertBefore?.$TST.unsafeNextTab; + log(` => insert ${child?.id} before next tab ${insertBefore?.id} of the child tab itelf`); } if (insertAfter == child) { - insertAfter = insertAfter && insertAfter.$TST.previousTab; - log(` => insert ${child && child.id} after previous tab ${insertAfter && insertAfter.id} of the child tab itelf`); + insertAfter = insertAfter?.$TST.previousTab; + log(` => insert ${child?.id} after previous tab ${insertAfter?.id} of the child tab itelf`); } // disallow to place tab in invalid position if (insertBefore) { if (parent && insertBefore.index <= parent.index) { insertBefore = null; - log(` => do not put ${child && child.id} before a tab preceding to the parent`); + log(` => do not put ${child?.id} before a tab preceding to the parent`); } //TODO: we need to reject more cases... } @@ -541,7 +541,7 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab if (lastMember != insertAfter && insertAfter.index >= lastMember.index) { insertAfter = lastMember; - log(` => do not put ${child && child.id} after the last tab ${insertAfter && insertAfter.id} in the tree`); + log(` => do not put ${child?.id} after the last tab ${insertAfter?.id} in the tree`); } //TODO: we need to reject more cases... } @@ -551,8 +551,7 @@ export function getReferenceTabsForNewChild(child, parent, { insertAt, ignoreTab export function getReferenceTabsForNewNextSibling(base, options = {}) { log('getReferenceTabsForNewNextSibling ', base); let insertBefore = base.$TST.nextSiblingTab; - if (insertBefore && - insertBefore.pinned && + if (insertBefore?.pinned && !options.pinned) { insertBefore = Tab.getFirstNormalTab(base.windowId); } @@ -634,6 +633,7 @@ export async function detachTabsFromTree(tabs, options = {}) { promisedAttach.push(detachAllChildren(tab, { ...options, behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD, + ignoreTabs: tabs, })); } if (promisedAttach.length > 0) @@ -642,7 +642,7 @@ export async function detachTabsFromTree(tabs, options = {}) { export async function detachAllChildren( tab = null, - { windowId, children, descendants, parent, nearestFollowingRootTab, newParent, behavior, dontExpand, dontSyncParentToOpenerTab, + { windowId, children, descendants, parent, nearestFollowingRootTab, newParent, ignoreTabs, behavior, dontExpand, dontSyncParentToOpenerTab, ...options } = {} ) { if (tab) { @@ -652,12 +652,14 @@ export async function detachAllChildren( descendants = tab.$TST.descendants; } log('detachAllChildren: ', - tab && tab.id, + tab?.id, { children, parent, nearestFollowingRootTab, newParent, behavior, dontExpand, dontSyncParentToOpenerTab }, options); // the "children" option is used for removing tab. children = children ? children.map(TabsStore.ensureLivingTab) : tab.$TST.children; + const ignoreTabsSet = new Set(ignoreTabs || []); + if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD && newParent && !children.includes(newParent)) @@ -675,9 +677,11 @@ export async function detachAllChildren( options.dontUpdateInsertionPositionInfo = true; // the "parent" option is used for removing tab. - parent = TabsStore.ensureLivingTab(parent) || (tab && tab.$TST.parent); - if (tab && - tab.$TST.isGroupTab && + parent = TabsStore.ensureLivingTab(parent) || tab?.$TST.parent; + while (ignoreTabsSet.has(parent)) { + parent = parent.$TST.parent; + } + if (tab?.$TST.isGroupTab && Tab.getRemovingTabs(tab.windowId).length == children.length) { behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN; options.dontUpdateIndent = false; @@ -689,7 +693,7 @@ export async function detachAllChildren( !configs.moveTabsToBottomWhenDetachedFromClosedParent) { nextTab = nearestFollowingRootTab !== undefined ? nearestFollowingRootTab : - tab && tab.$TST.nearestFollowingRootTab; + tab?.$TST.nearestFollowingRootTab; previousTab = nextTab ? nextTab.$TST.previousTab : Tab.getLastTab(windowId || tab.windowId); @@ -734,9 +738,9 @@ export async function detachAllChildren( promises.push(detachTab(child, { ...options, dontSyncParentToOpenerTab })); // reference tabs can be closed while waiting... - if (nextTab && nextTab.$TST.removing) + if (nextTab?.$TST.removing) nextTab = null; - if (previousTab && previousTab.$TST.removing) + if (previousTab?.$TST.removing) previousTab = null; if (nextTab) { @@ -801,8 +805,7 @@ export async function behaveAutoAttachedTab( baseTab = baseTab || Tab.getActiveTab(TabsStore.getCurrentWindowId() || tab.windowId); log('behaveAutoAttachedTab ', tab.id, baseTab.id, { baseTab, behavior }); - if (baseTab && - baseTab.$TST.ancestors.includes(tab)) { + if (baseTab?.$TST.ancestors.includes(tab)) { log(' => ignore possibly restored ancestor tab to avoid cyclic references'); return false; } @@ -1095,7 +1098,7 @@ async function collapseExpandSubtreeInternal(tab, params = {}) { tabId: tab.id, collapsed: !!params.collapsed, justNow: params.justNow, - anchorId: anchor && anchor.id, + anchorId: anchor?.id, visibilityChangedTabIds, last: true }); @@ -1231,7 +1234,7 @@ export async function collapseExpandTab(tab, params = {}) { type: Constants.kCOMMAND_NOTIFY_TAB_COLLAPSED_STATE_CHANGED, windowId: tab.windowId, tabId: tab.id, - anchorId: collapseExpandInfo.anchor && collapseExpandInfo.anchor.id, + anchorId: collapseExpandInfo.anchor?.id, justNow: params.justNow, collapsed: params.collapsed, last, @@ -1257,55 +1260,60 @@ export async function collapseExpandTreesIntelligentlyFor(tab, options = {}) { } win.doingIntelligentlyCollapseExpandCount++; - const expandedAncestors = [tab.id] - .concat(tab.$TST.ancestors.map(ancestor => ancestor.id)) - .concat(tab.$TST.descendants.map(descendant => descendant.id)); - const collapseTabs = Tab.getSubtreeCollapsedTabs(tab.windowId, { - '!id': expandedAncestors - }); - logCollapseExpand(`${collapseTabs.length} tabs can be collapsed, ancestors: `, expandedAncestors); - const allowedToCollapse = new Set(); - await Promise.all(collapseTabs.map(async tab => { - const allowed = await TSTAPI.tryOperationAllowed( - TSTAPI.kNOTIFY_TRY_COLLAPSE_TREE_FROM_OTHER_EXPANSION, - { tab }, - { tabProperties: ['tab'] } - ); - if (allowed) - allowedToCollapse.add(tab); - })); - for (const collapseTab of collapseTabs) { - if (!allowedToCollapse.has(collapseTab)) - continue; - let dontCollapse = false; - const parentTab = collapseTab.$TST.parent; - if (parentTab) { - dontCollapse = true; - if (!parentTab.$TST.subtreeCollapsed) { - for (const ancestor of collapseTab.$TST.ancestors) { - if (!expandedAncestors.includes(ancestor.id)) - continue; - dontCollapse = false; - break; + try { + const expandedAncestors = [tab.id] + .concat(tab.$TST.ancestors.map(ancestor => ancestor.id)) + .concat(tab.$TST.descendants.map(descendant => descendant.id)); + const collapseTabs = Tab.getSubtreeCollapsedTabs(tab.windowId, { + '!id': expandedAncestors + }); + logCollapseExpand(`${collapseTabs.length} tabs can be collapsed, ancestors: `, expandedAncestors); + const allowedToCollapse = new Set(); + await Promise.all(collapseTabs.map(async tab => { + const allowed = await TSTAPI.tryOperationAllowed( + TSTAPI.kNOTIFY_TRY_COLLAPSE_TREE_FROM_OTHER_EXPANSION, + { tab }, + { tabProperties: ['tab'] } + ); + if (allowed) + allowedToCollapse.add(tab); + })); + for (const collapseTab of collapseTabs) { + if (!allowedToCollapse.has(collapseTab)) + continue; + let dontCollapse = false; + const parentTab = collapseTab.$TST.parent; + if (parentTab) { + dontCollapse = true; + if (!parentTab.$TST.subtreeCollapsed) { + for (const ancestor of collapseTab.$TST.ancestors) { + if (!expandedAncestors.includes(ancestor.id)) + continue; + dontCollapse = false; + break; + } } } + logCollapseExpand(`${collapseTab.id}: dontCollapse = ${dontCollapse}`); + + const manuallyExpanded = collapseTab.$TST.states.has(Constants.kTAB_STATE_SUBTREE_EXPANDED_MANUALLY); + if (!dontCollapse && + !manuallyExpanded && + collapseTab.$TST.descendants.every(tab => !tab.$TST.canBecomeSticky)) + collapseExpandSubtree(collapseTab, { + ...options, + collapsed: true + }); } - logCollapseExpand(`${collapseTab.id}: dontCollapse = ${dontCollapse}`); - const manuallyExpanded = collapseTab.$TST.states.has(Constants.kTAB_STATE_SUBTREE_EXPANDED_MANUALLY); - if (!dontCollapse && - !manuallyExpanded && - collapseTab.$TST.descendants.every(tab => !tab.$TST.canBecomeSticky)) - collapseExpandSubtree(collapseTab, { - ...options, - collapsed: true - }); + collapseExpandSubtree(tab, { + ...options, + collapsed: false + }); + } + catch(error) { + log(`failed to collapse/expand tree under ${tab.id}: ${String(error)}`, error); } - - collapseExpandSubtree(tab, { - ...options, - collapsed: false - }); win.doingIntelligentlyCollapseExpandCount--; } @@ -1345,12 +1353,12 @@ export async function fixupSubtreeCollapsedState(tab, options = {}) { export async function moveTabSubtreeBefore(tab, nextTab, options = {}) { if (!tab) return; - if (nextTab && nextTab.$TST.isAllPlacedBeforeSelf([tab].concat(tab.$TST.descendants))) { + if (nextTab?.$TST.isAllPlacedBeforeSelf([tab].concat(tab.$TST.descendants))) { log('moveTabSubtreeBefore:no need to move'); return; } - log('moveTabSubtreeBefore: ', tab.id, nextTab && nextTab.id); + log('moveTabSubtreeBefore: ', tab.id, nextTab?.id); const win = TabsStore.windows.get(tab.windowId); win.subTreeMovingCount++; try { @@ -1359,8 +1367,8 @@ export async function moveTabSubtreeBefore(tab, nextTab, options = {}) { throw new Error('the tab was removed before moving of descendants'); await followDescendantsToMovedRoot(tab, options); } - catch(e) { - log(`failed to move subtree: ${String(e)}`); + catch(error) { + log(`failed to move subtree: ${String(error)}`, error); } await wait(0); win.subTreeMovingCount--; @@ -1370,8 +1378,8 @@ export async function moveTabSubtreeAfter(tab, previousTab, options = {}) { if (!tab) return; - log('moveTabSubtreeAfter: ', tab.id, previousTab && previousTab.id); - if (previousTab && previousTab.$TST.isAllPlacedAfterSelf([tab].concat(tab.$TST.descendants))) { + log('moveTabSubtreeAfter: ', tab.id, previousTab?.id); + if (previousTab?.$TST.isAllPlacedAfterSelf([tab].concat(tab.$TST.descendants))) { log(' => no need to move'); return; } @@ -1384,8 +1392,8 @@ export async function moveTabSubtreeAfter(tab, previousTab, options = {}) { throw new Error('the tab was removed before moving of descendants'); await followDescendantsToMovedRoot(tab, options); } - catch(e) { - log(`failed to move subtree: ${String(e)}`); + catch(error) { + log(`failed to move subtree: ${String(error)}`, error); } await wait(0); win.subTreeMovingCount--; @@ -1399,7 +1407,12 @@ async function followDescendantsToMovedRoot(tab, options = {}) { const win = TabsStore.windows.get(tab.windowId); win.subTreeChildrenMovingCount++; win.subTreeMovingCount++; - await TabsMove.moveTabsAfter(tab.$TST.descendants, tab, options); + try { + await TabsMove.moveTabsAfter(tab.$TST.descendants, tab, options); + } + catch(error) { + log(`failed to move descendants of ${tab.id}: ${String(error)}`, error); + } win.subTreeChildrenMovingCount--; win.subTreeMovingCount--; } @@ -1529,8 +1542,7 @@ export async function moveTabs(tabs, options = {}) { log('moveTabs: all windows and tabs are ready, ', movedTabIds, destinationWindowId); let toIndex = (tabs.some(tab => tab.pinned) ? Tab.getPinnedTabs(destinationWindowId) : Tab.getAllTabs(destinationWindowId)).length; log('toIndex = ', toIndex); - if (options.insertBefore && - options.insertBefore.windowId == destinationWindowId) { + if (options.insertBefore?.windowId == destinationWindowId) { try { toIndex = Tab.get(options.insertBefore.id).index; } @@ -1539,8 +1551,7 @@ export async function moveTabs(tabs, options = {}) { log('options.insertBefore is unavailable'); } } - else if (options.insertAfter && - options.insertAfter.windowId == destinationWindowId) { + else if (options.insertAfter?.windowId == destinationWindowId) { try { toIndex = Tab.get(options.insertAfter.id).index + 1; } @@ -1886,8 +1897,8 @@ class TabActionForNewPosition { } export function detectTabActionFromNewPosition(tab, moveInfo = {}) { - const isTabCreating = moveInfo && !!moveInfo.isTabCreating; - const isMovingByShortcut = moveInfo && !!moveInfo.isMovingByShortcut; + const isTabCreating = !!moveInfo?.isTabCreating; + const isMovingByShortcut = !!moveInfo?.isMovingByShortcut; if (tab.pinned) return new TabActionForNewPosition(tab.$TST.parentId ? 'detach' : 'move', { @@ -1910,18 +1921,48 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { const prevTab = tree.tabsById[target.previous]; const nextTab = tree.tabsById[target.next]; - log('prevTab: ', dumpTab(prevTab)); - log('nextTab: ', dumpTab(nextTab)); - const prevParent = prevTab && tree.tabsById[prevTab.parent]; - const nextParent = nextTab && tree.tabsById[nextTab.parent]; + // When multiple tabs are moved at once by outside of TST (e.g. moving of multiselected tabs) + // this method may be called multiple times asynchronously before previous operation finishes. + // Thus we need to refer the calculated "parent" if it is given. + const futurePrevParent = Tab.get(Tab.get(prevTab?.id)?.$TST?.temporaryMetadata.get('goingToBeAttachedTo')); + const futureNextParent = Tab.get(Tab.get(nextTab?.id)?.$TST?.temporaryMetadata.get('goingToBeAttachedTo')); + + const prevParent = prevTab && tree.tabsById[prevTab.parent] || + snapshotTab(Tab.get(prevTab?.parent)) || // Given treeForActionDetection may not contain the parent tab, so failsafe + snapshotTab(futurePrevParent); + const nextParent = nextTab && tree.tabsById[nextTab.parent] || + snapshotTab(Tab.get(nextTab?.parent)) || // Given treeForActionDetection may not contain the parent tab, so failsafe + snapshotTab(futureNextParent); + if (prevParent) + tree.tabsById[prevParent.id] = prevParent; + if (nextParent) + tree.tabsById[nextParent.id] = nextParent; + + // Given treeForActionDetection may not contain the parent tab, so we fixup the information. + if (prevTab && + !prevTab.parent && + prevParent) { + prevTab.parent = prevParent.id; + prevTab.level = prevParent.level + 1; + } + if (nextTab && + !nextTab.parent && + nextParent) { + nextTab.parent = nextParent.id; + nextTab.level = nextParent.level + 1; + } + log('prevTab: ', dumpTab(prevTab), `parent: ${prevTab?.parent}`); + log('nextTab: ', dumpTab(nextTab), `parent: ${nextTab?.parent}`); const prevLevel = prevTab ? prevTab.level : -1 ; const nextLevel = nextTab ? nextTab.level : -1 ; log('prevLevel: '+prevLevel); log('nextLevel: '+nextLevel); - const oldParent = tree.tabsById[target.parent]; + const oldParent = tree.tabsById[target.parent] || snapshotTab(Tab.get(target.parent)); + if (oldParent) + tree.tabsById[oldParent.id] = oldParent; let newParent = null; let mustToApply = false; @@ -1936,7 +1977,7 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { tab, isTabCreating, isMovingByShortcut, - insertAfter: prevTab && prevTab.id, + insertAfter: prevTab?.id, mustToApply, }); } @@ -1948,7 +1989,7 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { } else if (oldParent && prevTab && - oldParent == prevTab) { + oldParent?.id == prevTab?.id) { log('=> no need to fix case'); newParent = oldParent; } @@ -1961,7 +2002,7 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { log('=> moved to last position'); let ancestor = oldParent; while (ancestor) { - if (ancestor == prevParent) { + if (ancestor.id == prevParent?.id) { log(' => moving in related tree: keep it attached in existing tree'); newParent = prevParent; break; @@ -1971,15 +2012,15 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { if (!newParent) { log(' => moving from other tree: keep it orphaned'); } - mustToApply = !!oldParent && newParent != oldParent; + mustToApply = !!oldParent && newParent?.id != oldParent.id; } - else if (prevParent == nextParent) { + else if (prevParent?.id == nextParent?.id) { log('=> moved into existing tree'); newParent = prevParent; - mustToApply = !oldParent || newParent != oldParent; + mustToApply = !oldParent || newParent?.id != oldParent.id; } else if (prevLevel > nextLevel && - nextTab.parent != tab.id) { + nextTab?.parent != tab.id) { log('=> moved to end of existing tree'); if (!target.active && target.children.length == 0 && @@ -1992,35 +2033,35 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { const realDelta = Math.abs(toIndex - fromIndex); newParent = realDelta < 2 ? prevParent : (oldParent || nextParent) ; } - while (newParent && newParent.collapsed) { + while (newParent?.collapsed) { log('=> the tree is collapsed, up to parent tree') newParent = tree.tabsById[newParent.parent]; } - mustToApply = !!oldParent && newParent != oldParent; + mustToApply = !!oldParent && newParent?.id != oldParent.id; } else if (prevLevel < nextLevel && - nextTab.parent == prevTab.id) { + nextTab?.parent == prevTab?.id) { log('=> moved to first child position of existing tree'); newParent = prevTab || oldParent || nextParent; - mustToApply = !!oldParent && newParent != oldParent; + mustToApply = !!oldParent && newParent?.id != oldParent.id; } log('calculated parent: ', { - old: oldParent && oldParent.id, - new: newParent && newParent.id + old: oldParent?.id, + new: newParent?.id }); if (newParent) { let ancestor = newParent; while (ancestor) { - if (ancestor == target) { + if (ancestor.id == target.id) { if (moveInfo.toIndex - moveInfo.fromIndex == 1) { log('=> maybe move-down by keyboard shortcut or something.'); let nearestForeigner = tab.$TST.nearestFollowingForeignerTab; if (nearestForeigner && nearestForeigner == tab) nearestForeigner = nearestForeigner.$TST.nextTab; - log('nearest foreigner tab: ', nearestForeigner && nearestForeigner.id); + log('nearest foreigner tab: ', nearestForeigner?.id); if (nearestForeigner) { if (nearestForeigner.$TST.hasChild) return new TabActionForNewPosition('attach', { @@ -2054,8 +2095,8 @@ export function detectTabActionFromNewPosition(tab, moveInfo = {}) { isTabCreating, isMovingByShortcut, parent: newParent.id, - insertBefore: nextTab && nextTab.id, - insertAfter: prevTab && prevTab.id, + insertBefore: nextTab?.id, + insertAfter: prevTab?.id, mustToApply, }); } @@ -2085,11 +2126,11 @@ export function snapshotForActionDetection(targetTab) { const prevTab = targetTab.$TST.nearestCompletelyOpenedNormalPrecedingTab; const nextTab = targetTab.$TST.nearestCompletelyOpenedNormalFollowingTab; const tabs = Array.from(new Set([ - ...(prevTab && prevTab.$TST.ancestors || []), + ...(prevTab?.$TST?.ancestors || []), prevTab, targetTab, nextTab, - targetTab.$TST.parent + targetTab.$TST.parent, ])) .filter(TabsStore.ensureLivingTab) .sort((a, b) => a.index - b.index); @@ -2103,37 +2144,40 @@ function snapshotTree(targetTab, tabs) { function snapshotChild(tab) { if (!TabsStore.ensureLivingTab(tab) || tab.pinned) return null; - return snapshotById[tab.id] = { - id: tab.id, - url: tab.url, - cookieStoreId: tab.cookieStoreId, - active: tab.active, - children: tab.$TST.children.map(child => child.id), - collapsed: tab.$TST.subtreeCollapsed, - pinned: tab.pinned, - level: tab.$TST.ancestorIds.length, // parseInt(tab.$TST.getAttribute(Constants.kLEVEL) || 0), // we need to use the number of real ancestors instead of a cached "level", because it will be updated with delay - trackedAt: tab.$TST.trackedAt, - mayBeReplacedWithContainer: tab.$TST.mayBeReplacedWithContainer - }; + return snapshotById[tab.id] = snapshotTab(tab); } const snapshotArray = allTabs.map(tab => snapshotChild(tab)); for (const tab of allTabs) { const item = snapshotById[tab.id]; if (!item) continue; - const parent = tab.$TST.parent; - item.parent = parent && parent.id; - const next = tab.$TST.nearestCompletelyOpenedNormalFollowingTab; - item.next = next && next.id; - const previous = tab.$TST.nearestCompletelyOpenedNormalPrecedingTab; - item.previous = previous && previous.id; + item.parent = tab.$TST.parent?.id; + item.next = tab.$TST.nearestCompletelyOpenedNormalFollowingTab?.id; + item.previous = tab.$TST.nearestCompletelyOpenedNormalPrecedingTab?.id; } const activeTab = Tab.getActiveTab(targetTab.windowId); return { target: snapshotById[targetTab.id], active: activeTab && snapshotById[activeTab.id], tabs: snapshotArray, - tabsById: snapshotById + tabsById: snapshotById, + }; +} + +function snapshotTab(tab) { + if (!tab) + return null; + return { + id: tab.id, + url: tab.url, + cookieStoreId: tab.cookieStoreId, + active: tab.active, + children: tab.$TST.children.map(child => child.id), + collapsed: tab.$TST.subtreeCollapsed, + pinned: tab.pinned, + level: tab.$TST.level, // parseInt(tab.$TST.getAttribute(Constants.kLEVEL) || 0), // we need to use the number of real ancestors instead of a cached "level", because it will be updated with delay + trackedAt: tab.$TST.trackedAt, + mayBeReplacedWithContainer: tab.$TST.mayBeReplacedWithContainer, }; } diff --git a/waterfox/browser/components/sidebar/common/Tab.js b/waterfox/browser/components/sidebar/common/Tab.js index 4c9a484c9c4f2..84c3bfa9d8800 100644 --- a/waterfox/browser/components/sidebar/common/Tab.js +++ b/waterfox/browser/components/sidebar/common/Tab.js @@ -389,6 +389,10 @@ export default class Tab { return this.states.has(Constants.kTAB_STATE_STICKY); } + get stuck() { + return this.element?.parentNode?.classList.contains('sticky-tabs-container'); + } + get isNewTabCommandTab() { if (!this.tab || !configs.guessNewOrphanTabAsOpenedByNewTabCommand) @@ -444,6 +448,13 @@ export default class Tab { return (new URL(this.tab.url)).searchParams.get('temporaryAggressive') == 'true'; } + get replacedParentGroupTabCount() { + if (!this.tab || !this.isGroupTab) + return 0; + const count = parseInt((new URL(this.tab.url)).searchParams.get('replacedParentCount')); + return isNaN(count) ? 0 : count; + } + // Firefox Multi-Account Containers // https://addons.mozilla.org/firefox/addon/multi-account-containers/ // Temporary Containers @@ -962,6 +973,10 @@ export default class Tab { return ancestors; } + get level() { + return this.ancestorIds.length; + } + invalidateCachedAncestors() { this.cachedAncestorIds = null; for (const child of this.children) { @@ -1176,13 +1191,16 @@ export default class Tab { get needToBeGroupedSiblings() { if (!this.tab) return []; + const openerTabUniqueId = this.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID); + if (!openerTabUniqueId) + return []; return TabsStore.queryAll({ windowId: this.tab.windowId, tabs: TabsStore.toBeGroupedTabsInWindow.get(this.tab.windowId), normal: true, '!id': this.id, attributes: [ - Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, this.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID), + Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, openerTabUniqueId, Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, '' ], ordered: true @@ -1908,30 +1926,27 @@ export default class Tab { if (!this.tab) // already closed tab return; log(`memorizeNeighbors ${this.tab.id} as ${hint}`); - - const previousTab = this.unsafePreviousTab; - this.lastPreviousTabId = previousTab && previousTab.id; - - const nextTab = this.unsafeNextTab; - this.lastNextTabId = nextTab && nextTab.id; + this.lastPreviousTabId = this.unsafePreviousTab?.id; + this.lastNextTabId = this.unsafeNextTab?.id; } - get isSubstantiallyMoved() { + // https://github.com/piroor/treestyletab/issues/2309#issuecomment-518583824 + get movedInBulk() { const previousTab = this.unsafePreviousTab; if (this.lastPreviousTabId && - this.lastPreviousTabId != (previousTab && previousTab.id)) { - log(`isSubstantiallyMoved lastPreviousTabId=${this.lastNextTabId}, previousTab=${previousTab && previousTab.id}`); - return true; + this.lastPreviousTabId != previousTab?.id) { + log(`not bulkMoved lastPreviousTabId=${this.lastNextTabId}, previousTab=${previousTab?.id}`); + return false; } const nextTab = this.unsafeNextTab; if (this.lastNextTabId && - this.lastNextTabId != (nextTab && nextTab.id)) { - log(`isSubstantiallyMoved lastNextTabId=${this.lastNextTabId}, nextTab=${nextTab && nextTab.id}`); - return true; + this.lastNextTabId != nextTab?.id) { + log(`not bulkMoved lastNextTabId=${this.lastNextTabId}, nextTab=${nextTab?.id}`); + return false; } - return false; + return true; } get sanitized() { @@ -2040,6 +2055,8 @@ export default class Tab { ancestorTabIds: this.tab.$TST.ancestorIds, bundledTabId: this.tab.$TST.bundledTabId, }; + if (this.stuck) + exportedTab.states.push(Constants.kTAB_STATE_STUCK); if (configs.cacheAPITreeItems && light) this.$exportedForAPI = exportedTab; } @@ -2893,6 +2910,24 @@ Tab.hasLoadingTab = windowId => { }); }; +Tab.hasDuplicatedTabs = (windowId, options = {}) => { + const tabs = TabsStore.queryAll({ + windowId, + tabs: TabsStore.getTabsMap(TabsStore.livingTabsInWindow, windowId), + living: true, + ...options, + iterator: true + }); + const tabKeys = new Set(); + for (const tab of tabs) { + const key = `${tab.cookieStoreId}\n${tab.url}`; + if (tabKeys.has(key)) + return true; + tabKeys.add(key); + } + return false; +}; + Tab.hasMultipleTabs = (windowId, options = {}) => { const tabs = TabsStore.queryAll({ windowId, diff --git a/waterfox/browser/components/sidebar/common/Window.js b/waterfox/browser/components/sidebar/common/Window.js index a32e2d6325741..a30f4bc86f0b6 100644 --- a/waterfox/browser/components/sidebar/common/Window.js +++ b/waterfox/browser/components/sidebar/common/Window.js @@ -35,8 +35,8 @@ export default class Window { this.containerClassList = null; this.pinnedContainerElement = null; - this.internalMovingTabs = new Set(); - this.alreadyMovedTabs = new Set(); + this.internalMovingTabs = new Map(); + this.alreadyMovedTabs = new Map(); this.internalClosingTabs = new Set(); this.keepDescendantsTabs = new Set(); this.highlightingTabs = new Set(); @@ -45,11 +45,12 @@ export default class Window { this.subTreeMovingCount = this.subTreeChildrenMovingCount = this.doingIntelligentlyCollapseExpandCount = - this.internalFocusCount = - this.internalSilentlyFocusCount = - this.internalByMouseFocusCount = this.duplicatingTabsCount = 0; + this.internallyFocusingTabs = new Set(); + this.internallyFocusingByMouseTabs = new Set(); + this.internallyFocusingSilentlyTabs = new Set(); + this.preventToDetectTabBunchesUntil = Date.now() + configs.tabBunchesDetectionDelayOnNewWindow; this.openingTabs = new Set(); diff --git a/waterfox/browser/components/sidebar/common/api-tabs.js b/waterfox/browser/components/sidebar/common/api-tabs.js index 9fe3d36c9c4ec..ea19a513b8a84 100644 --- a/waterfox/browser/components/sidebar/common/api-tabs.js +++ b/waterfox/browser/components/sidebar/common/api-tabs.js @@ -29,55 +29,6 @@ export async function getIndexes(...queriedTabIds) { return indexes.map(tab => tab ? tab.index : -1); } -export async function blur(tab, unactivatableTabs = []) { - const unactivatableIndices = Array.from(new Set([tab, ...unactivatableTabs].map(tab => tab.index))); - const minUnactivatableIndex = Math.min.apply(null, unactivatableIndices); - const maxUnactivatableIndex = Math.max.apply(null, unactivatableIndices); - const unactivatableTabById = new Map(); - for (const tab of unactivatableTabs) { - unactivatableTabById.set(tab.id, tab); - } - - const allTabs = await browser.tabs.query({ windowId: tab.windowId }); - const unactivatableTabIds = new Set([tab.id, ...unactivatableTabs.map(tab => tab.id)]); - if (allTabs.length == unactivatableTabIds.size) - return; // there is no other focusible tab! - - const restTabs = allTabs.filter(tab => unactivatableTabIds.has(tab.id)); - const middleTabs = restTabs.filter(tab => tab.index > minUnactivatableIndex || tab.index < maxUnactivatableIndex); - const previousTab = minUnactivatableIndex > 0 && allTabs[minUnactivatableIndex - 1]; - const nextTab = maxUnactivatableIndex < allTabs.length - 1 && allTabs[maxUnactivatableIndex + 1]; - const allTabById = new Map(); - for (const tab of allTabs) { - allTabById.set(tab.id, tab); - } - - const scannedTabIds = new Set(); - let successorTab = tab; - do { - if (scannedTabIds.has(successorTab.id)) - break; // prevent infinite loop! - scannedTabIds.add(successorTab.id); - let nextSuccessorTab = unactivatableTabById.get(successorTab.successorTabId); - if (nextSuccessorTab) { - successorTab = nextSuccessorTab; - continue; - } - nextSuccessorTab = allTabById.get(successorTab.successorTabId) || - nextTab || - (middleTabs.length > 0 && middleTabs[0]) || - previousTab || - restTabs[0]; - if (unactivatableTabById.has(nextSuccessorTab.id)) { - successorTab = nextSuccessorTab; - continue; - } - await browser.tabs.update(nextSuccessorTab.id, { active: true }); - break; - } - while (successorTab); -} - export function isMissingTabError(error) { return ( error && diff --git a/waterfox/browser/components/sidebar/common/bookmark.js b/waterfox/browser/components/sidebar/common/bookmark.js index 75f693ff5adf7..836b0adc6d574 100644 --- a/waterfox/browser/components/sidebar/common/bookmark.js +++ b/waterfox/browser/components/sidebar/common/bookmark.js @@ -20,6 +20,7 @@ import { isLinux, } from './common.js'; import * as ApiTabs from './api-tabs.js'; +import * as TreeBehavior from './tree-behavior.js'; import * as Constants from './constants.js'; import * as ContextualIdentities from './contextual-identities.js'; import * as Dialog from './dialog.js'; @@ -637,6 +638,11 @@ reserveToGroupCreatedBookmarks.retryCount = 0; async function tryGroupCreatedBookmarks() { log('tryGroupCreatedBookmarks ', mCreatedBookmarks); + if (!configs.autoCreateFolderForBookmarksFromTree) { + log(' => autoCreateFolderForBookmarksFromTree is false'); + return; + } + const lastDraggedTabs = configs.lastDraggedTabs; if (lastDraggedTabs && lastDraggedTabs.tabIds.length > mCreatedBookmarks.length) { @@ -677,37 +683,69 @@ async function tryGroupCreatedBookmarks() { for (const bookmark of bookmarks) { parentIds.add(bookmark.parentId); } + log('parentIds: ', parentIds); if (parentIds.size > 1) { log(' => ignore bookmarks created under multiple folders'); return; } } + const tabs = lastDraggedTabs ? + lastDraggedTabs.tabIds.map(id => Tab.get(id)) : + (await Promise.all(bookmarks.map(async bookmark => { + const tabs = await browser.tabs.query({ url: bookmark.url }); + if (tabs.length == 0) + return null; + const tab = tabs.find(tab => tab.highlighted) || tabs[0]; + return Tab.get(tab); + }))).filter(tab => !!tab); + log('tabs: ', tabs); + if (tabs.length != bookmarks.length) { + log(' => ignore bookmarks created from non-tab sources'); + return; + } + + const treeStructure = TreeBehavior.getTreeStructureFromTabs(tabs); + log('treeStructure: ', treeStructure); + const topLevelTabsCount = treeStructure.filter(item => item.parent < 0).length; + if (topLevelTabsCount == treeStructure.length) { + log(' => no need to group bookmarks from dragged flat tabs'); + return; + } + + let titles = getTitlesWithTreeStructure(tabs); + if (tabs[0].$TST.isGroupTab && + titles.filter(title => !/^>/.test(title)).length == 1) { + log('delete needless bookmark for a group tab'); + browser.bookmarks.remove(bookmarks[0].id); + tabs.shift(); + bookmarks.shift(); + titles = getTitlesWithTreeStructure(tabs); + } + log('titles: ', titles); + + log('save tree structure to bookmarks'); + for (let i = 0, maxi = bookmarks.length; i < maxi; i++) { + const title = titles[i]; + if (title == tabs[i].title) + continue; + browser.bookmarks.update(bookmarks[i].id, { title }); + } + + log('ready to group bookmarks under a folder'); + const parentId = bookmarks[0].parentId; { // Do nothing if all bookmarks are created under a new // blank folder. const allChildren = await browser.bookmarks.getChildren(parentId); + log('allChildren.length vs bookmarks.length: ', allChildren.length, bookmarks.length); if (allChildren.length == bookmarks.length) { - log(' => ignore bookmarks created under a new blank folder'); + log(' => no need to create folder for bookmarks under a new blank folder'); return; } } - const possibleSourceTabs = (await Promise.all(bookmarks.map(async bookmark => { - const tabs = await browser.tabs.query({ url: bookmark.url }); - if (tabs.length == 0) - return null; - return tabs[0]; - }))).filter(tab => !!tab); - console.log('possibleSourceTabs ', possibleSourceTabs); - if (possibleSourceTabs.length != bookmarks.length) { - log(' => ignore bookmarks created from non-tab sources'); - return; - } - - log('ready to group bookmarks under a folder'); - log('create a folder for grouping'); mCreatingCount++; const folder = await browser.bookmarks.create({ @@ -728,28 +766,6 @@ async function tryGroupCreatedBookmarks() { index: movedCount++ }); } - - if (!lastDraggedTabs) - return; - - const tabs = lastDraggedTabs.tabIds.map(id => Tab.get(id)); - let titles = getTitlesWithTreeStructure(tabs); - if (tabs[0].$TST.isGroupTab && - titles.filter(title => !/^>/.test(title)).length == 1) { - log('delete needless bookmark for a group tab'); - browser.bookmarks.remove(bookmarks[0].id); - tabs.shift(); - bookmarks.shift(); - titles = getTitlesWithTreeStructure(tabs); - } - - log('save tree structure to bookmarks'); - for (let i = 0, maxi = bookmarks.length; i < maxi; i++) { - const title = titles[i]; - if (title == tabs[i].title) - continue; - browser.bookmarks.update(bookmarks[i].id, { title }); - } } if (Constants.IS_BACKGROUND && diff --git a/waterfox/browser/components/sidebar/common/cache-storage.js b/waterfox/browser/components/sidebar/common/cache-storage.js index 40227939b7605..e70805047ba14 100644 --- a/waterfox/browser/components/sidebar/common/cache-storage.js +++ b/waterfox/browser/components/sidebar/common/cache-storage.js @@ -6,10 +6,14 @@ 'use strict'; import * as UniqueId from '/common/unique-id.js'; +import { + asyncRunWithTimeout, +} from '/common/common.js'; const DB_NAME = 'PermanentStorage'; const DB_VERSION = 3; const EXPIRATION_TIME_IN_MSEC = 7 * 24 * 60 * 60 * 1000; // 7 days +const TIMEOUT_IN_MSEC = 1000 * 5; // 5 sec export const BACKGROUND = 'backgroundCaches'; const SIDEBAR = 'sidebarCaches'; // obsolete, but left here to delete old storage @@ -70,28 +74,42 @@ export async function setValue({ windowId, key, value } = {}) { const store = BACKGROUND; const cacheKey = `${windowUniqueId}-${key}`; - const timestamp = Date.now(); - try { - const transaction = db.transaction([store], 'readwrite'); - const cacheStore = transaction.objectStore(store); - - cacheStore.put({ - key: cacheKey, - windowId: windowUniqueId, - value, - timestamp, - }); - - transaction.oncomplete = () => { - //db.close(); - windowId = undefined; - key = undefined; - value = undefined; - }; - } - catch(error) { - console.error(`Failed to store cache ${cacheKey} in the store ${store}`, error); - } + asyncRunWithTimeout({ + task: () => new Promise((resolve, reject) => { + const timestamp = Date.now(); + try { + const transaction = db.transaction([store], 'readwrite'); + const cacheStore = transaction.objectStore(store); + const cacheRequest = cacheStore.put({ + key: cacheKey, + windowId: windowUniqueId, + value, + timestamp, + }); + + transaction.oncomplete = () => { + //db.close(); + windowId = undefined; + key = undefined; + value = undefined; + resolve(); + }; + + cacheRequest.onerror = event => { + console.error(`Failed to store cache ${cacheKey} in the store ${store}`, event); + reject(event); + }; + } + catch(error) { + console.error(`Failed to store cache ${cacheKey} in the store ${store}`, error); + reject(error); + } + }), + timeout: TIMEOUT_IN_MSEC, + onTimedOut() { + throw new Error(`CacheStorage.setValue for {windowId}/key timed out`); + }, + }); } export async function deleteValue({ windowId, key } = {}) { @@ -107,22 +125,47 @@ export async function deleteValue({ windowId, key } = {}) { const store = BACKGROUND; const cacheKey = `${windowUniqueId}-${key}`; - try { - const transaction = db.transaction([store], 'readwrite'); - const cacheStore = transaction.objectStore(store); - cacheStore.delete(cacheKey); - transaction.oncomplete = () => { - //db.close(); - windowId = undefined; - key = undefined; - }; - } - catch(error) { - console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, error); - } + asyncRunWithTimeout({ + task: () => new Promise((resolve, reject) => { + try { + const transaction = db.transaction([store], 'readwrite'); + const cacheStore = transaction.objectStore(store); + const cacheRequest = cacheStore.delete(cacheKey); + + transaction.oncomplete = () => { + //db.close(); + windowId = undefined; + key = undefined; + resolve(); + }; + + cacheRequest.onerror = event => { + console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, event); + reject(event); + }; + } + catch(error) { + console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, error); + reject(error); + } + }), + timeout: TIMEOUT_IN_MSEC, + onTimedOut() { + throw new Error(`CacheStorage.deleteValue for {windowId}/key timed out`); + }, + }); } export async function getValue({ windowId, key } = {}) { + return asyncRunWithTimeout({ + task: () => getValueInternal({ windowId, key }), + timeout: TIMEOUT_IN_MSEC, + onTimedOut() { + throw new Error(`CacheStorage.getValue for {windowId}/${key} timed out`); + }, + }); +} +async function getValueInternal({ windowId, key } = {}) { return new Promise(async (resolve, _reject) => { const [db, windowUniqueId] = await Promise.all([ openDB(), @@ -163,6 +206,11 @@ export async function getValue({ windowId, key } = {}) { cache.value = undefined; }; + cacheRequest.onerror = event => { + console.error('Failed to get from cache:', event); + resolve(null); + }; + transaction.oncomplete = () => { //db.close(); windowId = undefined; @@ -177,6 +225,15 @@ export async function getValue({ windowId, key } = {}) { } export async function clearForWindow(windowId) { + return asyncRunWithTimeout({ + task: () => clearForWindowInternal(windowId), + timeout: TIMEOUT_IN_MSEC, + onTimedOut() { + throw new Error(`CacheStorage.clearForWindow for {windowId} timed out`); + }, + }); +} +async function clearForWindowInternal(windowId) { reserveToExpireOldEntries(); return new Promise(async (resolve, reject) => { const [db, windowUniqueId] = await Promise.all([ diff --git a/waterfox/browser/components/sidebar/common/common.js b/waterfox/browser/components/sidebar/common/common.js index 12fd4e2024c1e..cf2b929546d2e 100644 --- a/waterfox/browser/components/sidebar/common/common.js +++ b/waterfox/browser/components/sidebar/common/common.js @@ -13,7 +13,6 @@ import * as Constants from './constants.js'; const WATERFOX_SPECIFIC_VALUES = { hideHorizontalTabsWhileActive: true, showTabPreview: true, - hoverTabPreviewDelayMs: 500, sidebarPosition: Constants.kTABBAR_POSITION_LEFT, suppressGapFromShownOrHiddenToolbarOnlyOnMouseOperation: false, @@ -303,6 +302,8 @@ export const configs = new Configs({ autoExpandOnLongHoverDelay: 500, autoExpandOnLongHoverRestoreIniitalState: true, + autoCreateFolderForBookmarksFromTree: true, + accelKey: '', skipCollapsedTabsForTabSwitchingShortcuts: false, @@ -341,11 +342,13 @@ export const configs = new Configs({ groupTabTemporaryStateForChildrenOfPinned: Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE, groupTabTemporaryStateForChildrenOfFirefoxView: Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE, groupTabTemporaryStateForOrphanedTabs: Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE, + groupTabTemporaryStateForAPI: Constants.kGROUP_TAB_TEMPORARY_STATE_NOTHING, renderTreeInGroupTabs: true, warnOnAutoGroupNewTabs: true, warnOnAutoGroupNewTabsWithListing: true, warnOnAutoGroupNewTabsWithListingMaxRows: 5, showAutoGroupOptionHint: true, + showAutoGroupOptionHintWithOpener: true, // behavior around newly opened tabs @@ -392,11 +395,13 @@ export const configs = new Configs({ moveParentBehavior_outsideSidebar_expanded: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD, moveParentBehavior_noSidebar_collapsed: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD, moveParentBehavior_noSidebar_expanded: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD, + closeParentBehavior_replaceWithGroup_thresholdToPrevent: 1, // negative value means "never prevent" moveTabsToBottomWhenDetachedFromClosedParent: false, promoteAllChildrenWhenClosedParentIsLastChild: true, successorTabControlLevel: Constants.kSUCCESSOR_TAB_CONTROL_IN_TREE, simulateSelectOwnerOnClose: true, simulateLockTabSizing: true, + deferScrollingToOutOfViewportSuccessor: true, simulateTabsLoadInBackgroundInverted: false, supportTabsMultiselect: typeof browser.menus.overrideContext == 'function', warnOnCloseTabs: true, @@ -847,6 +852,20 @@ export function nextFrame() { }); } +export async function asyncRunWithTimeout({ task, timeout, onTimedOut }) { + let succeeded = false; + return Promise.race([ + task().then(result => { + succeeded = true; + return result; + }), + wait(timeout).then(() => { + if (!succeeded) + return onTimedOut(); + }), + ]); +} + const mNotificationTasks = new Map(); @@ -1067,7 +1086,7 @@ export function watchOverflowStateChange({ target, moreResizeTargets, onOverflow }; let resizeObserver/*, mutationObserver*/; - if (useLegacyOverflowEvents) { + if (!useLegacyOverflowEvents) { const resizeTargets = new Set([target, ...(moreResizeTargets || [])]); resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { diff --git a/waterfox/browser/components/sidebar/common/constants.js b/waterfox/browser/components/sidebar/common/constants.js index 8a728934f27ee..65f14d6b0ee92 100644 --- a/waterfox/browser/components/sidebar/common/constants.js +++ b/waterfox/browser/components/sidebar/common/constants.js @@ -217,6 +217,7 @@ export const kTAB_STATE_GROUP_TAB = 'group-tab'; export const kTAB_STATE_NEW_TAB_COMMAND_TAB = 'newtab-command-tab'; export const kTAB_STATE_OPENED_FOR_SAME_WEBSITE = 'opened-for-same-website'; export const kTAB_STATE_STICKY = 'sticky'; +export const kTAB_STATE_STUCK = 'stuck'; // virtual state export const kTAB_INTERNAL_STATES = new Set([ // TST specific states 'tab', kTAB_STATE_LAST_ROW, @@ -239,6 +240,7 @@ export const kTAB_INTERNAL_STATES = new Set([ // TST specific states kTAB_STATE_FROM_FIREFOX_VIEW, kTAB_STATE_OPENED_FOR_SAME_WEBSITE, kTAB_STATE_STICKY, + kTAB_STATE_STUCK, ]); export const kTAB_TEMPORARY_STATES = new Set([ // states not trigger updating of cache kTAB_STATE_CREATING, @@ -269,6 +271,7 @@ export const kTAB_SAFE_STATES = new Set([ // exportable via API kTAB_STATE_FROM_FIREFOX_VIEW, kTAB_STATE_OPENED_FOR_SAME_WEBSITE, kTAB_STATE_STICKY, + kTAB_STATE_STUCK, ]); export const kTAB_SAFE_STATES_ARRAY = Array.from(kTAB_SAFE_STATES); diff --git a/waterfox/browser/components/sidebar/common/permissions.js b/waterfox/browser/components/sidebar/common/permissions.js index 5df2a8a294972..c7f2396e3b8f8 100644 --- a/waterfox/browser/components/sidebar/common/permissions.js +++ b/waterfox/browser/components/sidebar/common/permissions.js @@ -71,9 +71,10 @@ browser.runtime.onMessage.addListener((message, _sender) => { for (const request of requests) { const { onChanged, checkbox } = destroyRequest(request); - if (onChanged) - onChanged(true); - checkbox.checked = true; + const checked =onChanged ? + onChanged(true) : + undefined; + checkbox.checked = checked !== undefined ? !!checked : true; } }); @@ -114,7 +115,10 @@ export function bindToCheckbox(permissions, checkbox, options = {}) { isGranted(permissions) .then(granted => { - checkbox.checked = granted; + const checked = options.onInitialized ? + options.onInitialized(granted) : + undefined; + checkbox.checked = checked !== undefined ? !!checked : granted; }) .catch(_error => { checkbox.setAttribute('readonly', true); @@ -168,11 +172,12 @@ export function bindToCheckbox(permissions, checkbox, options = {}) { return; if (granted) { + const checked = options.onChanged ? + options.onChanged(true) : + undefined; for (const checkbox of checkboxes) { - checkbox.checked = true; + checkbox.checked = checked !== undefined ? !!checked : true; } - if (options.onChanged) - options.onChanged(true); browser.runtime.sendMessage({ type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED, permissions diff --git a/waterfox/browser/components/sidebar/common/retrieve-url.js b/waterfox/browser/components/sidebar/common/retrieve-url.js index 6c8c13942336b..a73c730f3b8af 100644 --- a/waterfox/browser/components/sidebar/common/retrieve-url.js +++ b/waterfox/browser/components/sidebar/common/retrieve-url.js @@ -14,6 +14,7 @@ function log(...args) { internalLogger('common/retrieve-url', ...args); } +export const kTYPE_PLAIN_TEXT = 'text/plain'; export const kTYPE_X_MOZ_URL = 'text/x-moz-url'; export const kTYPE_URI_LIST = 'text/uri-list'; export const kTYPE_MOZ_TEXT_INTERNAL = 'text/x-moz-text-internal'; @@ -23,7 +24,7 @@ const ACCEPTABLE_DATA_TYPES = [ kTYPE_URI_LIST, kTYPE_X_MOZ_URL, kTYPE_MOZ_TEXT_INTERNAL, - 'text/plain' + kTYPE_PLAIN_TEXT, ]; let mFileURLResolver = null; @@ -102,7 +103,7 @@ export async function fromClipboard({ selection } = {}) { if (await mSelectionClipboardProvider.isAvailable()) { const maybeUrlString = await mSelectionClipboardProvider.getTextData(); if (maybeUrlString) - urls.push(...fromData(maybeUrlString, 'text/plain')); + urls.push(...fromData(maybeUrlString, kTYPE_PLAIN_TEXT)); return sanitizeURLs(urls); } } @@ -172,7 +173,7 @@ function fromData(data, type) { .trim() .split('\n'); - case 'text/plain': + case kTYPE_PLAIN_TEXT: return data .replace(/\r/g, '\n') .replace(/\n\n+/g, '\n') diff --git a/waterfox/browser/components/sidebar/common/tabs-internal-operation.js b/waterfox/browser/components/sidebar/common/tabs-internal-operation.js index 3d47da4683a1f..44477b9e15695 100644 --- a/waterfox/browser/components/sidebar/common/tabs-internal-operation.js +++ b/waterfox/browser/components/sidebar/common/tabs-internal-operation.js @@ -7,6 +7,8 @@ // internal operations means operations bypassing WebExtensions' tabs APIs. +import EventListenerManager from '/extlib/EventListenerManager.js'; + import { log as internalLogger, dumpTab, @@ -26,6 +28,8 @@ function log(...args) { internalLogger('common/tabs-internal-operation', ...args); } +export const onBeforeTabsRemove = new EventListenerManager(); + export async function activateTab(tab, { byMouseOperation, keepMultiselection, silently } = {}) { if (!Constants.IS_BACKGROUND) throw new Error('Error: TabsInternalOperation.activateTab is available only on the background page, use a `kCOMMAND_ACTIVATE_TAB` message instead.'); @@ -35,17 +39,15 @@ export async function activateTab(tab, { byMouseOperation, keepMultiselection, s return; log('activateTab: ', dumpTab(tab)); const win = TabsStore.windows.get(tab.windowId); - win.internalFocusCount++; + win.internallyFocusingTabs.add(tab.id); if (byMouseOperation) - win.internalByMouseFocusCount++; + win.internallyFocusingByMouseTabs.add(tab.id); if (silently) - win.internalSilentlyFocusCount++; + win.internallyFocusingSilentlyTabs.add(tab.id); const onError = (e) => { - win.internalFocusCount--; - if (byMouseOperation) - win.internalByMouseFocusCount--; - if (silently) - win.internalSilentlyFocusCount--; + win.internallyFocusingTabs.delete(tab.id); + win.internallyFocusingByMouseTabs.delete(tab.id); + win.internallyFocusingSilentlyTabs.delete(tab.id); ApiTabs.handleMissingTabError(e); }; if (configs.supportTabsMultiselect && @@ -77,30 +79,52 @@ export async function blurTab(bluredTabs, { windowId, silently } = {}) { const bluredTabIds = new Set(Array.from(bluredTabs || [], tab => tab.id || tab)); - let bluredTabsFound = false; - let nextActiveTab = null; - for (const tab of Tab.getVisibleTabs(windowId || bluredTabs[0].windowId)) { - const blured = bluredTabIds.has(tab.id); - if (blured) - bluredTabsFound = true; - if (!bluredTabsFound) - nextActiveTab = tab; - if (bluredTabsFound && - !blured) { - nextActiveTab = tab; + // First, try to find successor based on successorTabId from left tabs. + let successorTab = Tab.get(bluredTabs.find(tab => tab.active)?.successorTabId); + const scannedTabIds = new Set(); + while (successorTab && bluredTabIds.has(successorTab.id)) { + if (scannedTabIds.has(successorTab.id)) + break; // prevent infinite loop! + scannedTabIds.add(successorTab.id); + const nextSuccessorTab = (successorTab.successorTabId > 0 && successorTab.successorTabId != successorTab.id) ? + Tab.get(successorTab.successorTabId) : + null; + if (!nextSuccessorTab) break; + successorTab = nextSuccessorTab; + } + log('blurTab/step 1: found successor = ', successorTab?.id); + + // Second, try to detect successor based on their order. + if (!successorTab || bluredTabIds.has(successorTab.id)) { + if (successorTab) + log(' => it cannot become the successor, find again'); + let bluredTabsFound = false; + for (const tab of Tab.getVisibleTabs(windowId || bluredTabs[0].windowId)) { + const blured = bluredTabIds.has(tab.id); + if (blured) + bluredTabsFound = true; + if (!bluredTabsFound) + successorTab = tab; + if (bluredTabsFound && + !blured) { + successorTab = tab; + break; + } } + log('blurTab/step 2: found successor = ', successorTab?.id); } - if (nextActiveTab) - await activateTab(nextActiveTab, { silently }); - return nextActiveTab; + + if (successorTab) + await activateTab(successorTab, { silently }); + return successorTab; } export function removeTab(tab) { return removeTabs([tab]); } -export function removeTabs(tabs, { keepDescendants, byMouseOperation, originalStructure, triggerTab } = {}) { +export async function removeTabs(tabs, { keepDescendants, byMouseOperation, originalStructure, triggerTab } = {}) { if (!Constants.IS_BACKGROUND) throw new Error('TabsInternalOperation.removeTabs is available only on the background page, use a `kCOMMAND_REMOVE_TABS_INTERNALLY` message instead.'); @@ -108,6 +132,8 @@ export function removeTabs(tabs, { keepDescendants, byMouseOperation, originalSt if (tabs.length == 0) return; + await onBeforeTabsRemove.dispatch(tabs); + const win = TabsStore.windows.get(tabs[0].windowId); const tabIds = []; let willChangeFocus = false; @@ -136,13 +162,12 @@ export function removeTabs(tabs, { keepDescendants, byMouseOperation, originalSt clearCache(tab); if (keepDescendants) win.keepDescendantsTabs.add(tab.id); - } - if (willChangeFocus && byMouseOperation) { - win.internalByMouseFocusCount++; - setTimeout(() => { // the operation can be canceled - if (win.internalByMouseFocusCount > 0) - win.internalByMouseFocusCount--; - }, 250); + if (willChangeFocus && byMouseOperation) { + win.internallyFocusingByMouseTabs.add(tab.id); + setTimeout(() => { // the operation can be canceled + win.internallyFocusingByMouseTabs.delete(tab.id); + }, 250); + } } } diff --git a/waterfox/browser/components/sidebar/common/tree-behavior.js b/waterfox/browser/components/sidebar/common/tree-behavior.js index 17f53194be618..f6d4a0c644733 100644 --- a/waterfox/browser/components/sidebar/common/tree-behavior.js +++ b/waterfox/browser/components/sidebar/common/tree-behavior.js @@ -82,15 +82,24 @@ export function getParentTabOperationBehavior(tab, { context, byInternalOperatio log(' => behavior: ', behavior); - if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY) { - behavior = parentTab ? Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN : Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD; - log(' => intelligent behavior: ', behavior); + const replacedParentCount = tab?.$TST?.replacedParentGroupTabCount; + if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB && + configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent >= 0 && + replacedParentCount && + replacedParentCount >= configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent) { + behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY; + log(' => the group tab is already replaced parent, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY'); } if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE && preventEntireTreeBehavior) { + behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY; + log(' => preventEntireTreeBehavior behavior, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY'); + } + + if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY) { behavior = parentTab ? Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN : Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD; - log(' => preventEntireTreeBehavior behavior: ', behavior); + log(' => intelligent behavior: ', behavior); } // Promote all children to upper level, if this is the last child of the parent. diff --git a/waterfox/browser/components/sidebar/common/tst-api.js b/waterfox/browser/components/sidebar/common/tst-api.js index b0a0a9a710c38..b139a8693d90d 100644 --- a/waterfox/browser/components/sidebar/common/tst-api.js +++ b/waterfox/browser/components/sidebar/common/tst-api.js @@ -134,6 +134,7 @@ export const kNOTIFY_TRY_COLLAPSE_TREE_FROM_COLLAPSE_COMMAND = 'try-collapse-tre export const kNOTIFY_TRY_COLLAPSE_TREE_FROM_COLLAPSE_ALL_COMMAND = 'try-collapse-tree-from-collapse-all-command'; export const kNOTIFY_TRY_FIXUP_TREE_ON_TAB_MOVED = 'try-fixup-tree-on-tab-moved'; export const kNOTIFY_TRY_HANDLE_NEWTAB = 'try-handle-newtab'; +export const kNOTIFY_TRY_SCROLL_TO_ACTIVATED_TAB = 'try-scroll-to-activated-tab'; export const kGET_TREE = 'get-tree'; export const kGET_LIGHT_TREE = 'get-light-tree'; export const kATTACH = 'attach'; @@ -272,6 +273,11 @@ export function clearCache(cache) { // bacause instances of the class will be very short-life and increases RAM usage on // massive tabs case. export async function exportTab(sourceTab, { addonId, light, isContextTab, interval, permissions, cache, cacheKey } = {}) { + const normalizedSourceTab = Tab.get(sourceTab); + if (!normalizedSourceTab) + throw new Error(`Fatal error: tried to export not a tab. ${sourceTab}`); + sourceTab = normalizedSourceTab; + if (!interval) interval = 0; if (!cache) diff --git a/waterfox/browser/components/sidebar/experiments/prefs.js b/waterfox/browser/components/sidebar/experiments/prefs.js index e3af91d4abd32..38e7604f94599 100644 --- a/waterfox/browser/components/sidebar/experiments/prefs.js +++ b/waterfox/browser/components/sidebar/experiments/prefs.js @@ -36,7 +36,7 @@ function initSidebarCategory(document, { locale, BASE_URL, BASE_PREF }) { if (document.querySelector('#category-tabsSidebar')) return true; - const generalItem = document.querySelector('#category-search'); + const generalItem = document.querySelector('#category-general'); if (!generalItem) return false; @@ -859,9 +859,25 @@ function initSidebarCategory(document, { locale, BASE_URL, BASE_PREF }) { document.defaultView.register_module('paneTabsSidebar', document.defaultView.gSidebarPage); if (document.URL.endsWith('#tabsSidebar')) { - document.querySelector('#categories').selectItem(document.querySelector('#category-tabsSidebar')); - document.defaultView.gotoPref('paneTabsSidebar'); - document.defaultView.gCategoryInits.get('paneTabsSidebar').init(); + let done = false; + const initialize = () => { + document.querySelector('#categories').selectItem(document.querySelector('#category-tabsSidebar')); + document.defaultView.gotoPref('paneTabsSidebar'); + document.defaultView.gCategoryInits.get('paneTabsSidebar').init(); + done = true; + }; + if (document.defaultView.gSearchResultsPane.inited) { + initialize(); + } + else { + document.addEventListener('DOMContentLoaded', initialize, { once: true }); + document.defaultView.setTimeout(1000, () => { + if (done) + return; + document.removeEventListener('DOMContentLoaded', initialize, { once: true }); + initialize(); + }); + } } } catch (error) { @@ -995,7 +1011,7 @@ const AboutPreferencesWatcher = { const startAt = Date.now(); const topWin = loadInfo.browsingContext.topChromeWindow; const timer = topWin.setInterval(() => { - if (Date.now() - startAt > 1000) { + if (Date.now() - startAt > 5000) { // timeout topWin.clearInterval(timer); return; diff --git a/waterfox/browser/components/sidebar/experiments/waterfoxBridge.js b/waterfox/browser/components/sidebar/experiments/waterfoxBridge.js index e7d216415897b..8c15bbee132cb 100644 --- a/waterfox/browser/components/sidebar/experiments/waterfoxBridge.js +++ b/waterfox/browser/components/sidebar/experiments/waterfoxBridge.js @@ -71,7 +71,7 @@ const BrowserWindowWatcher = { if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) { const installed = this.installTabsSidebar(win); if (installed) { - this.patchToTabPreviewModules(win); + this.patchToTabHoverPreviewModules(win); this.patchToPlacesModules(win); this.patchToFullScreenModules(win); win.addEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true }); @@ -96,7 +96,7 @@ const BrowserWindowWatcher = { if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) { this.uninstallTabsSidebar(win); try { - this.unpatchTabPreviewModules(win); + this.unpatchTabHoverPreviewModules(win); this.unpatchPlacesModules(win); this.unpatchFullScreenModules(win); win.removeEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true }); @@ -216,9 +216,9 @@ const BrowserWindowWatcher = { :root.tabs-sidebar-right #tabs-sidebar-moveRight { display: none; } - @supports not -moz-bool-pref("userChrome.icon.disabled") { - @supports -moz-bool-pref("userChrome.icon.menu") { - @supports -moz-bool-pref("userChrome.icon.context_menu") { + @media not (-moz-bool-pref: "userChrome.icon.disabled") { + @media (-moz-bool-pref: "userChrome.icon.menu") { + @media (-moz-bool-pref: "userChrome.icon.context_menu") { #tabs-sidebar-newTab { list-style-image: var(--uc-new-tab-icon); } @@ -327,18 +327,24 @@ const BrowserWindowWatcher = { transition: 0.8s margin-left ease-out, 0.8s margin-right ease-out; } - @supports -moz-bool-pref("userChrome.fullscreen.overlap") { - @supports -moz-bool-pref("browser.fullscreen.autohide") { + @media (-moz-bool-pref: "userChrome.fullscreen.overlap") { + @media (-moz-bool-pref: "browser.fullscreen.autohide") { :root[sizemode="fullscreen"] #tabs-sidebar-box, :root[sizemode="fullscreen"] #tabs-sidebar-splitter { + height: 100% !important; position: fixed !important; z-index: 900 !important; - height: 100% !important; } :root[sizemode="fullscreen"] #tabs-sidebar-splitter { width: 5px !important; z-index: 890 !important; } + :root[sizemode="fullscreen"]:not(.tabs-sidebar-right) #sidebar-box:not([positionend="true"]) ~ #tabs-sidebar-splitter, + :root[sizemode="fullscreen"].tabs-sidebar-right #sidebar-box:not([positionend="true"]) ~ #tabs-sidebar-splitter, + :root[sizemode="fullscreen"]:not(.tabs-sidebar-right) #sidebar-box[positionend="true"] ~ #tabs-sidebar-splitter, + :root[sizemode="fullscreen"].tabs-sidebar-right #sidebar-box[positionend="true"] ~ #tabs-sidebar-splitter { + order: unset !important; + } :root[sizemode="fullscreen"]:not(.tabs-sidebar-right) #tabs-sidebar-box, :root[sizemode="fullscreen"]:not(.tabs-sidebar-right) #tabs-sidebar-splitter { left: 0; @@ -351,7 +357,7 @@ const BrowserWindowWatcher = { } @media (prefers-reduced-motion: no-preference) { - @supports -moz-bool-pref("userChrome.decoration.animate") { + @media (-moz-bool-pref: "userChrome.decoration.animate") { :root[sizemode="fullscreen"] #tabs-sidebar-box { transition: margin-left 1.3s var(--animation-easing-function) 50ms, margin-right 1.3s var(--animation-easing-function) 50ms !important; @@ -366,11 +372,14 @@ const BrowserWindowWatcher = { document.querySelector('#mainCommandSet').appendChild(element(document, XUL, 'command', { id: 'toggle-tabs-sidebar-command', })); - document.querySelector('#mainKeyset').appendChild(element(document, XUL, 'key', { - id: 'toggle-tabs-sidebar-key', - keycode: 'VK_F1', - command: 'toggle-tabs-sidebar-command', - })); + // So, as a workaround we don't remove the key element on uninstallation and reuse existing key element. + if (!document.querySelector('#toggle-tabs-sidebar-key')) { + document.querySelector('#mainKeyset').appendChild(element(document, XUL, 'key', { + id: 'toggle-tabs-sidebar-key', + keycode: 'VK_F1', + command: 'toggle-tabs-sidebar-command', + })); + } document.querySelector('#viewSidebarMenu').appendChild(element(document, XUL, 'menuseparator', { id: 'viewmenu-tabs-sidebar-separator', @@ -655,7 +664,8 @@ const BrowserWindowWatcher = { for (const node of document.querySelectorAll(` #tabs-sidebar-splitter, #tabs-sidebar-box, - #toggle-tabs-sidebar-key, + /* Don't remove key element because only the first element added is effective.*/ + /*#toggle-tabs-sidebar-key,*/ #toggle-tabs-sidebar-command, #viewmenu-tabs-sidebar-separator, #viewmenu-toggle-tabs-sidebar, @@ -697,17 +707,27 @@ const BrowserWindowWatcher = { } }, - patchToTabPreviewModules(win) { - const tabPreview = win.document.querySelector('#tabbrowser-tab-preview'); + patchToTabHoverPreviewModules(win) { + const tabs = win.document.getElementById('tabbrowser-tabs'); + if (!tabs) + return; + + if (!tabs._previewPanel) { + // https://searchfox.org/mozilla-esr128/rev/7998c47697fb3ba3e380eda1281c8280da81a0b1/browser/components/tabbrowser/content/tabs.js#187 + const TabHoverPreviewPanel = ChromeUtils.importESModule('chrome://browser/content/tabbrowser/tab-hover-preview.mjs').default; + tabs._previewPanel = new TabHoverPreviewPanel(win.document.getElementById('tab-preview-panel')); + } + + const tabPreview = tabs._previewPanel; if (!tabPreview || - !tabPreview.showPreview) + !tabPreview.activate) return; tabPreview.__ws__calculateCoordinates = () => { const tabsSidebar = win.document.querySelector('#tabs-sidebar'); const tabsSidebarRect = tabsSidebar.getBoundingClientRect(); const align = win.document.documentElement.classList.contains('tabs-sidebar-right') ? 'right' : 'left'; - const previewRect = tabPreview.panel.getBoundingClientRect(); + const previewRect = tabPreview._panel.getBoundingClientRect(); const x = align == 'left' ? tabsSidebar.screenX + tabsSidebarRect.width - 2 : tabsSidebar.screenX - previewRect.width + 2; @@ -715,32 +735,50 @@ const BrowserWindowWatcher = { return [x, y]; }; - if (!tabPreview.__ws_orig__showPreview) - tabPreview.__ws_orig__showPreview = tabPreview.showPreview; - tabPreview.showPreview = function() { - if (typeof this.__ws__top == 'number') { - const [x, y] = tabPreview.__ws__calculateCoordinates(); - this.panel.openPopupAtScreen(x, y, false); + if (!tabPreview.__ws_orig__activate) + tabPreview.__ws_orig__activate = tabPreview.activate; + // https://searchfox.org/mozilla-esr128/rev/7998c47697fb3ba3e380eda1281c8280da81a0b1/browser/components/tabbrowser/content/tab-hover-preview.mjs#97 + const self = this; + tabPreview.activate = function(tab) { + if (this._isDisabled()) { + return; } - else { - this.panel.openPopup(this.tab, { - position: 'bottomleft topleft', - y: -2, - isContextMenu: false, - }); + this._tab = tab; + this._movePanel(); + + this._thumbnailElement = null; + this._maybeRequestThumbnail(); + if (this._panel.state == 'open') { + this._updatePreview(); } - win.addEventListener('wheel', this, { - capture: true, - passive: true, - }); - win.addEventListener('TabSelect', this); - this.panel.addEventListener('popuphidden', this); + if (this._timer) { + return; + } + this._timer = this._win.setTimeout(() => { + this._timer = null; + if (self.shouldShowSidebar(win.document) && + typeof this.__ws__top == 'number') { + const [x, y] = tabPreview.__ws__calculateCoordinates(); + this._panel.openPopupAtScreen(x, y, false); + } + else { + this._panel.openPopup(this._tab, { + // POPUP_OPTIONS https://searchfox.org/mozilla-esr128/rev/7998c47697fb3ba3e380eda1281c8280da81a0b1/browser/components/tabbrowser/content/tab-hover-preview.mjs#9 + position: 'bottomleft topleft', + x: 0, + y: -2, + }); + } + }, this._prefPreviewDelay); + this._win.addEventListener('TabSelect', this); + this._panel.addEventListener('popupshowing', this); }; - if (!tabPreview.panel.__ws_orig__moveToAnchor) - tabPreview.panel.__ws_orig__moveToAnchor = tabPreview.panel.moveToAnchor; - tabPreview.panel.moveToAnchor = function(...args) { - if (typeof tabPreview.__ws__top == 'number') { + if (!tabPreview._panel.__ws_orig__moveToAnchor) + tabPreview._panel.__ws_orig__moveToAnchor = tabPreview._panel.moveToAnchor; + tabPreview._panel.moveToAnchor = function(...args) { + if (self.shouldShowSidebar(win.document) && + typeof tabPreview.__ws__top == 'number') { const [x, y] = tabPreview.__ws__calculateCoordinates(); this.moveTo(x, y); return; @@ -749,22 +787,29 @@ const BrowserWindowWatcher = { }; }, - unpatchTabPreviewModules(win) { - const tabPreview = win.document.querySelector('#tabbrowser-tab-preview'); + unpatchTabHoverPreviewModules(win) { + const tabPreview = win.document.getElementById('tabbrowser-tabs')?._previewPanel; if (!tabPreview || - !tabPreview.showPreview) + !tabPreview.activate) return; - if (tabPreview.__ws_orig__showPreview) { - tabPreview.showPreview = tabPreview.__ws_orig__showPreview; - tabPreview.__ws_orig__showPreview = null; + if (tabPreview.__ws_orig__activate) { + tabPreview.activate = tabPreview.__ws_orig__activate; + tabPreview.__ws_orig__activate = null; + } + + if (tabPreview._panel?.__ws_orig__moveToAnchor) { + tabPreview._panel.moveToAnchor = tabPreview._panel.__ws_orig__moveToAnchor; + tabPreview._panel.__ws_orig__moveToAnchor = null; } }, patchToFullScreenModules(win) { const FullScreen = win.FullScreen; - const sidebarBox = win.document.querySelector('#tabs-sidebar-box'); - const sidebarSplitter = win.document.querySelector('#tabs-sidebar-splitter'); + const sidebarMainBox = win.document.querySelector('#sidebar-main'); + const sidebarBox = win.document.querySelector('#sidebar-box'); + const tabsSidebarBox = win.document.querySelector('#tabs-sidebar-box'); + const tabsSidebarSplitter = win.document.querySelector('#tabs-sidebar-splitter'); if (!FullScreen.__ws_orig__toggle) FullScreen.__ws_orig__toggle = FullScreen.toggle; @@ -775,7 +820,7 @@ const BrowserWindowWatcher = { if (enterFS) { if (!win.document.fullscreenElement) { if (win.gNavToolbox.getAttribute('fullscreenShouldAnimate') == 'true') - sidebarBox.setAttribute('fullscreenShouldAnimate', true); + tabsSidebarBox.setAttribute('fullscreenShouldAnimate', true); FullScreen.__ws_sidebar.hide(); } @@ -809,30 +854,32 @@ const BrowserWindowWatcher = { updateMouseTargetRect() { const contentsAreaRect = win.gBrowser.tabpanels.getBoundingClientRect(); + const tabsSidebarBoxRect = tabsSidebarBox.getBoundingClientRect(); + const sidebarMainBoxRect = sidebarMainBox.getBoundingClientRect(); const sidebarBoxRect = sidebarBox.getBoundingClientRect(); const isRight = this.isRightSide(); this._mouseTargetRect = { top: contentsAreaRect.top, bottom: contentsAreaRect.bottom, - left: contentsAreaRect.left + (isRight ? 0 : sidebarBoxRect.width + 50), - right: contentsAreaRect.right - (isRight ? sidebarBoxRect.width + 50 : 0), + left: Math.min(sidebarMainBoxRect.left, sidebarBoxRect.left, contentsAreaRect.left) + (isRight ? 0 : tabsSidebarBoxRect.width + 50), + right: Math.max(sidebarMainBoxRect.right, sidebarBoxRect.right, contentsAreaRect.right) - (isRight ? tabsSidebarBoxRect.width + 50 : 0), }; win.MousePosTracker.addListener(this); }, hide() { if (this.isRightSide()) - sidebarBox.style.marginRight = `-${sidebarBox.getBoundingClientRect().width}px`; + tabsSidebarBox.style.marginRight = `-${tabsSidebarBox.getBoundingClientRect().width}px`; else - sidebarBox.style.marginLeft = `-${sidebarBox.getBoundingClientRect().width}px`; + tabsSidebarBox.style.marginLeft = `-${tabsSidebarBox.getBoundingClientRect().width}px`; win.MousePosTracker.removeListener(this); this.startListenToShow(); }, show({ exitting } = {}) { - sidebarBox.removeAttribute('fullscreenShouldAnimate'); - sidebarBox.style.marginLeft = sidebarBox.style.marginRight = ''; + tabsSidebarBox.removeAttribute('fullscreenShouldAnimate'); + tabsSidebarBox.style.marginLeft = tabsSidebarBox.style.marginRight = ''; this.endListenToShow(); if (!exitting) { @@ -849,9 +896,9 @@ const BrowserWindowWatcher = { if (this.listeningToShow) return; this.listeningToShow = true; - sidebarSplitter.addEventListener('mouseover', FullScreen.__ws_sidebar); - sidebarSplitter.addEventListener('dragenter', FullScreen.__ws_sidebar); - sidebarSplitter.addEventListener('touchmove', FullScreen.__ws_sidebar, { + tabsSidebarSplitter.addEventListener('mouseover', FullScreen.__ws_sidebar); + tabsSidebarSplitter.addEventListener('dragenter', FullScreen.__ws_sidebar); + tabsSidebarSplitter.addEventListener('touchmove', FullScreen.__ws_sidebar, { passive: true, }); }, @@ -860,9 +907,9 @@ const BrowserWindowWatcher = { if (!this.listeningToShow) return; this.listeningToShow = false; - sidebarSplitter.removeEventListener('mouseover', FullScreen.__ws_sidebar); - sidebarSplitter.removeEventListener('dragenter', FullScreen.__ws_sidebar); - sidebarSplitter.removeEventListener('touchmove', FullScreen.__ws_sidebar, { + tabsSidebarSplitter.removeEventListener('mouseover', FullScreen.__ws_sidebar); + tabsSidebarSplitter.removeEventListener('dragenter', FullScreen.__ws_sidebar); + tabsSidebarSplitter.removeEventListener('touchmove', FullScreen.__ws_sidebar, { passive: true, }); }, @@ -1070,10 +1117,12 @@ const BrowserWindowWatcher = { break; case 'tabs-sidebar-bookmarkTab': - case 'tabs-sidebar-bookmarkTabs': - win.PlacesUIUtils.showBookmarkPagesDialog(win.PlacesCommandHook.uniqueSelectedPages); // same to #toolbar-context-bookmarkSelectedTab / #toolbar-context-bookmarkSelectedTabs + case 'tabs-sidebar-bookmarkTabs': { + const pages = win.PlacesCommandHook.getUniquePages(win.gBrowser.selectedTabs) + .map(page => Object.assign(page, { uri: Services.io.createExposableURI(page.uri) })); + win.PlacesUIUtils.showBookmarkPagesDialog(pages); // same to #toolbar-context-bookmarkSelectedTab / #toolbar-context-bookmarkSelectedTabs this.tryHidePopup(event); - break; + }; break; case 'tabs-sidebar-selectAll': win.gBrowser.selectAllTabs(); // same to #toolbar-context-selectAllTabs @@ -1130,16 +1179,22 @@ const BrowserWindowWatcher = { moveToolbarButtonToDefaultPosition() { try { - const state = JSON.parse(Services.prefs.getStringPref('browser.uiCustomization.state', '{}')); + const rawState = Services.prefs.getStringPref('browser.uiCustomization.state', '{}'); + if (!rawState) + return false; + + const state = JSON.parse(rawState); if (!state?.placements) return false; let foundInNavBar = false; + let found = false; for (const [name, items] of Object.entries(state.placements)) { if (!items.includes(this.id)) continue; if (name == 'nav-bar') foundInNavBar = true; + found = true; break; } @@ -1154,9 +1209,17 @@ const BrowserWindowWatcher = { if (foundInNavBar) lazy.CustomizableUI.moveWidgetWithinArea(this.id, index); - else + else if (!found) lazy.CustomizableUI.addWidgetToArea(this.id, this.defaultArea, index); + const win = Services.wm.getMostRecentBrowserWindow(); + win.setTimeout(() => { + if (win.document.getElementById(this.id)) + return; + console.log('failed to insert tabs sidebar button due to unhandled error in CustomizableUI module: retrying with reset'); + win.gCustomizeMode.reset(); + }, 250); // for safety, this delay need to be large enough + return true; } catch (error) { @@ -1223,13 +1286,18 @@ const BrowserWindowWatcher = { } }; break; - case 'browser.uiCustomization.state': + case 'browser.uiCustomization.state': { if (!Services.prefs.getStringPref(name)) { // resetting! - Services.wm.getMostRecentBrowserWindow().setTimeout(() => { + const tryInsertButton = () => { + if (!Services.prefs.getStringPref(name)) { + Services.wm.getMostRecentBrowserWindow().setTimeout(tryInsertButton, 10); + return; + } this.moveToolbarButtonToDefaultPosition(); - }, 100); + }; + tryInsertButton(); } - break; + }; break; } }, @@ -1546,17 +1614,15 @@ this.waterfoxBridge = class extends ExtensionAPI { async showPreviewPanel(tabId, top) { const tab = tabId && context.extension.tabManager.get(tabId); - if (!tab || - !Services.prefs.getBoolPref('browser.tabs.cardPreview.enabled', false)) + if (!tab) return; const document = tab.nativeTab.ownerDocument; - const tabPreview = document.querySelector('#tabbrowser-tab-preview'); + const tabPreview = document.getElementById('tabbrowser-tabs')?._previewPanel; if (!tabPreview) return; tabPreview.__ws__top = top; - tabPreview.tab = tab.nativeTab; - tabPreview.showPreview(); + tabPreview.activate(tab.nativeTab); }, async hidePreviewPanel(windowId) { @@ -1564,28 +1630,12 @@ this.waterfoxBridge = class extends ExtensionAPI { if (!win) return; - const tabPreview = win.window.document.querySelector('#tabbrowser-tab-preview'); + const tabPreview = document.getElementById('tabbrowser-tabs')?._previewPanel; if (!tabPreview) return; tabPreview.__ws__top = null; - tabPreview.tab = null; }, - onHoverPreviewChanged: new EventManager({ - context, - name: 'waterfoxBridge.onHoverPreviewChanged', - register: (fire) => { - const observe = (_subject, _topic, data) => { - fire.async(Services.prefs.getBoolPref('browser.tabs.cardPreview.enabled', false)).catch(() => {}); // ignore Message Manager disconnects - }; - Services.prefs.addObserver('browser.tabs.cardPreview.enabled', observe); - observe(); - return () => { - Services.prefs.removeObserver('browser.tabs.cardPreview.enabled', observe); - }; - }, - }).api(), - async openPreferences() { BrowserWindowWatcher.openOptions(); }, diff --git a/waterfox/browser/components/sidebar/experiments/waterfoxBridge.json b/waterfox/browser/components/sidebar/experiments/waterfoxBridge.json index 692a383fdb73a..38cb4c8760291 100644 --- a/waterfox/browser/components/sidebar/experiments/waterfoxBridge.json +++ b/waterfox/browser/components/sidebar/experiments/waterfoxBridge.json @@ -434,17 +434,6 @@ } ] }, - { - "name": "onHoverPreviewChanged", - "description": "Notified when hover preview is activated or deactivated.", - "type": "function", - "parameters": [ - { - "type": "boolean", - "name": "hoverPreviewEnabled" - } - ] - }, { "name": "onAutoplayBlocked", "description": "Notified when a tab's autoplay is blocked.", diff --git a/waterfox/browser/components/sidebar/manifest.json b/waterfox/browser/components/sidebar/manifest.json index f86cfe07d445a..8466e134ec2a3 100644 --- a/waterfox/browser/components/sidebar/manifest.json +++ b/waterfox/browser/components/sidebar/manifest.json @@ -1,291 +1,272 @@ { - "manifest_version": 2, - "name": "__MSG_extensionName__", - "version": "1.0.2.1", - "author": "Waterfox", - "hidden": true, - "description": "__MSG_extensionDescription__", - "permissions": [ - "activeTab", - "bookmarks", - "clipboardRead", - "contextualIdentities", - "cookies", - "menus", - "menus.overrideContext", - "notifications", - "scripting", - "search", - "sessions", - "storage", - "tabs", - "theme" - ], - "optional_permissions": [ - "" - ], - "background": { - "page": "background/background.html" - }, - "commands": { - "reloadTree": { - "description": "__MSG_context_reloadTree_command__" - }, - "reloadDescendants": { - "description": "__MSG_context_reloadDescendants_command__" - }, - "unblockAutoplayTree": { - "description": "__MSG_context_unblockAutoplayTree_command__" - }, - "unblockAutoplayDescendants": { - "description": "__MSG_context_unblockAutoplayDescendants_command__" - }, - "toggleMuteTree": { - "description": "__MSG_context_toggleMuteTree_command__" - }, - "toggleMuteDescendants": { - "description": "__MSG_context_toggleMuteDescendants_command__" - }, - "closeTree": { - "description": "__MSG_context_closeTree_command__" - }, - "closeDescendants": { - "description": "__MSG_context_closeDescendants_command__" - }, - "closeOthers": { - "description": "__MSG_context_closeOthers_command__" - }, - "toggleSticky": { - "description": "__MSG_context_toggleSticky_command__" - }, - "collapseTree": { - "description": "__MSG_context_collapseTree_command__" - }, - "collapseTreeRecursively": { - "description": "__MSG_context_collapseTreeRecursively_command__" - }, - "collapseAll": { - "description": "__MSG_context_collapseAll_command__" - }, - "expandTree": { - "description": "__MSG_context_expandTree_command__" - }, - "expandTreeRecursively": { - "description": "__MSG_context_expandTreeRecursively_command__" - }, - "expandAll": { - "description": "__MSG_context_expandAll_command__" - }, - "toggleTreeCollapsed": { - "description": "__MSG_command_toggleTreeCollapsed__" - }, - "toggleTreeCollapsedRecursively": { - "description": "__MSG_command_toggleTreeCollapsedRecursively__" - }, - "bookmarkTree": { - "description": "__MSG_context_bookmarkTree_command__" - }, - "newIndependentTab": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_independent_command__" - }, - "newChildTab": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_child_command__" - }, - "newChildTabTop": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_childTop_command__" - }, - "newChildTabEnd": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_childEnd_command__" - }, - "newSiblingTab": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_sibling_command__" - }, - "newNextSiblingTab": { - "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_nextSibling_command__" - }, - "newContainerTab": { - "description": "__MSG_tabbar_newTabWithContexualIdentity_tooltip__" - }, - "tabMoveUp": { - "description": "__MSG_command_tabMoveUp__" - }, - "treeMoveUp": { - "description": "__MSG_command_treeMoveUp__" - }, - "tabMoveDown": { - "description": "__MSG_command_tabMoveDown__" - }, - "treeMoveDown": { - "description": "__MSG_command_treeMoveDown__" - }, - "focusPrevious": { - "description": "__MSG_command_focusPrevious__" - }, - "focusPreviousSilently": { - "description": "__MSG_command_focusPreviousSilently__" - }, - "focusNext": { - "description": "__MSG_command_focusNext__" - }, - "focusNextSilently": { - "description": "__MSG_command_focusNextSilently__" - }, - "focusParent": { - "description": "__MSG_command_focusParent__" - }, - "focusParentOrCollapse": { - "description": "__MSG_command_focusParentOrCollapse__" - }, - "focusFirstChild": { - "description": "__MSG_command_focusFirstChild__" - }, - "focusFirstChildOrExpand": { - "description": "__MSG_command_focusFirstChildOrExpand__" - }, - "focusLastChild": { - "description": "__MSG_command_focusLastChild__" - }, - "focusPreviousSibling": { - "description": "__MSG_command_focusPreviousSibling__" - }, - "focusNextSibling": { - "description": "__MSG_command_focusNextSibling__" - }, - "simulateUpOnTree": { - "description": "__MSG_command_simulateUpOnTree__", - "suggested_key": { - "default": "Alt+Shift+Up" - } - }, - "simulateDownOnTree": { - "description": "__MSG_command_simulateDownOnTree__", - "suggested_key": { - "default": "Alt+Shift+Down" - } - }, - "simulateLeftOnTree": { - "description": "__MSG_command_simulateLeftOnTree__", - "suggested_key": { - "default": "Alt+Shift+Left" - } - }, - "simulateRightOnTree": { - "description": "__MSG_command_simulateRightOnTree__", - "suggested_key": { - "default": "Alt+Shift+Right" - } - }, - "tabbarUp": { - "description": "__MSG_command_tabbarUp__", - "suggested_key": { - "default": "Alt+Up" - } - }, - "tabbarPageUp": { - "description": "__MSG_command_tabbarPageUp__", - "suggested_key": { - "default": "Alt+PageUp" - } - }, - "tabbarHome": { - "description": "__MSG_command_tabbarHome__", - "suggested_key": { - "default": "Alt+Shift+Home" - } - }, - "tabbarDown": { - "description": "__MSG_command_tabbarDown__", - "suggested_key": { - "default": "Alt+Down" - } - }, - "tabbarPageDown": { - "description": "__MSG_command_tabbarPageDown__", - "suggested_key": { - "default": "Alt+PageDown" - } - }, - "tabbarEnd": { - "description": "__MSG_command_tabbarEnd__", - "suggested_key": { - "default": "Alt+End" - } - }, - "toggleSubPanel": { - "description": "__MSG_command_toggleSubPanel__", - "suggested_key": { - "default": "F2" - } - }, - "switchSubPanel": { - "description": "__MSG_command_switchSubPanel__" - }, - "increaseSubPanel": { - "description": "__MSG_command_increaseSubPanel__" - }, - "decreaseSubPanel": { - "description": "__MSG_command_decreaseSubPanel__" - } - }, - "web_accessible_resources": [ - "/resources/group-tab.html*" - ], - "protocol_handlers": [ - { - "protocol": "ext+ws", - "name": "Waterfox", - "uriTemplate": "/resources/protocol-handler.html?%s" - } - ], - "experiment_apis": { - "prefs": { - "schema": "experiments/prefs.json", - "parent": { - "scopes": [ - "addon_parent" - ], - "paths": [ - [ - "prefs" - ] - ], - "script": "experiments/prefs.js" - } - }, - "syncPrefs": { - "schema": "experiments/syncPrefs.json", - "child": { - "scopes": [ - "addon_child" - ], - "paths": [ - [ - "syncPrefs" - ] - ], - "script": "experiments/syncPrefs.js" - } - }, - "waterfoxBridge": { - "schema": "experiments/waterfoxBridge.json", - "parent": { - "scopes": [ - "addon_parent" - ], - "paths": [ - [ - "waterfoxBridge" - ] - ], - "script": "experiments/waterfoxBridge.js" - } - } - }, - "default_locale": "en", - "browser_specific_settings": { - "gecko": { - "id": "sidebar@waterfox.net", - "strict_min_version": "115.0" - } - } + "manifest_version": 2, + "name": "__MSG_extensionName__", + "version": "1.0.3", + "author": "Waterfox", + "hidden": true, + "description": "__MSG_extensionDescription__", + "permissions": [ + "activeTab", + "bookmarks", + "clipboardRead", + "contextualIdentities", + "cookies", + "menus", + "menus.overrideContext", + "notifications", + "search", + "sessions", + "storage", + "tabs", + "theme", + "" + ], + "background": { + "page": "background/background.html" + }, + "commands": { + "reloadTree": { + "description": "__MSG_context_reloadTree_command__" + }, + "reloadDescendants": { + "description": "__MSG_context_reloadDescendants_command__" + }, + "unblockAutoplayTree": { + "description": "__MSG_context_unblockAutoplayTree_command__" + }, + "unblockAutoplayDescendants": { + "description": "__MSG_context_unblockAutoplayDescendants_command__" + }, + "toggleMuteTree": { + "description": "__MSG_context_toggleMuteTree_command__" + }, + "toggleMuteDescendants": { + "description": "__MSG_context_toggleMuteDescendants_command__" + }, + "closeTree": { + "description": "__MSG_context_closeTree_command__" + }, + "closeDescendants": { + "description": "__MSG_context_closeDescendants_command__" + }, + "closeOthers": { + "description": "__MSG_context_closeOthers_command__" + }, + "toggleSticky": { + "description": "__MSG_context_toggleSticky_command__" + }, + "collapseTree": { + "description": "__MSG_context_collapseTree_command__" + }, + "collapseTreeRecursively": { + "description": "__MSG_context_collapseTreeRecursively_command__" + }, + "collapseAll": { + "description": "__MSG_context_collapseAll_command__" + }, + "expandTree": { + "description": "__MSG_context_expandTree_command__" + }, + "expandTreeRecursively": { + "description": "__MSG_context_expandTreeRecursively_command__" + }, + "expandAll": { + "description": "__MSG_context_expandAll_command__" + }, + "toggleTreeCollapsed": { + "description": "__MSG_command_toggleTreeCollapsed__" + }, + "toggleTreeCollapsedRecursively": { + "description": "__MSG_command_toggleTreeCollapsedRecursively__" + }, + "bookmarkTree": { + "description": "__MSG_context_bookmarkTree_command__" + }, + "newIndependentTab": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_independent_command__" + }, + "newChildTab": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_child_command__" + }, + "newChildTabTop": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_childTop_command__" + }, + "newChildTabEnd": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_childEnd_command__" + }, + "newSiblingTab": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_sibling_command__" + }, + "newNextSiblingTab": { + "description": "__MSG_tabbar_newTabButton_tooltip__: __MSG_tabbar_newTabAction_nextSibling_command__" + }, + "newContainerTab": { + "description": "__MSG_tabbar_newTabWithContexualIdentity_tooltip__" + }, + "tabMoveUp": { + "description": "__MSG_command_tabMoveUp__" + }, + "treeMoveUp": { + "description": "__MSG_command_treeMoveUp__" + }, + "tabMoveDown": { + "description": "__MSG_command_tabMoveDown__" + }, + "treeMoveDown": { + "description": "__MSG_command_treeMoveDown__" + }, + "focusPrevious": { + "description": "__MSG_command_focusPrevious__" + }, + "focusPreviousSilently": { + "description": "__MSG_command_focusPreviousSilently__" + }, + "focusNext": { + "description": "__MSG_command_focusNext__" + }, + "focusNextSilently": { + "description": "__MSG_command_focusNextSilently__" + }, + "focusParent": { + "description": "__MSG_command_focusParent__" + }, + "focusParentOrCollapse": { + "description": "__MSG_command_focusParentOrCollapse__" + }, + "focusFirstChild": { + "description": "__MSG_command_focusFirstChild__" + }, + "focusFirstChildOrExpand": { + "description": "__MSG_command_focusFirstChildOrExpand__" + }, + "focusLastChild": { + "description": "__MSG_command_focusLastChild__" + }, + "focusPreviousSibling": { + "description": "__MSG_command_focusPreviousSibling__" + }, + "focusNextSibling": { + "description": "__MSG_command_focusNextSibling__" + }, + "simulateUpOnTree": { + "description": "__MSG_command_simulateUpOnTree__", + "suggested_key": { + "default": "Alt+Shift+Up", + "mac": "MacCtrl+Shift+Up" + } + }, + "simulateDownOnTree": { + "description": "__MSG_command_simulateDownOnTree__", + "suggested_key": { + "default": "Alt+Shift+Down", + "mac": "MacCtrl+Shift+Down" + } + }, + "simulateLeftOnTree": { + "description": "__MSG_command_simulateLeftOnTree__", + "suggested_key": { + "default": "Alt+Shift+Left", + "mac": "MacCtrl+Shift+Left" + } + }, + "simulateRightOnTree": { + "description": "__MSG_command_simulateRightOnTree__", + "suggested_key": { + "default": "Alt+Shift+Right", + "mac": "MacCtrl+Shift+Right" + } + }, + "tabbarUp": { + "description": "__MSG_command_tabbarUp__", + "suggested_key": { + "default": "Alt+Up" + } + }, + "tabbarPageUp": { + "description": "__MSG_command_tabbarPageUp__", + "suggested_key": { + "default": "Alt+PageUp" + } + }, + "tabbarHome": { + "description": "__MSG_command_tabbarHome__", + "suggested_key": { + "default": "Alt+Shift+Home" + } + }, + "tabbarDown": { + "description": "__MSG_command_tabbarDown__", + "suggested_key": { + "default": "Alt+Down" + } + }, + "tabbarPageDown": { + "description": "__MSG_command_tabbarPageDown__", + "suggested_key": { + "default": "Alt+PageDown" + } + }, + "tabbarEnd": { + "description": "__MSG_command_tabbarEnd__", + "suggested_key": { + "default": "Alt+End" + } + }, + "toggleSubPanel": { + "description": "__MSG_command_toggleSubPanel__", + "suggested_key": { + "default": "F2" + } + }, + "switchSubPanel": { + "description": "__MSG_command_switchSubPanel__" + }, + "increaseSubPanel": { + "description": "__MSG_command_increaseSubPanel__" + }, + "decreaseSubPanel": { + "description": "__MSG_command_decreaseSubPanel__" + } + }, + "web_accessible_resources": ["/resources/group-tab.html*"], + "protocol_handlers": [ + { + "protocol": "ext+ws", + "name": "Waterfox", + "uriTemplate": "/resources/protocol-handler.html?%s" + } + ], + "experiment_apis": { + "prefs": { + "schema": "experiments/prefs.json", + "parent": { + "scopes": ["addon_parent"], + "paths": [["prefs"]], + "script": "experiments/prefs.js" + } + }, + "syncPrefs": { + "schema": "experiments/syncPrefs.json", + "child": { + "scopes": ["addon_child"], + "paths": [["syncPrefs"]], + "script": "experiments/syncPrefs.js" + } + }, + "waterfoxBridge": { + "schema": "experiments/waterfoxBridge.json", + "parent": { + "scopes": ["addon_parent"], + "paths": [["waterfoxBridge"]], + "script": "experiments/waterfoxBridge.js" + } + } + }, + "default_locale": "en", + "browser_specific_settings": { + "gecko": { + "id": "sidebar@waterfox.net", + "strict_min_version": "128.0" + } + } } diff --git a/waterfox/browser/components/sidebar/options/init.js b/waterfox/browser/components/sidebar/options/init.js index 6b32944b93fed..cf988b81aa6fe 100644 --- a/waterfox/browser/components/sidebar/options/init.js +++ b/waterfox/browser/components/sidebar/options/init.js @@ -55,7 +55,8 @@ const options = new Options(configs, { }); document.title = browser.i18n.getMessage('config_title'); -if ((location.hash && location.hash != '#') || +if ((location.hash && + /^#!?$/.test(location.hash)) || /independent=true/.test(location.search)) document.body.classList.add('independent'); @@ -139,6 +140,8 @@ let mUserStyleRulesFieldEditor; const mDarkModeMedia = window.matchMedia('(prefers-color-scheme: dark)'); +let mShowExpertOptionsTemporarily = false; + function onConfigChanged(key) { const value = configs[key]; switch (key) { @@ -181,19 +184,24 @@ function onConfigChanged(key) { } }; break; - case 'showExpertOptions': - document.documentElement.classList.toggle('show-expert-options', configs.showExpertOptions); + case 'showExpertOptions': { + if (mShowExpertOptionsTemporarily && !configs.showExpertOptions) + document.querySelector('#showExpertOptions').checked = true; + const show = mShowExpertOptionsTemporarily || configs.showExpertOptions; + document.documentElement.classList.toggle('show-expert-options', show); for (const item of document.querySelectorAll('#parentTabOperationBehaviorModeGroup li li')) { const radio = item.querySelector('input[type="radio"]'); - if (configs.showExpertOptions || radio.checked) { + if (show || radio.checked) { item.style.display = ''; - radio.style.display = configs.showExpertOptions ? '' : 'none'; + radio.style.display = show || radio.checked ? '' : 'none'; } else { - item.style.display = radio.style.display = 'none'; + item.style.display = radio.style.display = 'none'; } } - break; + if (mShowExpertOptionsTemporarily && !configs.showExpertOptions) + mShowExpertOptionsTemporarily = false; + }; break; case 'syncDeviceInfo': { const name = (configs.syncDeviceInfo || {}).name || ''; @@ -614,6 +622,12 @@ window.addEventListener('DOMContentLoaded', async () => { console.error(error); } + mShowExpertOptionsTemporarily = !!( + location.hash && + !/^#!?$/.test(location.hash) && + document.querySelector(`.expert #${location.hash.replace(/^#!?/, '')}, .expert#${location.hash.replace(/^#!?/, '')}`) + ); + try { options.buildUIForAllConfigs(document.querySelector('#group-allConfigs')); onConfigChanged('successorTabControlLevel'); @@ -801,6 +815,9 @@ function initPermissionOptions() { Permissions.CLIPBOARD_READ, document.querySelector('#clipboardReadPermissionGranted_middleClickPasteURLOnNewTabButton'), { + onInitialized: (granted) => { + return granted && configs.middleClickPasteURLOnNewTabButton; + }, onChanged: (granted) => { configs.middleClickPasteURLOnNewTabButton = granted; } diff --git a/waterfox/browser/components/sidebar/options/options.css b/waterfox/browser/components/sidebar/options/options.css index ce60bc4ba183e..797fdde652ced 100644 --- a/waterfox/browser/components/sidebar/options/options.css +++ b/waterfox/browser/components/sidebar/options/options.css @@ -131,7 +131,7 @@ label.has-radio:not(.inline) { label input[type="radio"] ~ img, label input[type="checkbox"] ~ img { - border: 1px solid ThreeDShadow; + border: 1px solid var(--ThreeDShadow); vertical-align: middle; margin-top: 0.15em; margin-bottom: 0.15em; diff --git a/waterfox/browser/components/sidebar/options/options.html b/waterfox/browser/components/sidebar/options/options.html index b1d0951566d92..97bd776775a82 100644 --- a/waterfox/browser/components/sidebar/options/options.html +++ b/waterfox/browser/components/sidebar/options/options.html @@ -770,6 +770,16 @@

__MSG_config_newTab_caption__

__MSG_groupTab_temporaryAggressive_label__ __MSG_config_groupTabTemporaryState_option_checked_after__

+

@@ -1286,6 +1296,12 @@

__MSG_config_drag_caption__

__MSG_config_insertDroppedTabsAt_end__ + +

+