diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx index 7c8bb753f8..17d6f3d24c 100644 --- a/chrome/content/zotero/components/tabBar.jsx +++ b/chrome/content/zotero/components/tabBar.jsx @@ -65,6 +65,15 @@ const TabBar = forwardRef(function (props, ref) { }); function handleTabMouseDown(event, id) { + if (event.button === 2) { + let { screenX, screenY } = event; + // Popup gets immediately closed without this + setTimeout(() => { + props.onContextMenu(screenX, screenY, id); + }, 0); + return; + } + if (event.target.closest('.tab-close')) { return; } diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js index 9cea97bb86..fb539f7759 100644 --- a/chrome/content/zotero/tabs.js +++ b/chrome/content/zotero/tabs.js @@ -51,6 +51,7 @@ var Zotero_Tabs = new function () { }]; this._selectedID = 'zotero-pane'; this._prevSelectedID = null; + this._history = []; this._getTab = function (id) { var tabIndex = this._tabs.findIndex(tab => tab.id == id); @@ -78,6 +79,7 @@ var Zotero_Tabs = new function () { onTabSelect={this.select.bind(this)} onTabMove={this.move.bind(this)} onTabClose={this.close.bind(this)} + onContextMenu={this._openMenu.bind(this)} />, document.getElementById('tab-bar-container'), () => { @@ -181,27 +183,41 @@ var Zotero_Tabs = new function () { }; /** - * Close a tab + * Close tabs * - * @param {String} id + * @param {String|Array|undefined} ids One or more ids, or empty for the current tab */ - this.close = function (id) { - var { tab, tabIndex } = this._getTab(id || this._selectedID); - if (tabIndex == 0) { + this.close = function (ids) { + if (!ids) { + ids = [this._selectedID]; + } + else if (!Array.isArray(ids)) { + ids = [ids]; + } + if (ids.includes('zotero-pane')) { throw new Error('Library tab cannot be closed'); } - if (!tab) { - return; + var historyEntry = []; + var closedIDs = []; + var tmpTabs = this._tabs.slice(); + for (var id of ids) { + var { tab, tabIndex } = this._getTab(id); + if (!tab) { + continue; + } + if (tab.id == this._selectedID) { + this.select(this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id); + } + this._tabs.splice(tabIndex, 1); + document.getElementById(tab.id).remove(); + if (tab.onClose) { + tab.onClose(); + } + historyEntry.push({ index: tmpTabs.indexOf(tab), data: tab.data }); + closedIDs.push(id); } - if (tab.id == this._selectedID) { - this.select(this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id); - } - this._tabs.splice(tabIndex, 1); - document.getElementById(tab.id).remove(); - if (tab.onClose) { - tab.onClose(); - } - Zotero.Notifier.trigger('close', 'tab', [tab.id], true); + this._history.push(historyEntry); + Zotero.Notifier.trigger('close', 'tab', [closedIDs]); this._update(); }; @@ -209,7 +225,27 @@ var Zotero_Tabs = new function () { * Close all tabs except the first one */ this.closeAll = function () { - this._tabs.slice(1).map(tab => this.close(tab.id)); + this.close(this._tabs.slice(1).map(x => x.id)); + }; + + /** + * Undo tabs closing + */ + this.undoClose = function () { + var historyEntry = this._history.pop(); + if (historyEntry) { + for (var tab of historyEntry) { + if (Zotero.Items.exists(tab.data.itemID)) { + Zotero.Reader.open(tab.data.itemID, + null, + { + tabIndex: tab.index, + openInBackground: true + } + ); + } + } + } }; /** @@ -283,10 +319,99 @@ var Zotero_Tabs = new function () { * * @param {Integer} index */ - this.jump = function(index) { + this.jump = function (index) { this.select(this._tabs[Math.min(index, this._tabs.length - 1)].id); }; + this._openMenu = function (x, y, id) { + var { tabIndex } = this._getTab(id); + window.Zotero_Tooltip.stop(); + let menuitem; + let popup = document.createElement('menupopup'); + document.querySelector('popupset').appendChild(popup); + popup.addEventListener('popuphidden', function (event) { + if (event.target === popup) { + popup.remove(); + } + }); + if (id !== 'zotero-pane') { + // Show in library + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('general.showInLibrary')); + menuitem.addEventListener('command', () => { + var reader = Zotero.Reader.getByTabID(id); + if (reader) { + ZoteroPane_Local.selectItem(reader.itemID); + this.select('zotero-pane'); + } + }); + popup.appendChild(menuitem); + // Move tab + let menu = document.createElement('menu'); + menu.setAttribute('label', Zotero.getString('tabs.move')); + let menupopup = document.createElement('menupopup'); + menu.append(menupopup); + popup.appendChild(menu); + // Move to start + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('tabs.moveToStart')); + menuitem.setAttribute('disabled', tabIndex == 1); + menuitem.addEventListener('command', () => { + this.move(id, 1); + }); + menupopup.appendChild(menuitem); + // Move to end + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('tabs.moveToEnd')); + menuitem.setAttribute('disabled', tabIndex == this._tabs.length - 1); + menuitem.addEventListener('command', () => { + this.move(id, this._tabs.length); + }); + menupopup.appendChild(menuitem); + // Move to new window + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('tabs.moveToWindow')); + menuitem.setAttribute('disabled', false); + menuitem.addEventListener('command', () => { + var reader = Zotero.Reader.getByTabID(id); + if (reader) { + this.close(id); + Zotero.Reader.open(reader.itemID, null, { openInWindow: true }); + } + }); + menupopup.appendChild(menuitem); + // Separator + popup.appendChild(document.createElement('menuseparator')); + } + // Undo close + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('tabs.undoClose')); + menuitem.setAttribute('disabled', !this._history.length); + menuitem.addEventListener('command', () => { + this.undoClose(); + }); + popup.appendChild(menuitem); + if (!(this._tabs.length == 2 && id != 'zotero-pane')) { + // Close other tabs + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('tabs.closeOther')); + menuitem.addEventListener('command', () => { + this.close(this._tabs.slice(1).filter(x => x.id != id).map(x => x.id)); + }); + popup.appendChild(menuitem); + } + if (id != 'zotero-pane') { + // Close + menuitem = document.createElement('menuitem'); + menuitem.setAttribute('label', Zotero.getString('general.close')); + menuitem.addEventListener('command', () => { + this.close(id); + }); + popup.appendChild(menuitem); + } + popup.openPopupAtScreen(x, y, true); + }; + /** * Update state of the tab bar. * Only used on Windows and Linux. On macOS, the tab bar is always shown. diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 70afacb301..1d6190cc2f 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -664,7 +664,7 @@ class ReaderInstance { } class ReaderTab extends ReaderInstance { - constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, background }) { + constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, index, background }) { super(); this._itemID = itemID; this._sidebarWidth = sidebarWidth; @@ -675,6 +675,7 @@ class ReaderTab extends ReaderInstance { let { id, container } = this._window.Zotero_Tabs.add({ type: 'reader', title: title || '', + index, data: { itemID }, @@ -906,12 +907,17 @@ class Reader { notify(event, type, ids, extraData) { if (type === 'tab') { - let reader = Zotero.Reader.getByTabID(ids[0]); - if (reader) { - if (event === 'close') { - this._readers.splice(this._readers.indexOf(reader), 1); + if (event === 'close') { + for (let id of ids) { + let reader = Zotero.Reader.getByTabID(id); + if (reader) { + this._readers.splice(this._readers.indexOf(reader), 1); + } } - else if (event === 'select') { + } + else if (event === 'select') { + let reader = Zotero.Reader.getByTabID(ids[0]); + if (reader) { this.triggerAnnotationsImportCheck(reader._itemID); } } @@ -976,7 +982,7 @@ class Reader { await this.open(item.id, location, options); } - async open(itemID, location, { title, openInBackground, openInWindow } = {}) { + async open(itemID, location, { title, tabIndex, openInBackground, openInWindow } = {}) { this._loadSidebarOpenState(); this.triggerAnnotationsImportCheck(itemID); let reader; @@ -1017,6 +1023,7 @@ class Reader { reader = new ReaderTab({ itemID, title, + index: tabIndex, background: openInBackground, sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 399f8394ee..f923f943dc 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -533,6 +533,15 @@ var ZoteroPane = new function() return; } } + + // Undo closed tabs + if ((Zotero.isMac && event.metaKey || !Zotero.isMac && event.ctrlKey) + && event.shiftKey && !event.altKey && event.key.toLowerCase() == 't') { + Zotero_Tabs.undoClose(); + event.preventDefault(); + event.stopPropagation(); + return; + } // Tab navigation: Ctrl-PageUp / PageDown // TODO: Select across tabs without selecting with Ctrl-Shift, as in Firefox? diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index b3b5a00c89..e48c2759c4 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -50,6 +50,7 @@ general.export = Export general.moreInformation = More Information general.seeForMoreInformation = See %S for more information. general.open = Open %S +general.close = Close general.enable = Enable general.disable = Disable general.remove = Remove @@ -67,6 +68,7 @@ general.fix = Fix… general.tryAgain = Try Again general.tryLater = Try Later general.showDirectory = Show Directory +general.showInLibrary = Show in Library general.continue = Continue general.copy = Copy general.copyToClipboard = Copy to Clipboard @@ -1323,7 +1325,6 @@ noteEditor.set = Set noteEditor.edit = Edit noteEditor.addCitation = Add Citation noteEditor.findAndReplace = Find and Replace -noteEditor.showInLibrary = Show in Library noteEditor.editInWindow = Edit in a Separate Window noteEditor.applyAnnotationColors = Apply Annotation Colors noteEditor.removeAnnotationColors = Remove Annotation Colors @@ -1354,3 +1355,10 @@ spellCheck.addRemoveDictionaries = Add/Remove Dictionaries… spellCheck.dictionaryManager.title = Add/Remove Dictionaries spellCheck.dictionaryManager.updateAvailable = %S (update available) spellCheck.dictionaryManager.error.unableToInstall = Unable to install “%S”: + +tabs.move = Move Tab +tabs.moveToStart = Move to Start +tabs.moveToEnd = Move to End +tabs.moveToWindow = Move to New Window +tabs.undoClose = Reopen Closed Tab +tabs.closeOther = Close Other Tabs diff --git a/note-editor b/note-editor index 9749a4b2be..4ba41a4168 160000 --- a/note-editor +++ b/note-editor @@ -1 +1 @@ -Subproject commit 9749a4b2be4baac5b16f28e70a7e7997b000ef9e +Subproject commit 4ba41a41689448aecc21a72aec0559d39ca4509e