diff --git a/chrome/content/zotero/platformKeys.js b/chrome/content/zotero/platformKeys.js index 148615c965..38502b3760 100644 --- a/chrome/content/zotero/platformKeys.js +++ b/chrome/content/zotero/platformKeys.js @@ -40,6 +40,72 @@ window.addEventListener('DOMContentLoaded', () => { if (fileQuitItemUnix) fileQuitItemUnix.hidden = true; if (editPreferencesSeparator) editPreferencesSeparator.hidden = true; if (editPreferencesItem) editPreferencesItem.hidden = true; + + // macOS 15 Sequoia has a new system keyboard shortcut, Ctrl-Enter, + // that shows a context menu on the focused control. Firefox currently + // doesn't handle it very well - it shows a context menu on the element + // in the middle of the window, whatever element that may be. + // Prevent/retarget these events (but not Ctrl-clicks). + let lastPreventedContextMenuTime = 0; + document.addEventListener('contextmenu', (event) => { + if (!(event.button === 2 && event.buttons === 0 && !event.ctrlKey)) { + return; + } + + event.stopPropagation(); + event.stopImmediatePropagation(); + event.preventDefault(); + + // We usually get three of these in a row - only act on the first + if (event.timeStamp - lastPreventedContextMenuTime < 50) { + return; + } + lastPreventedContextMenuTime = event.timeStamp; + + let targetElement = document.activeElement; + if (!targetElement) { + return; + } + if (targetElement.hasAttribute('aria-activedescendant')) { + let activeDescendant = targetElement.querySelector( + '#' + CSS.escape(targetElement.getAttribute('aria-activedescendant')) + ); + if (activeDescendant) { + targetElement = activeDescendant; + } + } + + let [clientX, clientY] = Zotero.Utilities.Internal.getContextMenuPosition(targetElement); + let screenX = window.mozInnerScreenX + clientX; + let screenY = window.mozInnerScreenY + clientY; + + // Run in the next tick, because otherwise our rate-limiting above + // prevents this from working on form fields (somehow) + setTimeout(() => { + targetElement.dispatchEvent(new PointerEvent('contextmenu', { + bubbles: true, + cancelable: true, + button: 2, + buttons: 2, + clientX, + clientY, + layerX: clientX, // Wrong, but nobody should ever use these + layerY: clientY, + screenX, + screenY, + })); + }); + }, { capture: true }); + + // Make sure the Ctrl-Enter isn't handled by listeners further down in + // the tree as a regular Enter + document.documentElement.addEventListener('keydown', (event) => { + if (event.ctrlKey && event.key === 'Enter') { + event.stopPropagation(); + event.stopImmediatePropagation(); + event.preventDefault(); + } + }, { capture: true }); } else { // Set behavior on all non-macOS platforms diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index 64c5284366..b510598ff5 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -2482,6 +2482,66 @@ Zotero.Utilities.Internal = { } return textContent; + }, + + /** + * + * @param {Element} targetElement + * @returns {[number, number]} clientX and clientY + */ + getContextMenuPosition(targetElement) { + let selection; + if (targetElement.editor?.selection) { + selection = targetElement.editor.selection; + if (!selection.rangeCount) { + selection = null; + } + } + else { + selection = targetElement.ownerDocument.getSelection(); + if (!selection.rangeCount || !targetElement.contains(selection.getRangeAt(0).startContainer)) { + selection = null; + } + } + + let rect; + let anchorToBottom; + let anchorToEnd; + if (selection) { + let range = selection.getRangeAt(0); + if (range.getClientRects().length) { + rect = range.getBoundingClientRect(); + } + // If the selection is between lines in an editor, it'll be + // inside the editor's native anonymous text node and won't + // have any rects for some reason. + // If that's the case, use the text node's bounds. + else if (range.startContainer === range.endContainer && range.startContainer.isNativeAnonymous + && range.startContainer.firstChild?.nodeType === Node.TEXT_NODE) { + let quads = range.startContainer.firstChild.getBoxQuads(); + rect = quads[quads.length - 1].getBounds(); + } + else { + rect = range.commonAncestorContainer.getBoundingClientRect(); + } + anchorToBottom = !range.collapsed; + anchorToEnd = range.collapsed; + } + else { + rect = targetElement.getBoundingClientRect(); + anchorToBottom = true; + anchorToEnd = false; + } + + let clientX; + if (Zotero.rtl) { + clientX = rect.x + (anchorToEnd ? 0 : rect.width - 3); + } + else { + clientX = rect.x + (anchorToEnd ? rect.width + 3 : 0); + } + let clientY = rect.y + (anchorToBottom ? rect.height + 8 : 5); + return [clientX, clientY]; } };