diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx index 593e0f3289..31db8b197f 100644 --- a/chrome/content/zotero/components/tabBar.jsx +++ b/chrome/content/zotero/components/tabBar.jsx @@ -23,126 +23,107 @@ ***** END LICENSE BLOCK ***** */ -"use strict"; +'use strict'; -import React, { forwardRef, useState, useImperativeHandle, useEffect } from 'react'; +import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect } from 'react'; import cx from 'classnames'; const TabBar = forwardRef(function (props, ref) { - const [tabs, setTabs] = useState(props.initialTabs); - const [selectedIndex, setSelectedIndex] = useState(0); - - useImperativeHandle(ref, () => ({ - get selectedIndex() { - return selectedIndex; - }, - - selectLeft() { - var newIndex = selectedIndex - 1; - if (newIndex < 0) { - newIndex = tabs.length + newIndex; - } - setSelectedIndex(newIndex); - }, - - selectRight() { - var newIndex = selectedIndex + 1; - if (newIndex >= tabs.length) { - newIndex = newIndex - tabs.length; - } - setSelectedIndex(newIndex); - }, - - select(index) { - if (index > tabs.length - 1) { - throw new Error("Tab index out of bounds"); - } - setSelectedIndex(index); - }, - - add({ title, type }) { - var newTabs = [...tabs]; - newTabs.push({ title, type }); - setTabs(newTabs); - setSelectedIndex(newTabs.length - 1); - }, - - rename(title, index) { - var newTabs = tabs.map((tab, currentIndex) => { - let newTab = Object.assign({}, tab); - if (index == currentIndex) { - newTab.title = title; - } - return newTab; - }); - setTabs(newTabs); - }, - - close(index) { - if (index == 0) { - return; - } - removeTab(index); - } - })); - + const [tabs, setTabs] = useState([]); + const draggingID = useRef(null); + const tabsHolderRef = useRef(); + const mouseMoveWaitUntil = useRef(0); + useEffect(() => { - if (props.onTabSelected) { - props.onTabSelected(selectedIndex); + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('mouseup', handleMouseUp); + }; + }, []); + + useImperativeHandle(ref, () => ({ setTabs })); + + function handleMouseDown(event, id, index) { + if (index != 0) { + draggingID.current = id; } - }, [selectedIndex]); - - - function removeTab(index) { - var newTabs = [...tabs]; - newTabs.splice(index, 1); - setTabs(newTabs); - setSelectedIndex(Math.min(selectedIndex, newTabs.length - 1)); - if (props.onTabClosed) { - props.onTabClosed(index); - } - } - - function handleTabClick(event, index) { - setSelectedIndex(index); + props.onTabSelect(id); event.stopPropagation(); } - - function handleTabClose(event, index) { - removeTab(index); - if (props.onTabClose) { - props.onTabClose(index); + + function handleMouseMove(event) { + if (!draggingID.current || mouseMoveWaitUntil.current > Date.now()) { + return; } + let points = Array.from(tabsHolderRef.current.children).map((child) => { + let rect = child.getBoundingClientRect(); + return rect.left + rect.width / 2; + }); + let index = null; + for (let i = 0; i < points.length - 1; i++) { + let point1 = points[i]; + let point2 = points[i + 1]; + if (event.clientX > Math.min(point1, point2) + && event.clientX < Math.max(point1, point2)) { + index = i + 1; + break; + } + } + if (index === null) { + let point1 = points[0]; + let point2 = points[points.length - 1]; + if ((point1 < point2 && event.clientX < point1 + || point1 > point2 && event.clientX > point1)) { + index = 0; + } + else { + index = points.length; + } + } + if (index == 0) { + index = 1; + } + props.onTabMove(draggingID.current, index); + mouseMoveWaitUntil.current = Date.now() + 100; + } + + function handleMouseUp(event) { + draggingID.current = null; event.stopPropagation(); } - - function renderTab(tab, index) { + + function handleTabClose(event, id) { + props.onTabClose(id); + event.stopPropagation(); + } + + function renderTab({ id, title, selected }, index) { return (
handleTabClick(event, index)} + key={id} + className={cx('tab', { selected })} + onMouseDown={(event) => handleMouseDown(event, id, index)} > -
{tab.title}
+
{title}
handleTabClose(event, index)} + onClick={(event) => handleTabClose(event, id)} > x
); } - + return ( -
+
-
- { tabs.map((tab, index) => renderTab(tab, index)) } +
+ {tabs.map((tab, index) => renderTab(tab, index))}
-
+
); }); -export default TabBar; \ No newline at end of file +export default TabBar; diff --git a/chrome/content/zotero/reader.xul b/chrome/content/zotero/reader.xul index 33c14a78a6..fd166d36d0 100644 --- a/chrome/content/zotero/reader.xul +++ b/chrome/content/zotero/reader.xul @@ -23,6 +23,7 @@ Components.utils.import('resource://gre/modules/Services.jsm'); + @@ -62,22 +63,13 @@ - - - - - - - + this._selectedIndex + Object.defineProperty(this, 'selectedID', { + get: () => this._selectedID }); - + + Object.defineProperty(this, 'selectedIndex', { + get: () => this._getTab(this._selectedID).tabIndex + }); + Object.defineProperty(this, 'deck', { get: () => document.getElementById('tabs-deck') }); - - this._tabBarRef = {}; - this._tabs = [ - { - title: "", - type: 'library' - } - ]; - this._selectedIndex = 0; - + + this._tabBarRef = React.createRef(); + this._tabs = [{ + id: 'zotero-pane', + type: 'library', + title: '' + }]; + this._selectedID = 'zotero-pane'; + + this._getTab = function (id) { + var tabIndex = this._tabs.findIndex(tab => tab.id == id); + return { tab: this._tabs[tabIndex], tabIndex }; + }; + + this._update = function () { + this._tabBarRef.current.setTabs(this._tabs.map(tab => ({ + id: tab.id, + type: tab.type, + title: tab.title, + selected: tab.id == this._selectedID + }))); + }; + this.init = function () { ReactDOM.render( , - document.getElementById('tab-bar-container') + document.getElementById('tab-bar-container'), + () => { + this._update(); + } ); - }; - - - this.selectLeft = function () { - this._tabBarRef.current.selectLeft(); - }; - - - this.selectRight = function () { - this._tabBarRef.current.selectRight(); - }; - - - this.select = function (index) { - this._tabBarRef.current.select(index); - }, - - + /** - * @return {Element} - The element created in the deck + * Add a new tab + * + * @param {String} type + * @param {String} title + * @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 ({ title, type, url, index }) { - this._tabBarRef.current.add({ title, type }); - - var elem; - if (url) { - elem = document.createElement('iframe'); - elem.setAttribute('type', 'content'); - elem.setAttribute('src', url); + this.add = function ({ type, title, index, select, onClose }) { + if (typeof type != 'string') { + throw new Error(`'type' should be a string (was ${typeof type})`); } - else { - elem = document.createElementNS(HTML_NS, 'div'); - elem.textContent = title; + if (typeof title != 'string') { + throw new Error(`'title' should be a string (was ${typeof title})`); } - - var deck = this.deck; - deck.insertBefore(elem, index === undefined ? null : deck.childNodes[index]); - - return elem; - }; - - - this.rename = function (title, index) { - if (index === undefined) { - index = this._selectedIndex; + if (index !== undefined && (!Number.isInteger(index) || index < 1)) { + throw new Error(`'index' should be an integer > 0 (was ${index} (${typeof index})`); } - this._tabs[index].title = title; - this._tabBarRef.current.rename(title, index); - }; - - - this.close = function (index) { - if (index === undefined) { - index = this._selectedIndex; + if (onClose !== undefined && typeof onClose != 'function') { + throw new Error(`'onClose' should be a function (was ${typeof onClose})`); } - this._tabBarRef.current.close(index); + var id = 'tab-' + Zotero.Utilities.randomString(); + var container = document.createElement('vbox'); + container.id = id; + this.deck.appendChild(container); + var tab = { id, type, title, onClose }; + index = index || this._tabs.length; + this._tabs.splice(index, 0, tab); + this._update(); + if (select) { + this.select(id); + } + return { id, container }; }; - - - this._onTabSelected = function (index) { - this._selectedIndex = index; - this.deck.selectedIndex = index; + + /** + * Set a new tab title + * + * @param {String} id + * @param {String} title + */ + this.rename = function (id, title) { + if (typeof title != 'string') { + throw new Error(`'title' should be a string (was ${typeof title})`); + } + var { tab } = this._getTab(id); + if (!tab) { + return; + } + tab.title = title; + this._update(); }; - - - this._onTabClosed = function (index) { - this._tabs.splice(index, 1); - var deck = this.deck; - deck.removeChild(deck.childNodes[index]); + + /** + * Close a tab + * + * @param {String} id + */ + this.close = function (id) { + var { tab, tabIndex } = this._getTab(id || this._selectedID); + if (tabIndex == 0) { + throw new Error('Library tab cannot be closed'); + } + if (!tab) { + return; + } + this.select((this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id); + this._tabs.splice(tabIndex, 1); + document.getElementById(tab.id).remove(); + if (tab.onClose) { + tab.onClose(); + } + this._update(); }; -}; \ No newline at end of file + + /** + * Move a tab to the specified index + * + * @param {String} id + * @param {Integer} newIndex + */ + this.move = function (id, newIndex) { + if (!Number.isInteger(newIndex) || newIndex < 1) { + throw new Error(`'newIndex' should be an interger > 0 (was ${newIndex} (${typeof newIndex})`); + } + var { tab, tabIndex } = this._getTab(id); + if (tabIndex == 0) { + throw new Error('Library tab cannot be moved'); + } + if (!tab || tabIndex == newIndex) { + return; + } + if (newIndex > tabIndex) { + newIndex--; + } + this._tabs.splice(tabIndex, 1); + this._tabs.splice(newIndex, 0, tab); + this._update(); + }; + + /** + * Select a tab + * + * @param {String} id + */ + this.select = function (id) { + var { tab } = this._getTab(id); + if (!tab) { + return; + } + this._selectedID = id; + this.deck.selectedIndex = Array.from(this.deck.children).findIndex(x => x.id == id); + this._update(); + }; + + /** + * Select the previous tab (closer to the library tab) + */ + this.selectPrev = function () { + var { tabIndex } = this._getTab(this._selectedID); + this.select((this._tabs[tabIndex - 1] || this._tabs[this._tabs.length - 1]).id); + }; + + /** + * Select the next tab (farther to the library tab) + */ + this.selectNext = function () { + var { tabIndex } = this._getTab(this._selectedID); + this.select((this._tabs[tabIndex + 1] || this._tabs[0]).id); + }; +}; diff --git a/chrome/content/zotero/xpcom/pdfWorker/manager.js b/chrome/content/zotero/xpcom/pdfWorker/manager.js index fa7b6c1d33..6e1535963a 100644 --- a/chrome/content/zotero/xpcom/pdfWorker/manager.js +++ b/chrome/content/zotero/xpcom/pdfWorker/manager.js @@ -249,9 +249,9 @@ class PDFWorker { else { Zotero.PDF.hasUnmachedAnnotations[itemID] = !!annotations.length; } - for (let readerWindow of Zotero.Reader._readerWindows) { - if (readerWindow._itemID === itemID) { - readerWindow.toggleImportPrompt(!!Zotero.PDF.hasUnmachedAnnotations[itemID]); + for (let reader of Zotero.Reader._readers) { + if (reader._itemID === itemID) { + reader.toggleImportPrompt(!!Zotero.PDF.hasUnmachedAnnotations[itemID]); } } Zotero.PDF.dateChecked[itemID] = Zotero.Date.dateToISO(new Date()); diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 887c0395de..c4f8e37df5 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -4,7 +4,7 @@ Zotero.PDF = { hasUnmachedAnnotations: {} }; -class ReaderWindow { +class ReaderInstance { constructor() { this.pdfStateFileName = '.zotero-pdf-state'; this.annotationItemIDs = []; @@ -18,66 +18,6 @@ class ReaderWindow { this._isReaderInitialized = false; } - init() { - let win = Services.wm.getMostRecentWindow('navigator:browser'); - if (!win) return; - - this._window = win.open( - 'chrome://zotero/content/reader.xul', '', 'chrome,resizable' - ); - - this._window.addEventListener('DOMContentLoaded', (event) => { - this._window.addEventListener('dragover', this._handleDragOver, true); - this._window.addEventListener('drop', this._handleDrop, true); - this._window.addEventListener('keypress', this._handleKeyPress); - - this._window.fillTooltip = (tooltip) => { - let node = this._window.document.tooltipNode.closest('*[title]'); - if (!node) { - return false; - } - tooltip.setAttribute('label', node.getAttribute('title')); - return true; - }; - - this._window.menuCmd = (cmd) => { - if (cmd === 'export') { - let zp = Zotero.getActiveZoteroPane(); - zp.exportPDF(this._itemID); - return; - } - let data = { - action: 'menuCmd', - cmd - }; - this._postMessage(data); - }; - - let readerIframe = this._window.document.getElementById('reader'); - if (!(readerIframe && readerIframe.contentWindow && readerIframe.contentWindow.document === event.target)) { - return; - } - let editor = this._window.document.getElementById('zotero-reader-editor'); - editor.navigateHandler = async (uri, location) => { - let item = await Zotero.URI.getURIItem(uri); - if (!item) { - return; - } - if (item.id === this._itemID) { - this.navigate(location); - } - else { - await this.open({ - itemID: item.id, - location - }); - } - }; - this._iframeWindow = this._window.document.getElementById('reader').contentWindow; - this._iframeWindow.addEventListener('message', this._handleMessage); - }); - } - async open({ itemID, state, location, skipHistory }) { if (itemID === this._itemID) { return false; @@ -86,9 +26,6 @@ class ReaderWindow { if (!item) { return false; } - let path = await item.getFilePathAsync(); - let buf = await OS.File.read(path, {}); - buf = new Uint8Array(buf).buffer; if (this._itemID && !skipHistory) { this._prevHistory.push({ itemID: this._itemID, @@ -97,15 +34,10 @@ class ReaderWindow { this._nextHistory = []; } this._itemID = item.id; - let title = item.getField('title'); - let parentItemID = item.parentItemID; - if (parentItemID) { - let parentItem = await Zotero.Items.getAsync(parentItemID); - if (parentItem) { - title = parentItem.getField('title'); - } - } - this._window.document.title = title; + this.updateTitle(); + let path = await item.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; // TODO: Remove when fixed item._loaded.childItems = true; let ids = item.getAnnotations(); @@ -125,7 +57,7 @@ class ReaderWindow { }, [buf]); return true; } - + updateTitle() { let item = Zotero.Items.get(this._itemID); let title = item.getField('title'); @@ -136,7 +68,7 @@ class ReaderWindow { title = parentItem.getField('title'); } } - this._window.document.title = title; + this._setTitleValue(title); } async setAnnotations(ids) { @@ -166,17 +98,13 @@ class ReaderWindow { this._postMessage({ action: 'toggleImportPrompt', enable }); } - close() { - this._window.close(); - } - async _saveState(state) { let item = Zotero.Items.get(this._itemID); let file = Zotero.Attachments.getStorageDirectory(item); file.append(this.pdfStateFileName); await Zotero.File.putContentsAsync(file, JSON.stringify(state)); } - + async _loadState() { // TODO: Validate data to make sure the older format doesn't crash the future pdf-reader let item = Zotero.Items.get(this._itemID); @@ -188,7 +116,8 @@ class ReaderWindow { let state = JSON.parse(await Zotero.File.getContentsAsync(file)); return state; } - } catch (e) { + } + catch (e) { Zotero.logError(e); } return null; @@ -228,12 +157,25 @@ class ReaderWindow { return `data:image/svg+xml,`; } + _openTagsPopup(x, y, item) { + let menupopup = this._window.document.createElement('menupopup'); + menupopup.style.minWidth = '300px'; + menupopup.setAttribute('ignorekeys', true); + let tagsbox = this._window.document.createElement('tagsbox'); + menupopup.appendChild(tagsbox); + tagsbox.setAttribute('flex', '1'); + this._popupset.appendChild(menupopup); + menupopup.openPopupAtScreen(x, y, false); + tagsbox.mode = 'edit'; + tagsbox.item = item; + } + _openAnnotationPopup(x, y, annotationId, colors, selectedColor) { - let popup = this._window.document.getElementById('annotationPopup'); - popup.hidePopup(); - while (popup.firstChild) { - popup.removeChild(popup.firstChild); - } + let popup = this._window.document.createElement('menupopup'); + this._popupset.appendChild(popup); + popup.addEventListener('popuphidden', function () { + popup.remove(); + }); let menuitem = this._window.document.createElement('menuitem'); menuitem.setAttribute('label', 'Delete'); menuitem.addEventListener('command', () => { @@ -266,11 +208,11 @@ class ReaderWindow { } _openColorPopup(x, y, colors, selectedColor) { - let popup = this._window.document.getElementById('colorPopup'); - popup.hidePopup(); - while (popup.firstChild) { - popup.removeChild(popup.firstChild); - } + let popup = this._window.document.createElement('menupopup'); + this._popupset.appendChild(popup); + popup.addEventListener('popuphidden', function () { + popup.remove(); + }); let menuitem; for (let color of colors) { menuitem = this._window.document.createElement('menuitem'); @@ -389,8 +331,7 @@ class ReaderWindow { let libraryID = attachment.libraryID; let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key); if (annotation) { - this._window.document.getElementById('tags').item = annotation; - this._window.document.getElementById('tagsPopup').openPopupAtScreen(x, y, false); + this._openTagsPopup(x, y, annotation); } return; } @@ -449,6 +390,183 @@ class ReaderWindow { } } + + async _waitForReader() { + if (this._isReaderInitialized) { + return; + } + let n = 0; + while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) { + if (n >= 500) { + throw new Error('Waiting for reader failed'); + } + await Zotero.Promise.delay(10); + n++; + } + this._isReaderInitialized = true; + } + + /** + * Return item JSON in the pdf-reader ready format + * @param itemID + * @returns {Object|null} + */ + async _getAnnotation(itemID) { + try { + let item = Zotero.Items.get(itemID); + if (!item || !item.isAnnotation()) { + return null; + } + // TODO: Remve when fixed + item._loaded.childItems = true; + item = await Zotero.Annotations.toJSON(item); + item.id = item.key; + item.image = item.image; + delete item.key; + for (let key in item) { + item[key] = item[key] || ''; + } + item.tags = item.tags || []; + return item; + } + catch (e) { + Zotero.logError(e); + return null; + } + } +} + +class ReaderTab extends ReaderInstance { + constructor() { + super(); + this._window = Services.wm.getMostRecentWindow('navigator:browser'); + let { id, container } = this._window.Zotero_Tabs.add({ + type: 'reader', + title: '', + select: true, + onClose: () => { + this.tabID = null; + this.close(); + } + }); + this.tabID = id; + this._tabContainer = container; + + this._iframe = this._window.document.createElement('iframe'); + this._iframe.setAttribute('flex', '1'); + this._iframe.setAttribute('type', 'content'); + this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/viewer.html'); + this._tabContainer.appendChild(this._iframe); + + this._popupset = this._window.document.createElement('popupset'); + this._tabContainer.appendChild(this._popupset); + + this._window.addEventListener('DOMContentLoaded', (event) => { + if (this._iframe && this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) { + this._iframeWindow = this._iframe.contentWindow; + this._iframeWindow.addEventListener('message', this._handleMessage); + } + }); + + this._iframe.setAttribute('tooltip', 'iframeTooltip'); + } + + close() { + if (this.onClose) { + this.onClose(); + } + + if (this.tabID) { + this._window.Zotero_Tabs.close(this.tabID); + } + } + + _setTitleValue(title) { + this._window.Zotero_Tabs.rename(this.tabID, title); + } +} + + +class ReaderWindow extends ReaderInstance { + constructor() { + super(); + + this.init(); + } + + init() { + let win = Services.wm.getMostRecentWindow('navigator:browser'); + if (!win) return; + + this._window = win.open( + 'chrome://zotero/content/reader.xul', '', 'chrome,resizable' + ); + + this._window.addEventListener('DOMContentLoaded', (event) => { + if (event.target === this._window.document) { + this._window.addEventListener('dragover', this._handleDragOver, true); + this._window.addEventListener('drop', this._handleDrop, true); + this._window.addEventListener('keypress', this._handleKeyPress); + + this._popupset = this._window.document.getElementById('zotero-reader-popupset'); + + this._window.menuCmd = (cmd) => { + if (cmd === 'export') { + let zp = Zotero.getActiveZoteroPane(); + zp.exportPDF(this._itemID); + return; + } + let data = { + action: 'menuCmd', + cmd + }; + this._postMessage(data); + }; + + let editor = this._window.document.getElementById('zotero-reader-editor'); + editor.navigateHandler = async (uri, location) => { + let item = await Zotero.URI.getURIItem(uri); + if (!item) { + return; + } + if (item.id === this._itemID) { + this.navigate(location); + } + else { + await this.open({ + itemID: item.id, + location + }); + } + }; + + this._iframe = this._window.document.getElementById('reader'); + } + + if (this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) { + this._iframeWindow = this._window.document.getElementById('reader').contentWindow; + this._iframeWindow.addEventListener('message', this._handleMessage); + } + }); + } + + close() { + this._window.close(); + } + + updateTitle() { + let item = Zotero.Items.get(this._itemID); + let title = item.getField('title'); + let parentItemID = item.parentItemID; + if (parentItemID) { + let parentItem = Zotero.Items.get(parentItemID); + if (parentItem) { + title = parentItem.getField('title'); + } + } + this._window.document.title = title; + } + _handleDragOver = (event) => { if (event.dataTransfer.getData('zotero/item')) { event.preventDefault(); @@ -512,60 +630,18 @@ class ReaderWindow { } } - async _waitForReader() { - if (this._isReaderInitialized) { - return; - } - let n = 0; - while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) { - if (n >= 500) { - throw new Error('Waiting for reader failed'); - } - await Zotero.Promise.delay(10); - n++; - } - this._isReaderInitialized = true; - } - - /** - * Return item JSON in the pdf-reader ready format - * @param itemID - * @returns {Object|null} - */ - async _getAnnotation(itemID) { - try { - let item = Zotero.Items.get(itemID); - if (!item || !item.isAnnotation()) { - return null; - } - // TODO: Remve when fixed - item._loaded.childItems = true; - item = await Zotero.Annotations.toJSON(item); - item.id = item.key; - item.image = item.image; - delete item.key; - for (let key in item) { - item[key] = item[key] || ''; - } - item.tags = item.tags || []; - return item; - } - catch (e) { - Zotero.logError(e); - return null; - } - } } + class Reader { constructor() { - this._readerWindows = []; + this._readers = []; this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader'); } notify(event, type, ids, extraData) { // Listen for the parent item, PDF attachment and its annotation items updates - for (let readerWindow of this._readerWindows) { + for (let readerWindow of this._readers) { if (event === 'delete') { let disappearedIds = readerWindow.annotationItemIDs.filter(x => ids.includes(x)); if (disappearedIds.length) { @@ -608,36 +684,55 @@ class Reader { } } - _getReaderWindow(itemID) { - return this._readerWindows.find(v => v._itemID === itemID); - } - - async openURI(itemURI, location) { + async openURI(itemURI, location, openWindow) { let item = await Zotero.URI.getURIItem(itemURI); if (!item) return; - this.open(item.id, location); + await this.open(item.id, location, openWindow); } - async open(itemID, location) { + async open(itemID, location, openWindow) { this.triggerAnnotationsImportCheck(itemID); - let reader = this._getReaderWindow(itemID); + let reader; + + if (openWindow) { + reader = this._readers.find(r => r._itemID === itemID && (r instanceof ReaderWindow)); + } + else { + reader = this._readers.find(r => r._itemID === itemID); + } + if (reader) { if (location) { reader.navigate(location); } } - else { + else if (openWindow) { reader = new ReaderWindow(); - reader.init(); if (!(await reader.open({ itemID, location }))) { return; } - this._readerWindows.push(reader); + this._readers.push(reader); reader._window.addEventListener('unload', () => { - this._readerWindows.splice(this._readerWindows.indexOf(reader), 1); + this._readers.splice(this._readers.indexOf(reader), 1); }); } - reader._window.focus(); + else { + reader = new ReaderTab(); + if (!(await reader.open({ itemID, location }))) { + return; + } + this._readers.push(reader); + reader.onClose = () => { + this._readers.splice(this._readers.indexOf(reader), 1); + }; + } + + if (reader instanceof ReaderWindow) { + reader._window.focus(); + } + else { + reader._window.Zotero_Tabs.select(reader.tabID); + } } async triggerAnnotationsImportCheck(itemID) { diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index ee9d1481c0..b7af1d897c 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -526,6 +526,21 @@ var ZoteroPane = new function() * Trigger actions based on keyboard shortcuts */ function handleKeyDown(event, from) { + // Close current tab + if (event.key == 'w') { + let close = Zotero.isMac + ? (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey) + : (event.ctrlKey && !event.shiftKey && !event.altKey); + if (close) { + if (Zotero_Tabs.selectedIndex > 0) { + Zotero_Tabs.close(); + event.preventDefault(); + event.stopPropagation(); + } + return; + } + } + try { // Ignore keystrokes outside of Zotero pane if (!(event.originalTarget.ownerDocument instanceof XULDocument)) { @@ -547,30 +562,15 @@ var ZoteroPane = new function() let ctrlOnly = event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey; if (ctrlOnly) { if (event.key == 'PageUp') { - Zotero_Tabs.selectLeft(); + Zotero_Tabs.selectPrev(); } else if (event.key == 'PageDown') { - Zotero_Tabs.selectRight(); + Zotero_Tabs.selectNext(); } event.preventDefault(); return; } - // Close current tab - if (event.key == 'w') { - let close = Zotero.isMac - ? (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey) - : (event.ctrlKey && !event.shiftKey && !event.altKey); - if (close) { - if (Zotero_Tabs.selectedIndex > 0) { - Zotero_Tabs.close(); - event.preventDefault(); - event.stopPropagation(); - } - return; - } - } - // Highlight collections containing selected items // // We use Control (17) on Windows because Alt triggers the menubar; @@ -1197,8 +1197,7 @@ var ZoteroPane = new function() } // Rename tab - // TODO: What if PDF is in front? - Zotero_Tabs.rename(collectionTreeRow.getName()); + Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName()); // Clear quick search and tag selector when switching views document.getElementById('zotero-tb-search').value = ""; @@ -4078,8 +4077,7 @@ var ZoteroPane = new function() var launchFile = async (path, contentType, itemID) => { // Custom PDF handler if (contentType === 'application/pdf') { - this.viewPDF(itemID); - // TODO: Still leave an option to use an external PDF viewer + this.viewPDF(itemID, event.shiftKey); return; let pdfHandler = Zotero.Prefs.get("fileHandler.pdf"); if (pdfHandler) { @@ -4246,8 +4244,8 @@ var ZoteroPane = new function() } }); - this.viewPDF = function (itemID) { - Zotero.Reader.open(itemID); + this.viewPDF = function (itemID, openWindow) { + Zotero.Reader.open(itemID, null, openWindow); }; diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index d84fbd6e44..e6c9d11007 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -231,6 +231,8 @@ + + diff --git a/scss/components/_tabBar.scss b/scss/components/_tabBar.scss index 7d4c3fd267..8c7be5801b 100644 --- a/scss/components/_tabBar.scss +++ b/scss/components/_tabBar.scss @@ -12,16 +12,16 @@ .tab { -moz-appearance: none; - width: 200px; + max-width: 200px; + flex: 1; height: 30px; line-height: 30px; - display: flex; - justify-content: center; - align-content: center; position: relative; background: #f9f9f9; border-top: 2px solid transparent; color: #000; + text-align: center; + padding: 0 30px; &:not(:last-child) { border-right: $tab-border; @@ -32,8 +32,12 @@ } .tab-name { - margin-top: -2px; line-height: 30px; + overflow-x: hidden; + text-overflow: ellipsis; + display: -moz-box; + position: relative; + top: -2px; } .tab-close { diff --git a/scss/linux/_tabBar.scss b/scss/linux/_tabBar.scss new file mode 100644 index 0000000000..f5f07e24d5 --- /dev/null +++ b/scss/linux/_tabBar.scss @@ -0,0 +1,3 @@ +.tabs-holder { + flex: 1; +} diff --git a/scss/zotero-react-client-unix.scss b/scss/zotero-react-client-unix.scss index d3709baeef..438b47928d 100644 --- a/scss/zotero-react-client-unix.scss +++ b/scss/zotero-react-client-unix.scss @@ -7,4 +7,5 @@ @import "linux/createParent"; @import "linux/editable"; @import "linux/search"; +@import "linux/tabBar"; @import "linux/tagsBox";