From 6aad0cbb9c7b9b486441646d9ab29e2c5bf67453 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Thu, 29 Sep 2022 13:25:58 +0300 Subject: [PATCH] Add split view for PDF reader (#2832) --- chrome/content/zotero/reader.xul | 13 ++ .../content/zotero/standalone/standalone.js | 2 + .../content/zotero/standalone/standalone.xul | 15 +++ chrome/content/zotero/tabs.js | 22 ++- chrome/content/zotero/xpcom/reader.js | 126 +++++++++++++++--- chrome/locale/en-US/zotero/zotero.dtd | 2 + chrome/locale/en-US/zotero/zotero.properties | 2 + pdf-reader | 2 +- 8 files changed, 158 insertions(+), 26 deletions(-) diff --git a/chrome/content/zotero/reader.xul b/chrome/content/zotero/reader.xul index 16ac27a6fa..67f6d4f50e 100644 --- a/chrome/content/zotero/reader.xul +++ b/chrome/content/zotero/reader.xul @@ -163,6 +163,19 @@ label="&zotero.pdfReader.zoomPageHeight;" oncommand="menuCmd('zoomPageHeight')" /> + + + + + + diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js index 3005a62196..c29366a88c 100644 --- a/chrome/content/zotero/tabs.js +++ b/chrome/content/zotero/tabs.js @@ -89,6 +89,12 @@ var Zotero_Tabs = new function () { return tab && tab.id; }; + this.setSecondViewState = function (tabID, state) { + let { tab } = this._getTab(tabID); + tab.data.secondViewState = state; + Zotero.Session.debounceSave(); + }; + this.init = function () { ReactDOM.render( { - var { tab, tabIndex } = this._getTab(id); if (tab.data.itemID) { tabIndex++; - Zotero.Reader.open(tab.data.itemID, null, { tabIndex, allowDuplicate: true }); - + let { secondViewState } = tab.data; + Zotero.Reader.open(tab.data.itemID, null, { tabIndex, allowDuplicate: true, secondViewState }); } }); popup.appendChild(menuitem); diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 78281d60a6..e13cd4b923 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -52,6 +52,11 @@ class ReaderInstance { } } + getSecondViewState() { + let state = this._iframeWindow.wrappedJSObject.getSecondViewState(); + return state ? JSON.parse(JSON.stringify(state)) : undefined; + } + async migrateMendeleyColors(libraryID, annotations) { let colorMap = new Map(); colorMap.set('#fff5ad', '#ffd400'); @@ -88,7 +93,7 @@ class ReaderInstance { return true; } - async open({ itemID, state, location }) { + async open({ itemID, state, location, secondViewState }) { let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); let library = Zotero.Libraries.get(libraryID); await library.waitForDataLoad('item'); @@ -122,6 +127,7 @@ class ReaderInstance { buf, annotations, state, + secondViewState, location, readOnly: this._isReadOnly(), authorName: item.library.libraryType === 'group' ? Zotero.Users.getCurrentName() : '', @@ -258,6 +264,14 @@ class ReaderInstance { isZoomPageHeightActive() { return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentScaleValue === "page-fit"'); } + + isSplitVerticallyActive() { + return this._iframeWindow.wrappedJSObject.getSplitType() === 'vertical'; + } + + isSplitHorizontallyActive() { + return this._iframeWindow.wrappedJSObject.getSplitType() === 'horizontal'; + } allowNavigateFirstPage() { return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentPageNumber > 1'); @@ -365,6 +379,12 @@ class ReaderInstance { } return; } + else if (cmd === 'splitVertically') { + this._splitVertically(); + } + else if (cmd === 'splitHorizontally') { + this._splitHorizontally(); + } let data = { action: 'menuCmd', @@ -580,6 +600,26 @@ class ReaderInstance { return `data:image/svg+xml,`; } + _splitVertically() { + if (this.isSplitVerticallyActive()) { + this._iframeWindow.wrappedJSObject.unsplitView(); + } + else { + this._iframeWindow.wrappedJSObject.splitView(); + } + setTimeout(() => this._updateSecondViewState(), 500); + } + + _splitHorizontally() { + if (this.isSplitHorizontallyActive()) { + this._iframeWindow.wrappedJSObject.unsplitView(); + } + else { + this._iframeWindow.wrappedJSObject.splitView(true); + } + setTimeout(() => this._updateSecondViewState(), 500); + } + _openTagsPopup(item, selector) { let menupopup = this._window.document.createElement('menupopup'); menupopup.className = 'tags-popup'; @@ -598,7 +638,7 @@ class ReaderInstance { } } - _openPagePopup(data) { + _openPagePopup(data, secondView) { let popup = this._window.document.createElement('menupopup'); this._popupset.appendChild(popup); popup.addEventListener('popuphidden', function () { @@ -619,14 +659,14 @@ class ReaderInstance { menuitem = this._window.document.createElement('menuitem'); menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomIn')); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomIn' }); + this._postMessage({ action: 'popupCmd', cmd: 'zoomIn' }, [], secondView); }); popup.appendChild(menuitem); // Zoom out menuitem = this._window.document.createElement('menuitem'); menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomOut')); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomOut' }); + this._postMessage({ action: 'popupCmd', cmd: 'zoomOut' }, [], secondView); }); popup.appendChild(menuitem); // Zoom 'Auto' @@ -635,7 +675,7 @@ class ReaderInstance { menuitem.setAttribute('type', 'checkbox'); menuitem.setAttribute('checked', data.isZoomAuto); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomAuto' }); + this._postMessage({ action: 'popupCmd', cmd: 'zoomAuto' }, [], secondView); }); popup.appendChild(menuitem); // Zoom 'Page Width' @@ -644,7 +684,7 @@ class ReaderInstance { menuitem.setAttribute('type', 'checkbox'); menuitem.setAttribute('checked', data.isZoomPageWidth); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomPageWidth' }); + this._postMessage({ action: 'popupCmd', cmd: 'zoomPageWidth' }, [], secondView); }); popup.appendChild(menuitem); // Zoom 'Page Height' @@ -653,17 +693,33 @@ class ReaderInstance { menuitem.setAttribute('type', 'checkbox'); menuitem.setAttribute('checked', data.isZoomPageHeight); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomPageHeight' }); + this._postMessage({ action: 'popupCmd', cmd: 'zoomPageHeight' }, [], secondView); }); popup.appendChild(menuitem); // Separator popup.appendChild(this._window.document.createElement('menuseparator')); + // Split Vertically + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('pdfReader.splitVertically')); + menuitem.setAttribute('type', 'checkbox'); + menuitem.setAttribute('checked', this.isSplitVerticallyActive()); + menuitem.addEventListener('command', () => this._splitVertically()); + popup.appendChild(menuitem); + // Split Horizontally + menuitem = this._window.document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('pdfReader.splitHorizontally')); + menuitem.setAttribute('type', 'checkbox'); + menuitem.setAttribute('checked', this.isSplitHorizontallyActive()); + menuitem.addEventListener('command', () => this._splitHorizontally()); + popup.appendChild(menuitem); + // Separator + popup.appendChild(this._window.document.createElement('menuseparator')); // Next page menuitem = this._window.document.createElement('menuitem'); menuitem.setAttribute('label', Zotero.getString('pdfReader.nextPage')); menuitem.setAttribute('disabled', !data.enableNextPage); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'nextPage' }); + this._postMessage({ action: 'popupCmd', cmd: 'nextPage' }, [], secondView); }); popup.appendChild(menuitem); // Previous page @@ -671,7 +727,7 @@ class ReaderInstance { menuitem.setAttribute('label', Zotero.getString('pdfReader.previousPage')); menuitem.setAttribute('disabled', !data.enablePrevPage); menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'prevPage' }); + this._postMessage({ action: 'popupCmd', cmd: 'prevPage' }, [], secondView); }); popup.appendChild(menuitem); popup.openPopupAtScreen(data.x, data.y, true); @@ -893,25 +949,52 @@ class ReaderInstance { popup.openPopupAtScreen(data.x, data.y, true); } - async _postMessage(message, transfer) { + async _postMessage(message, transfer, secondView) { await this._waitForReader(); - this._iframeWindow.postMessage({ itemID: this._itemID, message }, this._iframeWindow.origin, transfer); + this._iframeWindow.postMessage({ itemID: this._itemID, message, secondView }, this._iframeWindow.origin, transfer); + } + + _updateSecondViewState() { + if (this.tabID) { + let win = Zotero.getMainWindow(); + if (win) { + win.Zotero_Tabs.setSecondViewState(this.tabID, this.getSecondViewState()); + } + } } _handleMessage = async (event) => { let message; + let secondViewIframeWindow = this._iframeWindow.document.getElementById('secondViewIframe'); + if (secondViewIframeWindow) { + secondViewIframeWindow = secondViewIframeWindow.contentWindow; + } try { - if (event.source !== this._iframeWindow) { + if (event.source !== this._iframeWindow + && event.source !== secondViewIframeWindow) { return; } // Clone data to avoid the dead object error when the window is closed let data = JSON.parse(JSON.stringify(event.data)); + let { secondView } = data; // Filter messages coming from previous reader instances, // except for `setAnnotation` to still allow saving it if (data.itemID !== this._itemID && data.message.action !== 'setAnnotation') { return; } message = data.message; + + if (secondView) { + switch (message.action) { + case 'openPagePopup': break; + case 'setState': { + this._updateSecondViewState(); + return; + } + default: return; + } + } + switch (message.action) { case 'initialized': { this._resolveInitPromise(); @@ -998,7 +1081,7 @@ class ReaderInstance { return; } case 'openPagePopup': { - this._openPagePopup(message.data); + this._openPagePopup(message.data, secondView); return; } case 'openAnnotationPopup': { @@ -1302,6 +1385,8 @@ class ReaderWindow extends ReaderInstance { this._window.document.getElementById('view-menuitem-zoom-auto').setAttribute('checked', this.isZoomAutoActive()); this._window.document.getElementById('view-menuitem-zoom-page-width').setAttribute('checked', this.isZoomPageWidthActive()); this._window.document.getElementById('view-menuitem-zoom-page-height').setAttribute('checked', this.isZoomPageHeightActive()); + this._window.document.getElementById('view-menuitem-split-vertically').setAttribute('checked', this.isSplitVerticallyActive()); + this._window.document.getElementById('view-menuitem-split-horizontally').setAttribute('checked', this.isSplitHorizontallyActive()); } _onGoMenuOpen() { @@ -1361,7 +1446,7 @@ class Reader { 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 })); + .forEach(x => this.open(x.itemID, null, { title: x.title, openInWindow: true, secondViewState: x.secondViewState })); } _loadSidebarState() { @@ -1484,7 +1569,12 @@ class Reader { getWindowStates() { return this._readers .filter(r => r instanceof ReaderWindow) - .map(r => ({ type: 'reader', itemID: r._itemID, title: r._title })); + .map(r => ({ + type: 'reader', + itemID: r._itemID, + title: r._title, + secondViewState: r.getSecondViewState() + })); } async openURI(itemURI, location, options) { @@ -1493,7 +1583,7 @@ class Reader { await this.open(item.id, location, options); } - async open(itemID, location, { title, tabIndex, tabID, openInBackground, openInWindow, allowDuplicate } = {}) { + async open(itemID, location, { title, tabIndex, tabID, openInBackground, openInWindow, allowDuplicate, secondViewState } = {}) { this._loadSidebarState(); this.triggerAnnotationsImportCheck(itemID); let reader; @@ -1534,7 +1624,7 @@ class Reader { bottomPlaceholderHeight: this._bottomPlaceholderHeight }); this._readers.push(reader); - if (!(await reader.open({ itemID, location }))) { + if (!(await reader.open({ itemID, location, secondViewState }))) { return; } Zotero.Session.debounceSave(); @@ -1555,7 +1645,7 @@ class Reader { bottomPlaceholderHeight: this._bottomPlaceholderHeight }); this._readers.push(reader); - if (!(await reader.open({ itemID, location }))) { + if (!(await reader.open({ itemID, location, secondViewState }))) { return; } reader.onChangeSidebarWidth = (width) => { diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index 2d9f3bd24a..723a2724ae 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -325,4 +325,6 @@ + + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index b23cb46e2c..fdfcedde37 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -1372,6 +1372,8 @@ pdfReader.zoomOut = Zoom Out pdfReader.zoomAuto = Automatically Resize pdfReader.zoomPageWidth = Zoom to Page Width pdfReader.zoomPageHeight = Zoom to Page Height +pdfReader.splitVertically = Split Vertically +pdfReader.splitHorizontally = Split Horizontally pdfReader.nextPage = Next Page pdfReader.previousPage = Previous Page pdfReader.page = Page diff --git a/pdf-reader b/pdf-reader index 40f5d26b7c..f8e5602100 160000 --- a/pdf-reader +++ b/pdf-reader @@ -1 +1 @@ -Subproject commit 40f5d26b7cc754fb72ba10eff979560721d4fe0e +Subproject commit f8e560210017bc621056fcf654cf3dd7a657f55a