From ec8b6beb01ac35517742e6cb17d4d0907d3aa426 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Thu, 5 Nov 2026 22:52:32 -0500 Subject: [PATCH] Keep item pane, quick search, and column settings per window Each window keeps its own item pane section order, pinned and collapsed sections, quick search mode, and item tree columns, so changing them in one window doesn't affect others. The settings are saved and restored with the window and copied to windows opened from it, and the most recent change is saved globally for windows that start without them. --- .../content/zotero/collectionViewItemTree.jsx | 4 +- .../zotero/elements/collapsibleSection.js | 11 +- chrome/content/zotero/elements/itemDetails.js | 16 +-- .../zotero/elements/itemPaneSidenav.js | 17 ++- chrome/content/zotero/elements/libraryView.js | 4 +- .../zotero/elements/quickSearchTextbox.js | 8 +- chrome/content/zotero/itemTree.jsx | 9 ++ chrome/content/zotero/xpcom/prefs.js | 66 +++++++++- chrome/content/zotero/xpcom/zotero.js | 10 +- chrome/content/zotero/zoteroPane.js | 121 +++++++++++++++++- test/tests/tabsTest.js | 66 ++++++++++ 11 files changed, 295 insertions(+), 37 deletions(-) diff --git a/chrome/content/zotero/collectionViewItemTree.jsx b/chrome/content/zotero/collectionViewItemTree.jsx index 22d1326536..e3a309632b 100644 --- a/chrome/content/zotero/collectionViewItemTree.jsx +++ b/chrome/content/zotero/collectionViewItemTree.jsx @@ -350,7 +350,9 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider { let rowChanged; switch (type) { case 'search': - rowChanged = collectionTreeRow.setSearch(data, Zotero.Prefs.get('search.quicksearch-mode')); + rowChanged = collectionTreeRow.setSearch( + data, Zotero.Prefs.getForWindow(window, 'search.quicksearch-mode') + ); break; case 'citation-search': rowChanged = collectionTreeRow.setSearch(data, 'fields'); diff --git a/chrome/content/zotero/elements/collapsibleSection.js b/chrome/content/zotero/elements/collapsibleSection.js index c509da76db..a6bda3dba9 100644 --- a/chrome/content/zotero/elements/collapsibleSection.js +++ b/chrome/content/zotero/elements/collapsibleSection.js @@ -200,7 +200,9 @@ this.render(); }); - this._prefsObserverID = Zotero.Prefs.registerObserver(`panes.${this.dataset.pane}.open`, this._restoreOpenState.bind(this)); + this._unobserveOpenState = Zotero.Prefs.observeForWindow( + window, `panes.${this.dataset.pane}.open`, this._restoreOpenState.bind(this) + ); if (this.hasAttribute('data-l10n-id') && !this.hasAttribute('data-l10n-args')) { this.setAttribute('data-l10n-args', JSON.stringify({ count: 0 })); @@ -358,12 +360,13 @@ this._contextMenu?.remove(); - Zotero.Prefs.unregisterObserver(this._prefsObserverID); + this._unobserveOpenState?.(); + this._unobserveOpenState = null; } _saveOpenState() { if (this._disableSavingOpenState || this._skipSaveOpenState) return; - Zotero.Prefs.set(`panes.${this.dataset.pane}.open`, this.open); + Zotero.Prefs.setForWindow(window, `panes.${this.dataset.pane}.open`, this.open); } _restoreOpenState() { @@ -372,7 +375,7 @@ return; } this._restoringOpenState = true; - this.open = Zotero.Prefs.get(`panes.${this.dataset.pane}.open`) ?? true; + this.open = Zotero.Prefs.getForWindow(window, `panes.${this.dataset.pane}.open`) ?? true; this._restoringOpenState = false; } diff --git a/chrome/content/zotero/elements/itemDetails.js b/chrome/content/zotero/elements/itemDetails.js index 437d1de60b..d05b447805 100644 --- a/chrome/content/zotero/elements/itemDetails.js +++ b/chrome/content/zotero/elements/itemDetails.js @@ -187,7 +187,9 @@ this._notifierID = Zotero.Notifier.registerObserver( this, ['item', 'itempane', 'tab'], 'ItemDetails'); - this._prefsObserverID = Zotero.Prefs.registerObserver('pinnedPane', this._restorePinnedPane.bind(this)); + this._unobservePinnedPane = Zotero.Prefs.observeForWindow( + window, 'pinnedPane', this._restorePinnedPane.bind(this) + ); this._disableScrollHandler = false; this._pinnedPaneMinScrollHeight = 0; @@ -211,7 +213,8 @@ this._intersectionOb.disconnect(); Zotero.Notifier.unregisterObserver(this._notifierID); - Zotero.Prefs.unregisterObserver(this._prefsObserverID); + this._unobservePinnedPane?.(); + this._unobservePinnedPane = null; } async render() { @@ -666,19 +669,14 @@ return; } let pinnedPane = this.pinnedPane; - if (pinnedPane) { - Zotero.Prefs.set('pinnedPane', pinnedPane); - } - else { - Zotero.Prefs.clear('pinnedPane'); - } + Zotero.Prefs.setForWindow(window, 'pinnedPane', pinnedPane || undefined); } _restorePinnedPane() { if (this.tabType !== 'library') { return; } - let pinnedPane = Zotero.Prefs.get('pinnedPane') || ''; + let pinnedPane = Zotero.Prefs.getForWindow(window, 'pinnedPane') || ''; if (this.pinnedPane !== pinnedPane) { this.pinnedPane = pinnedPane; } diff --git a/chrome/content/zotero/elements/itemPaneSidenav.js b/chrome/content/zotero/elements/itemPaneSidenav.js index c376a35d45..b8a92ed72e 100644 --- a/chrome/content/zotero/elements/itemPaneSidenav.js +++ b/chrome/content/zotero/elements/itemPaneSidenav.js @@ -87,7 +87,7 @@ _dropIndicator = null; - _prefObserverID = null; + _unobservePaneOrder = null; get observedAttributes() { return ["no-context-notes"]; @@ -341,7 +341,9 @@ }); this.setAttribute("role", "tablist"); - this._prefObserverID = Zotero.Prefs.registerObserver("sidenav.order", this.handlePaneOrderChange); + this._unobservePaneOrder = Zotero.Prefs.observeForWindow( + window, "sidenav.order", this.handlePaneOrderChange + ); this._initialized = true; } @@ -357,7 +359,8 @@ this._buttonContainer.removeEventListener('dragend', this.handleButtonDragEnd); this._buttonContainer.removeEventListener('dragleave', this.handleButtonDragLeave); - Zotero.Prefs.unregisterObserver(this._prefObserverID); + this._unobservePaneOrder?.(); + this._unobservePaneOrder = null; this._initialized = false; } @@ -456,13 +459,13 @@ currentOrder.push(paneID); } - Zotero.Prefs.set("sidenav.order", currentOrder.join(",")); + Zotero.Prefs.setForWindow(window, "sidenav.order", currentOrder.join(",")); } getPersistedOrder(value = null) { if (value === null) { - value = Zotero.Prefs.get("sidenav.order"); + value = Zotero.Prefs.getForWindow(window, "sidenav.order"); } if (!value) return this._builtInPanes; try { @@ -991,10 +994,10 @@ } }; - handlePaneOrderChange = async (value) => { + handlePaneOrderChange = async () => { // If no container, wait until it's set if (!this.container) return; - let order = this.getPersistedOrder(value); + let order = this.getPersistedOrder(); let hasChange = false; for (let i = 0; i < order.length; i++) { let paneID = order[i]; diff --git a/chrome/content/zotero/elements/libraryView.js b/chrome/content/zotero/elements/libraryView.js index 616a329ace..a0fbd92bfb 100644 --- a/chrome/content/zotero/elements/libraryView.js +++ b/chrome/content/zotero/elements/libraryView.js @@ -1131,7 +1131,7 @@ // items list refreshes once let searchText = state.quickSearch || ''; let tags = new Set(state.tags?.selected || []); - let mode = Zotero.Prefs.get('search.quicksearch-mode'); + let mode = Zotero.Prefs.getForWindow(window, 'search.quicksearch-mode'); // A quick search with conditions in it is run as a search, as search() does let query = Zotero.SearchQuery.getSearch(searchText, { mode }); this._quickSearchIsQuery = !!query; @@ -1551,7 +1551,7 @@ // carries none of its own -- with one, a multi-library selection would // match only in that library let query = Zotero.SearchQuery.getSearch(searchVal, { - mode: Zotero.Prefs.get('search.quicksearch-mode') + mode: Zotero.Prefs.getForWindow(window, 'search.quicksearch-mode') }); if (query) { this._quickSearchIsQuery = true; diff --git a/chrome/content/zotero/elements/quickSearchTextbox.js b/chrome/content/zotero/elements/quickSearchTextbox.js index f20c25ca1d..a1ce4c6d96 100644 --- a/chrome/content/zotero/elements/quickSearchTextbox.js +++ b/chrome/content/zotero/elements/quickSearchTextbox.js @@ -113,7 +113,7 @@ event.stopPropagation(); // If there's text in the field, seed the Advanced Search with it, // reproducing the current quick search mode as editable conditions. - let mode = Zotero.Prefs.get('search.quicksearch-mode'); + let mode = Zotero.Prefs.getForWindow(window, 'search.quicksearch-mode'); // Shift-click opens the Advanced Search in a new window, and // Cmd/Ctrl-click in a new tab if (event.shiftKey) { @@ -173,7 +173,7 @@ item.value = mode; item.addEventListener('command', () => { - Zotero.Prefs.set("search.quicksearch-mode", mode); + Zotero.Prefs.setForWindow(window, "search.quicksearch-mode", mode); this.updateMode(); if (this.value) { @@ -193,10 +193,10 @@ } updateMode() { - let mode = Zotero.Prefs.get("search.quicksearch-mode"); + let mode = Zotero.Prefs.getForWindow(window, "search.quicksearch-mode"); if (!this._searchModes[mode]) { - Zotero.Prefs.set("search.quicksearch-mode", "fields"); + Zotero.Prefs.setForWindow(window, "search.quicksearch-mode", "fields"); mode = 'fields'; } diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index c66fbe78be..8fffab0862 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -2504,6 +2504,7 @@ var ItemTree = class ItemTree extends LibraryTree { } } this._columnPrefs = prefs; + window.ZoteroPane?.setItemTreeColumnPrefs?.(this.prefsId, prefs); // Invalidate the cached _columns array; next _getColumns() call will // rebuild canonical state from COLUMNS defs + _columnPrefs + viewType. this._columnsId = null; @@ -2519,8 +2520,16 @@ var ItemTree = class ItemTree extends LibraryTree { _loadColumnPrefsFromFile = async () => { if (!this.props.columnPicker) return; + // A main window keeps its own column settings, starting with the ones saved most + // recently in any window + let windowPrefs = window.ZoteroPane?.getItemTreeColumnPrefs?.(this.prefsId); + if (windowPrefs) { + this._columnPrefs = windowPrefs; + return; + } await _migrateTreePrefsFile(); this._columnPrefs = await TreePrefs.get(this.prefsId); + window.ZoteroPane?.setItemTreeColumnPrefs?.(this.prefsId, this._columnPrefs); } /** diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js index 9e80ad3c23..14e8682be1 100644 --- a/chrome/content/zotero/xpcom/prefs.js +++ b/chrome/content/zotero/xpcom/prefs.js @@ -410,16 +410,10 @@ Zotero.Prefs = new function () { Zotero.logError(e); } }], + // Main windows keep their own mode (see ZoteroPane.getWindowSetting()) [ "search.quicksearch-mode", function (val) { var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); - var enumerator = wm.getEnumerator("navigator:browser"); - while (enumerator.hasMoreElements()) { - var win = enumerator.getNext(); - if (!win.ZoteroPane) continue; - Zotero.updateQuickSearchBox(win.ZoteroPane.document); - } - var enumerator = wm.getEnumerator("zotero:item-selector"); while (enumerator.hasMoreElements()) { var win = enumerator.getNext(); @@ -477,6 +471,64 @@ Zotero.Prefs = new function () { var _observers = {}; var _observersBySymbol = {}; + // Whether a pref is being set as a main window's own setting, which other windows ignore + this.settingForWindow = false; + + /** + * Get a setting that each main window keeps for itself (see ZoteroPane.getWindowSetting()), + * or the pref of the same name outside of a main window + * + * @param {Window} win + * @param {String} name + * @return {*} + */ + this.getForWindow = function (win, name) { + if (win.ZoteroPane?.getWindowSetting) { + return win.ZoteroPane.getWindowSetting(name); + } + return this.get(name); + }; + + /** + * @param {Window} win + * @param {String} name + * @param {*} value - undefined to clear + */ + this.setForWindow = function (win, name, value) { + if (win.ZoteroPane?.setWindowSetting) { + win.ZoteroPane.setWindowSetting(name, value); + } + else if (value === undefined) { + this.clear(name); + } + else { + this.set(name, value); + } + }; + + /** + * Observe a setting that each main window keeps for itself, or the pref of the same name + * outside of a main window + * + * @param {Window} win + * @param {String} name + * @param {Function} handler + * @return {Function} - Call to stop observing + */ + this.observeForWindow = function (win, name, handler) { + if (win.ZoteroPane?.getWindowSetting) { + let listener = (event) => { + if (event.detail.name == name) { + handler(); + } + }; + win.addEventListener('zotero-window-setting-change', listener); + return () => win.removeEventListener('zotero-window-setting-change', listener); + } + let symbol = this.registerObserver(name, handler); + return () => this.unregisterObserver(symbol); + }; + /** * @param {String} name - Preference name; if not global, this is on the extensions.zotero branch * @param {Function} handler diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 7f0ff2529a..776b728f05 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -1175,6 +1175,8 @@ const { CommandLineOptions } = ChromeUtils.importESModule("chrome://zotero/conte * @param {Object} [args.movedTab] - { windowID, tabID } of a tab in another window to move * into this one as its only tab * @param {Object} [args.geometry] - { screenX, screenY, width, height } to open the window at + * @param {Object} [args.windowSettings] - The window's own settings, as returned by + * ZoteroPane.getWindowSettings() -- by default, those of the most recent window * @return {ChromeWindow} */ this.openMainWindow = function (args) { @@ -1182,8 +1184,12 @@ const { CommandLineOptions } = ChromeUtils.importESModule("chrome://zotero/conte var flags = "chrome,all,dialog=no,resizable=yes"; var ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] .getService(Components.interfaces.nsIWindowWatcher); - // A new window takes its pane layout from the window it's opened from - Services.wm.getMostRecentWindow("navigator:browser")?.ZoteroPane?.serializePersist(); + // A new window takes its pane layout and settings from the window it's opened from + let sourcePane = Services.wm.getMostRecentWindow("navigator:browser")?.ZoteroPane; + sourcePane?.serializePersist(); + if (sourcePane?.getWindowSettings && !args?.sessionState && !args?.windowSettings) { + args = Object.assign({}, args, { windowSettings: sourcePane.getWindowSettings() }); + } // Box the args so they reliably cross openWindow()'s nsISupports argument return ww.openWindow(null, chromeURI, '_blank', flags, args ? { wrappedJSObject: args } : null); } diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 5028a10108..0f133acc71 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -244,6 +244,10 @@ var ZoteroPane = new function () { var _initialReaderTabState = null; var _initialNoteTabState = null; var _initialMovedTab = null; + // Settings this window keeps for itself (see getWindowSetting()) + var _windowSettings = normalizeWindowSettings(); + // Observers of the prefs behind this window's settings, by pref name + var _windowSettingObservers = new Map(); var lastFocusedElement = null; this.lastKeyPress = null; // When this window was last activated, for ordering saved window states @@ -606,6 +610,7 @@ var ZoteroPane = new function () { */ function takeWindowState() { let args = getWindowArgs(); + _windowSettings = normalizeWindowSettings(args.windowSettings); // Where a tab torn off from another window was dropped if (args.geometry) { restoreWindowGeometry(args.geometry); @@ -629,6 +634,7 @@ var ZoteroPane = new function () { } if (args.sessionState) { _sessionPaneState = args.sessionState; + _windowSettings = normalizeWindowSettings(_sessionPaneState.settings); restoreWindowGeometry(_sessionPaneState.geometry); return; } @@ -638,6 +644,9 @@ var ZoteroPane = new function () { let states = Zotero.Session.getPendingPaneStates(); _sessionPaneState = states[0] || null; _otherSessionPaneStates = states.slice(1); + if (_sessionPaneState?.settings) { + _windowSettings = normalizeWindowSettings(_sessionPaneState.settings); + } // The window opens with the persisted geometry, which is shared by all windows and // holds the position of whichever closed last, so when there are other windows to // restore, use its own saved geometry instead @@ -798,6 +807,12 @@ var ZoteroPane = new function () { Zotero.Keys.windowInit(document); + window.addEventListener('zotero-window-setting-change', (event) => { + if (event.detail.name == 'search.quicksearch-mode') { + Zotero.updateQuickSearchBox(document); + } + }); + if (Zotero.restoreFromServer) { Zotero.restoreFromServer = false; @@ -1057,6 +1072,10 @@ var ZoteroPane = new function () { ZoteroContextPane.destroy(); Zotero.Plugins.notifyMainWindowUnload(window); + for (let symbol of _windowSettingObservers.values()) { + Zotero.Prefs.unregisterObserver(symbol); + } + _windowSettingObservers.clear(); // A window closed before it restored its saved state shouldn't come back in the next // session, unless it's the last one @@ -7100,10 +7119,110 @@ var ZoteroPane = new function () { height: window.outerHeight, sizemode: document.documentElement.getAttribute('sizemode') }, - tabs: Zotero_Tabs.getState() + tabs: Zotero_Tabs.getState(), + settings: this.getWindowSettings() }; }; + /** + * Get a setting that this window keeps for itself, such as an item pane or quick search + * setting + * + * A window's settings are saved with its state and copied to windows opened from it, so + * changing one in a window doesn't affect other windows. A setting the window hasn't + * changed or been opened with comes from the pref of the same name, which holds the value + * set most recently in any window, and a change to the pref from outside of a main window + * applies to all windows. + * + * @param {String} name - Pref name + * @return {*} + */ + this.getWindowSetting = function (name) { + observeWindowSettingPref(name); + if (!(name in _windowSettings.prefs)) { + _windowSettings.prefs[name] = Zotero.Prefs.get(name); + } + return structuredClone(_windowSettings.prefs[name]); + }; + + /** + * Set a setting for this window, and save it to the pref of the same name for windows that + * open without one + * + * @param {String} name - Pref name + * @param {*} value - undefined to clear + */ + this.setWindowSetting = function (name, value) { + observeWindowSettingPref(name); + _windowSettings.prefs[name] = structuredClone(value); + Zotero.Prefs.settingForWindow = true; + try { + if (value === undefined) { + Zotero.Prefs.clear(name); + } + else { + Zotero.Prefs.set(name, value); + } + } + finally { + Zotero.Prefs.settingForWindow = false; + } + dispatchWindowSettingChange(name); + }; + + /** + * Get the column settings this window keeps for an item tree, if it has any + * + * @param {String} id - The tree's prefsId + * @return {Object|null} + */ + this.getItemTreeColumnPrefs = function (id) { + let prefs = _windowSettings.itemTreeColumns[id]; + return prefs ? structuredClone(prefs) : null; + }; + + /** + * @param {String} id - The tree's prefsId + * @param {Object} prefs + */ + this.setItemTreeColumnPrefs = function (id, prefs) { + _windowSettings.itemTreeColumns[id] = structuredClone(prefs); + }; + + /** + * @return {Object} - The settings this window keeps, for its state + */ + this.getWindowSettings = function () { + return structuredClone(_windowSettings); + }; + + function normalizeWindowSettings(settings) { + return { + prefs: structuredClone(settings?.prefs || {}), + itemTreeColumns: structuredClone(settings?.itemTreeColumns || {}) + }; + } + + function observeWindowSettingPref(name) { + if (_windowSettingObservers.has(name)) { + return; + } + _windowSettingObservers.set(name, Zotero.Prefs.registerObserver(name, () => { + // A change made as a window's own setting applies only to that window + if (Zotero.Prefs.settingForWindow) { + return; + } + _windowSettings.prefs[name] = Zotero.Prefs.get(name); + dispatchWindowSettingChange(name); + })); + } + + function dispatchWindowSettingChange(name) { + window.dispatchEvent( + new CustomEvent('zotero-window-setting-change', { detail: { name } }) + ); + } + /** * Unserializes zotero-persist elements from preferences * diff --git a/test/tests/tabsTest.js b/test/tests/tabsTest.js index 275e5745e9..5f27d06f7d 100644 --- a/test/tests/tabsTest.js +++ b/test/tests/tabsTest.js @@ -741,6 +741,72 @@ describe("Zotero_Tabs", function() { } }); + it("should keep item pane and quick search settings to each window", async function () { + await selectCollection(win, collection); + await win.ZoteroPane.selectItem(item.id); + await selectCollection(win2, collection); + await zp2.selectItem(item.id); + let getSection = w => w.ZoteroPane.itemPane._itemDetails + .querySelector('collapsible-section[data-pane="info"]'); + let open = getSection(win).open; + let mode = Zotero.Prefs.getForWindow(win, 'search.quicksearch-mode'); + let otherMode = mode == 'fields' ? 'titleCreatorYear' : 'fields'; + try { + getSection(win2).open = !open; + assert.equal(getSection(win).open, open); + // The pref holds the most recent change, for windows that open without one + assert.equal(Zotero.Prefs.get('panes.info.open'), !open); + + zp2.libraryView.getElement('zotero-tb-search').searchModePopup + .querySelector(`menuitem[value="${otherMode}"]`).doCommand(); + assert.equal(Zotero.Prefs.getForWindow(win2, 'search.quicksearch-mode'), otherMode); + assert.equal(Zotero.Prefs.getForWindow(win, 'search.quicksearch-mode'), mode); + } + finally { + getSection(win2).open = open; + Zotero.Prefs.setForWindow(win2, 'search.quicksearch-mode', mode); + } + }); + + it("should reopen a window with its own settings", async function () { + let mode = Zotero.Prefs.getForWindow(win, 'search.quicksearch-mode'); + let otherMode = mode == 'fields' ? 'titleCreatorYear' : 'fields'; + let isDateAddedHidden = view => view.itemsView._getColumns() + .find(column => column.dataKey == 'dateAdded').hidden; + let dateAddedHidden = isDateAddedHidden(zp.libraryView); + + let win3 = await loadZoteroPane(); + Zotero.Prefs.setForWindow(win3, 'search.quicksearch-mode', otherMode); + let itemsView = win3.ZoteroPane.itemsView; + itemsView.tree._columns.toggleHidden( + itemsView._getColumns().findIndex(column => column.dataKey == 'dateAdded') + ); + let openWindows = Zotero.getAllMainWindows(); + win3.close(); + await waitForCallback(() => win3.closed, 50, 20); + + await tabs.undoClose(); + let newWin; + try { + await waitForCallback( + () => (newWin = Zotero.getAllMainWindows().find(x => !openWindows.includes(x))) + && newWin.ZoteroPane.libraryView?.loaded, + 100, + 20 + ); + assert.equal(Zotero.Prefs.getForWindow(newWin, 'search.quicksearch-mode'), otherMode); + assert.equal(isDateAddedHidden(newWin.ZoteroPane.libraryView), !dateAddedHidden); + + assert.equal(Zotero.Prefs.getForWindow(win, 'search.quicksearch-mode'), mode); + await zp.itemsView.reloadColumnPrefs(); + assert.equal(isDateAddedHidden(zp.libraryView), dateAddedHidden); + } + finally { + newWin?.close(); + Zotero.Prefs.set('search.quicksearch-mode', mode); + } + }); + it("should open each selected collection in a tab of its own in a new window", async function () { let otherCollection = await createDataObject('collection'); await zp2.collectionsView.restoreSelection([collection.treeViewID, otherCollection.treeViewID]);