Add split view for PDF reader (#2832)

Fixes #2821
This commit is contained in:
Martynas Bagdonas 2022-09-29 13:25:58 +03:00
parent 03f05c8afa
commit 61c0dfee38
8 changed files with 160 additions and 28 deletions

View file

@ -220,6 +220,19 @@
label="&zotero.pdfReader.zoomPageHeight;"
oncommand="menuCmd('zoomPageHeight')"
/>
<menuseparator class="menu-type-reader"/>
<menuitem
id="view-menuitem-split-vertically"
type="checkbox"
label="&zotero.pdfReader.splitVertically;"
oncommand="menuCmd('splitVertically')"
/>
<menuitem
id="view-menuitem-split-horizontally"
type="checkbox"
label="&zotero.pdfReader.splitHorizontally;"
oncommand="menuCmd('splitHorizontally')"
/>
</menupopup>
</menu>
<menu

View file

@ -405,6 +405,8 @@ const ZoteroStandalone = new function() {
this.updateMenuItemCheckmark('view-menuitem-zoom-auto', reader.isZoomAutoActive());
this.updateMenuItemCheckmark('view-menuitem-zoom-page-width', reader.isZoomPageWidthActive());
this.updateMenuItemCheckmark('view-menuitem-zoom-page-height', reader.isZoomPageHeightActive());
this.updateMenuItemCheckmark('view-menuitem-split-vertically', reader.isSplitVerticallyActive());
this.updateMenuItemCheckmark('view-menuitem-split-horizontally', reader.isSplitHorizontallyActive());
}
// Layout mode

View file

@ -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(
<TabBar
@ -138,7 +144,8 @@ var Zotero_Tabs = new function () {
null,
{
title: tab.title,
openInBackground: !tab.selected
openInBackground: !tab.selected,
secondViewState: tab.data.secondViewState
}
);
}
@ -352,7 +359,8 @@ var Zotero_Tabs = new function () {
tabID: tab.id,
title: tab.title,
tabIndex,
allowDuplicate: true
allowDuplicate: true,
secondViewState: tab.data.secondViewState
});
return;
}
@ -461,7 +469,7 @@ var Zotero_Tabs = new function () {
};
this._openMenu = function (x, y, id) {
var { tabIndex } = this._getTab(id);
var { tab, tabIndex } = this._getTab(id);
window.Zotero_Tooltip.stop();
let menuitem;
let popup = document.createXULElement('menupopup');
@ -518,7 +526,8 @@ var Zotero_Tabs = new function () {
var reader = Zotero.Reader.getByTabID(id);
if (reader) {
this.close(id);
Zotero.Reader.open(reader.itemID, null, { openInWindow: true });
let { secondViewState } = tab.data;
Zotero.Reader.open(reader.itemID, null, { openInWindow: true, secondViewState });
}
});
menupopup.appendChild(menuitem);
@ -526,11 +535,10 @@ var Zotero_Tabs = new function () {
menuitem = document.createXULElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.duplicate'));
menuitem.addEventListener('command', () => {
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);

View file

@ -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,7 +264,15 @@ class ReaderInstance {
isZoomPageHeightActive() {
return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentScaleValue === 'page-fit';
}
isSplitVerticallyActive() {
return this._iframeWindow.wrappedJSObject.getSplitType() === 'vertical';
}
isSplitHorizontallyActive() {
return this._iframeWindow.wrappedJSObject.getSplitType() === 'horizontal';
}
allowNavigateFirstPage() {
return this._iframeWindow.wrappedJSObject.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',
@ -578,6 +598,26 @@ class ReaderInstance {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect shape-rendering="geometricPrecision" fill="${fill}" stroke-width="2" x="2" y="2" stroke="${stroke}" width="12" height="12" rx="3"/></svg>`;
}
_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.createXULElement('menupopup');
menupopup.addEventListener('popuphidden', function (event) {
@ -601,8 +641,8 @@ class ReaderInstance {
tagsbox.newTag();
}
}
_openPagePopup(data) {
_openPagePopup(data, secondView) {
let popup = this._window.document.createXULElement('menupopup');
this._popupset.appendChild(popup);
popup.addEventListener('popuphidden', function () {
@ -623,14 +663,14 @@ class ReaderInstance {
menuitem = this._window.document.createXULElement('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.createXULElement('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'
@ -639,7 +679,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'
@ -648,7 +688,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'
@ -657,17 +697,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.createXULElement('menuseparator'));
// Split Vertically
menuitem = this._window.document.createXULElement('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.createXULElement('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.createXULElement('menuseparator'));
// Next page
menuitem = this._window.document.createXULElement('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
@ -675,7 +731,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);
@ -899,25 +955,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();
@ -1004,7 +1087,7 @@ class ReaderInstance {
return;
}
case 'openPagePopup': {
this._openPagePopup(message.data);
this._openPagePopup(message.data, secondView);
return;
}
case 'openAnnotationPopup': {
@ -1308,6 +1391,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() {
@ -1367,7 +1452,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() {
@ -1490,7 +1575,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) {
@ -1499,7 +1589,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;
@ -1540,7 +1630,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();
@ -1561,7 +1651,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) => {

View file

@ -393,6 +393,21 @@
oncommand="ZoteroStandalone.onReaderCmd('zoomPageHeight')"
/>
<menuseparator class="menu-type-reader"/>
<menuitem
id="view-menuitem-split-vertically"
class="menu-type-reader"
type="checkbox"
label="&zotero.pdfReader.splitVertically;"
oncommand="ZoteroStandalone.onReaderCmd('splitVertically')"
/>
<menuitem
id="view-menuitem-split-horizontally"
class="menu-type-reader"
type="checkbox"
label="&zotero.pdfReader.splitHorizontally;"
oncommand="ZoteroStandalone.onReaderCmd('splitHorizontally')"
/>
<menuseparator class="menu-type-reader"/>
<menu id="layout-menu" label="&layout.label;">
<menupopup oncommand="ZoteroStandalone.onViewMenuItemClick(event)">

View file

@ -325,4 +325,6 @@
<!ENTITY zotero.pdfReader.zoomAuto "Automatically Resize">
<!ENTITY zotero.pdfReader.zoomPageWidth "Zoom to Page Width">
<!ENTITY zotero.pdfReader.zoomPageHeight "Zoom to Page Height">
<!ENTITY zotero.pdfReader.splitVertically "Split Vertically">
<!ENTITY zotero.pdfReader.splitHorizontally "Split Horizontally">
<!ENTITY zotero.pdfReader.transferFromPDF "Import Annotations…">

View file

@ -1368,6 +1368,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

@ -1 +1 @@
Subproject commit 40f5d26b7cc754fb72ba10eff979560721d4fe0e
Subproject commit 9587442831c46491c9a0a4066a37c0863d1c9efd