Add split view for PDF reader (#2832)

This commit is contained in:
Martynas Bagdonas 2022-09-29 13:25:58 +03:00 committed by GitHub
parent a7e6fc330c
commit 6aad0cbb9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 26 deletions

View file

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

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

@ -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;
}
@ -458,7 +466,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.createElement('menupopup');
@ -515,7 +523,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);
@ -523,11 +532,10 @@ var Zotero_Tabs = new function () {
menuitem = document.createElement('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,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,<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.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) => {

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

@ -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

@ -1 +1 @@
Subproject commit 40f5d26b7cc754fb72ba10eff979560721d4fe0e
Subproject commit f8e560210017bc621056fcf654cf3dd7a657f55a