From 85b142ccb2e515dfeec2fffc22cfdfb4fc0a7f06 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Mon, 23 Aug 2021 20:02:57 +0300 Subject: [PATCH] Restore tabs state (#2148) --- chrome/content/zotero/contextPane.js | 97 +++++++++++++------------- chrome/content/zotero/tabs.js | 42 ++++++++++- chrome/content/zotero/xpcom/reader.js | 55 +++++++++++---- chrome/content/zotero/xpcom/session.js | 80 +++++++++++++++++++++ chrome/content/zotero/xpcom/zotero.js | 9 +++ chrome/content/zotero/zoteroPane.js | 27 ++++++- components/zotero-service.js | 1 + 7 files changed, 243 insertions(+), 68 deletions(-) create mode 100644 chrome/content/zotero/xpcom/session.js diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index 6b840af82c..0f9eb6d422 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -58,7 +58,7 @@ var ZoteroContextPane = new function () { this.update = _update; this.getActiveEditor = _getActiveEditor; - this.onLoad = function () { + this.init = function () { if (!Zotero) { return; } @@ -83,8 +83,33 @@ var ZoteroContextPane = new function () { else { _tabToolbar.style.right = 0; } + + // vbox + var vbox = document.createElement('vbox'); + vbox.setAttribute('flex', '1'); - _init(); + _contextPaneInner.append(vbox); + + // Toolbar extension + var toolbarExtension = document.createElement('box'); + toolbarExtension.style.height = '32px'; + toolbarExtension.id = 'zotero-context-toolbar-extension'; + + _panesDeck = document.createElement('deck'); + _panesDeck.setAttribute('flex', 1); + _panesDeck.setAttribute('selectedIndex', 0); + + vbox.append(toolbarExtension, _panesDeck); + + // Item pane deck + _itemPaneDeck = document.createElement('deck'); + // Notes pane deck + _notesPaneDeck = document.createElement('deck'); + _notesPaneDeck.style.backgroundColor = 'white'; + _notesPaneDeck.setAttribute('flex', 1); + _notesPaneDeck.className = 'notes-pane-deck'; + + _panesDeck.append(_itemPaneDeck, _notesPaneDeck); this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane'); window.addEventListener('resize', _update); @@ -94,7 +119,7 @@ var ZoteroContextPane = new function () { Zotero.Reader.onChangeSidebarOpen = _updatePaneWidth; }; - this.onUnload = function () { + this.destroy = function () { _itemToggle.removeEventListener('click', _toggleItemButton); _notesToggle.removeEventListener('click', _toggleNotesButton); window.removeEventListener('resize', _update); @@ -106,7 +131,7 @@ var ZoteroContextPane = new function () { _notesContexts = []; }; - this.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) { + this.notify = function (action, type, ids, extraData) { if (type == 'item') { // Update, remove or re-create item panes for (let context of _itemContexts.slice()) { @@ -174,14 +199,14 @@ var ZoteroContextPane = new function () { || !document.activeElement.closest('.context-node iframe[anonid="editor-view"]'))) { reader.focus(); } + + var attachment = await Zotero.Items.getAsync(reader.itemID); + if (attachment) { + _selectNotesContext(attachment.libraryID); + var notesContext = _getNotesContext(attachment.libraryID); + notesContext.updateFromCache(); + } })(); - - var attachment = Zotero.Items.get(reader.itemID); - if (attachment) { - _selectNotesContext(attachment.libraryID); - var notesContext = _getNotesContext(attachment.libraryID); - notesContext.updateFromCache(); - } } _contextPaneSplitter.setAttribute('hidden', false); @@ -193,7 +218,7 @@ var ZoteroContextPane = new function () { _update(); } } - }); + }; function _toggleItemButton() { _togglePane(0); @@ -346,35 +371,6 @@ var ZoteroContextPane = new function () { splitter.setAttribute('state', hide ? 'collapsed' : 'open'); _update(); } - - function _init() { - // vbox - var vbox = document.createElement('vbox'); - vbox.setAttribute('flex', '1'); - - _contextPaneInner.append(vbox); - - // Toolbar extension - var toolbarExtension = document.createElement('box'); - toolbarExtension.style.height = '32px'; - toolbarExtension.id = 'zotero-context-toolbar-extension'; - - _panesDeck = document.createElement('deck'); - _panesDeck.setAttribute('flex', 1); - _panesDeck.setAttribute('selectedIndex', 0); - - vbox.append(toolbarExtension, _panesDeck); - - // Item pane deck - _itemPaneDeck = document.createElement('deck'); - // Notes pane deck - _notesPaneDeck = document.createElement('deck'); - _notesPaneDeck.style.backgroundColor = 'white'; - _notesPaneDeck.setAttribute('flex', 1); - _notesPaneDeck.className = 'notes-pane-deck'; - - _panesDeck.append(_itemPaneDeck, _notesPaneDeck); - } function _getCurrentAttachment() { var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); @@ -737,7 +733,16 @@ var ZoteroContextPane = new function () { } } - function _addItemContext(tabID, itemID) { + async function _addItemContext(tabID, itemID) { + var container = document.createElement('vbox'); + container.id = tabID + '-context'; + container.className = 'zotero-item-pane-content'; + _itemPaneDeck.appendChild(container); + + var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); + var library = Zotero.Libraries.get(libraryID); + await library.waitForDataLoad('item'); + var item = Zotero.Items.get(itemID); if (!item) { return; @@ -745,11 +750,6 @@ var ZoteroContextPane = new function () { var libraryID = item.libraryID; var readOnly = _isLibraryReadOnly(libraryID); var parentID = item.parentID; - - var container = document.createElement('vbox'); - container.id = tabID + '-context'; - container.className = 'zotero-item-pane-content'; - _itemPaneDeck.appendChild(container); var context = { tabID, @@ -786,6 +786,3 @@ var ZoteroContextPane = new function () { itemBox.item = parentItem; } }; - -addEventListener('load', function (e) { ZoteroContextPane.onLoad(e); }, false); -addEventListener('unload', function (e) { ZoteroContextPane.onUnload(e); }, false); diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js index 064e99ca71..6ef495271a 100644 --- a/chrome/content/zotero/tabs.js +++ b/chrome/content/zotero/tabs.js @@ -85,17 +85,53 @@ var Zotero_Tabs = new function () { ); }; + this.getState = function () { + return this._tabs.map((tab) => { + var o = { + type: tab.type, + title: tab.title, + }; + if (tab.data) { + o.data = tab.data; + } + if (tab.id == this._selectedID) { + o.selected = true; + } + return o; + }); + }; + + this.restoreState = function(tabs) { + for (let tab of tabs) { + if (tab.type === 'library') { + this.rename('zotero-pane', tab.title); + } + else if (tab.type === 'reader') { + if (Zotero.Items.exists(tab.data.itemID)) { + Zotero.Reader.open(tab.data.itemID, + null, + { + title: tab.title, + openInBackground: !tab.selected + } + ); + } + } + } + }; + /** * Add a new tab * * @param {String} type * @param {String} title + * @param {String} data - Extra data about the tab to pass to notifier and session * @param {Integer} index * @param {Boolean} select * @param {Function} onClose * @return {{ id: string, container: XULElement}} id - tab id, container - a new tab container created in the deck */ - this.add = function ({ type, title, index, select, onClose, notifierData }) { + this.add = function ({ type, data, title, index, select, onClose }) { if (typeof type != 'string') { throw new Error(`'type' should be a string (was ${typeof type})`); } @@ -112,11 +148,11 @@ var Zotero_Tabs = new function () { var container = document.createElement('vbox'); container.id = id; this.deck.appendChild(container); - var tab = { id, type, title, onClose }; + var tab = { id, type, title, data, onClose }; index = index || this._tabs.length; this._tabs.splice(index, 0, tab); this._update(); - Zotero.Notifier.trigger('add', 'tab', [id], { [id]: notifierData }, true); + Zotero.Notifier.trigger('add', 'tab', [id], { [id]: data }, true); if (select) { this.select(id); } diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 467033ad37..24aa15a465 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -33,6 +33,7 @@ class ReaderInstance { this._window = null; this._iframeWindow = null; this._itemID = null; + this._title = ''; this._isReaderInitialized = false; this._showItemPaneToggle = false; this._initPromise = new Promise((resolve, reject) => { @@ -50,7 +51,11 @@ class ReaderInstance { } async open({ itemID, state, location }) { - let item = await Zotero.Items.getAsync(itemID); + let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); + let library = Zotero.Libraries.get(libraryID); + await library.waitForDataLoad('item'); + + let item = Zotero.Items.get(itemID); if (!item) { return false; } @@ -101,6 +106,7 @@ class ReaderInstance { title = parentItem.getDisplayTitle(); } } + this._title = title; this._setTitleValue(title); } @@ -644,7 +650,7 @@ class ReaderInstance { } class ReaderTab extends ReaderInstance { - constructor({ itemID, sidebarWidth, sidebarOpen, bottomPlaceholderHeight }) { + constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, background }) { super(); this._itemID = itemID; this._sidebarWidth = sidebarWidth; @@ -654,11 +660,11 @@ class ReaderTab extends ReaderInstance { this._window = Services.wm.getMostRecentWindow('navigator:browser'); let { id, container } = this._window.Zotero_Tabs.add({ type: 'reader', - title: '', - select: true, - notifierData: { + title: title || '', + data: { itemID - } + }, + select: !background }); this.tabID = id; this._tabContainer = container; @@ -832,6 +838,13 @@ class Reader { return this._sidebarWidth; } + async init() { + await Zotero.uiReadyPromise; + Zotero.Session.state.windows + .filter(x => x.type == 'reader' && Zotero.Items.exists(x.itemID)) + .forEach(x => this.open(x.itemID, null, { title: x.title, openInWindow: true })); + } + _loadSidebarOpenState() { let win = Zotero.getMainWindow(); if (win) { @@ -888,6 +901,10 @@ class Reader { this.triggerAnnotationsImportCheck(reader._itemID); } } + + if (event === 'add' || event === 'close') { + Zotero.Session.debounceSave(); + } } // Listen for parent item, PDF attachment and its annotations updates else if (type === 'item') { @@ -932,19 +949,25 @@ class Reader { getByTabID(tabID) { return this._readers.find(r => (r instanceof ReaderTab) && r.tabID === tabID); } - - async openURI(itemURI, location, openWindow) { - let item = await Zotero.URI.getURIItem(itemURI); - if (!item) return; - await this.open(item.id, location, openWindow); + + getWindowStates() { + return this._readers + .filter(r => r instanceof ReaderWindow) + .map(r => ({ type: 'reader', itemID: r._itemID, title: r._title })); } - async open(itemID, location, openWindow) { + async openURI(itemURI, location, options) { + let item = await Zotero.URI.getURIItem(itemURI); + if (!item) return; + await this.open(item.id, location, options); + } + + async open(itemID, location, { title, openInBackground, openInWindow } = {}) { this._loadSidebarOpenState(); this.triggerAnnotationsImportCheck(itemID); let reader; - if (openWindow) { + if (openInWindow) { reader = this._readers.find(r => r._itemID === itemID && (r instanceof ReaderWindow)); } else { @@ -960,7 +983,7 @@ class Reader { reader.navigate(location); } } - else if (openWindow) { + else if (openInWindow) { reader = new ReaderWindow({ sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, @@ -970,13 +993,17 @@ class Reader { if (!(await reader.open({ itemID, location }))) { return; } + Zotero.Session.debounceSave(); reader._window.addEventListener('unload', () => { this._readers.splice(this._readers.indexOf(reader), 1); + Zotero.Session.debounceSave(); }); } else { reader = new ReaderTab({ itemID, + title, + background: openInBackground, sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, bottomPlaceholderHeight: this._bottomPlaceholderHeight diff --git a/chrome/content/zotero/xpcom/session.js b/chrome/content/zotero/xpcom/session.js new file mode 100644 index 0000000000..da5ecf0392 --- /dev/null +++ b/chrome/content/zotero/xpcom/session.js @@ -0,0 +1,80 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://digitalscholar.org/ + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Session = new function () { + const SESSION_FILE_NAME = 'session.json'; + const DEBOUNCED_SAVING_DELAY = 5 * 60 * 1000; // 5 min + + let _state = { + windows: [] + }; + + Zotero.defineProperty(this, 'state', { + get: () => { + return _state; + } + }); + + this.init = async function () { + try { + let sessionFile = OS.Path.join(Zotero.Profile.dir, SESSION_FILE_NAME); + let state = await Zotero.File.getContentsAsync(sessionFile); + _state = JSON.parse(state); + } + catch (e) { + Zotero.logError(e); + } + }; + + this.setLastClosedZoteroPaneState = function (state) { + _state.windows = [state]; + }; + + this.debounceSave = Zotero.Utilities.debounce(() => { + this.save(); + }, DEBOUNCED_SAVING_DELAY); + + this.save = async function () { + try { + // Saving is triggered in `zotero.js` when a quit event is received, + // though if it was triggered by closing a window, ZoteroPane might + // be already destroyed at the time + let panes = Zotero.getZoteroPanes().map(x => x.getState()); + let readers = Zotero.Reader.getWindowStates(); + if (panes.length) { + _state.windows = [...readers, ...panes]; + } + else if (readers.length) { + _state.windows = _state.windows.filter(x => x.type != 'reader'); + _state.windows = [..._state.windows, ...readers]; + } + let sessionFile = OS.Path.join(Zotero.Profile.dir, SESSION_FILE_NAME); + await Zotero.File.putContentsAsync(sessionFile, JSON.stringify(_state)); + } + catch (e) { + Zotero.logError(e); + } + }; +}; diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index d0c08090e7..9e4c12a662 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -380,6 +380,12 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); // Make sure data directory isn't in Dropbox, etc. yield Zotero.DataDirectory.checkForUnsafeLocation(dataDir); + Services.obs.addObserver({ + observe: function () { + Zotero.Session.save(); + } + }, "quit-application-granted", false); + // Register shutdown handler to call Zotero.shutdown() var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }}; Services.obs.addObserver(_shutdownObserver, "quit-application", false); @@ -690,6 +696,8 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); yield Zotero.CharacterSets.init(); yield Zotero.RelationPredicates.init(); + yield Zotero.Session.init(); + Zotero.locked = false; // Initialize various services @@ -727,6 +735,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); yield Zotero.Retractions.init(); yield Zotero.NoteBackups.init(); yield Zotero.Dictionaries.init(); + Zotero.Reader.init(); // Migrate fields from Extra that can be moved to item fields after a schema update yield Zotero.Schema.migrateExtraFields(); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index c57a5a7aaf..399f8394ee 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -153,6 +153,7 @@ var ZoteroPane = new function() Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : ""; Zotero_Tabs.init(); + ZoteroContextPane.init(); await ZoteroPane.initCollectionsTree(); await ZoteroPane.initItemsTree(); @@ -248,6 +249,17 @@ var ZoteroPane = new function() var importer = new Zotero_Import_Mendeley(); importer.deleteNonPrimaryFiles(); }, 10000) + + // Restore pane state + try { + let state = Zotero.Session.state.windows.find(x => x.type == 'pane'); + if (state) { + Zotero_Tabs.restoreState(state.tabs); + } + } + catch (e) { + Zotero.logError(e); + } } @@ -355,6 +367,12 @@ var ZoteroPane = new function() observerService.removeObserver(_reloadObserver, "zotero-reloaded"); + ZoteroContextPane.destroy(); + + if (!Zotero.getZoteroPanes().length) { + Zotero.Session.setLastClosedZoteroPaneState(this.getState()); + } + Zotero_Tabs.closeAll(); } @@ -3801,7 +3819,7 @@ var ZoteroPane = new function() await Zotero.Reader.open( itemID, extraData && extraData.location, - originalEvent && originalEvent.shiftKey + { openInWindow: originalEvent && originalEvent.shiftKey } ); return; } @@ -4855,6 +4873,13 @@ var ZoteroPane = new function() } + this.getState = function () { + return { + type: 'pane', + tabs: Zotero_Tabs.getState() + }; + }; + /** * Unserializes zotero-persist elements from preferences */ diff --git a/components/zotero-service.js b/components/zotero-service.js index 6f3914de6a..6c39c5cb2b 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -120,6 +120,7 @@ const xpcomFilesLocal = [ 'router', 'schema', 'server', + 'session', 'streamer', 'style', 'sync',