Ignore AltGr keystrokes in Ctrl+Alt keyboard shortcuts (#825)
* Ignore AltGr keystrokes in Ctrl+Alt keyboard shortcuts
Browsers report AltGr (right Alt on AZERTY/QWERTZ and most non-US
layouts, used to type @ # { } [ ] | \ and the euro sign) as
ctrlKey+altKey. The default keybinds map destructive actions to
Ctrl+Alt+<letter> (delete_session, new_session, incognito,
open_calendar), so a non-US user typing a special character could
silently fire them.
Guard the shortcut matcher, the editor keydown handler, and the rebind
capture with getModifierState('AltGraph'), which is true for AltGr but
false for a genuine left Ctrl+Alt. macOS is excluded: there the Option
key legitimately sets AltGraph and there is no AltGr/Ctrl+Alt collision
to guard against, so the guard would otherwise break Ctrl+Option /
Cmd+Option shortcuts (notably in Firefox).
The detection lives in one place — isAltGrEvent / IS_MAC in
static/js/platform.js — and all three call sites route through it, so the
guards can't drift apart.
The editor handler only skips the Ctrl+Alt chord block, so layout
shortcuts reachable via AltGr (e.g. [ ] brush size = AltGr+5/+8 on
AZERTY) keep working.
* Require Ctrl+Alt for the AltGr guard and consolidate keybind test marks
isAltGrEvent now also checks ctrlKey+altKey so it only suppresses the
"AltGr reported as Ctrl+Alt" collision; an event asserting AltGraph on
its own (a Linux ISO_Level3_Shift layout, a stray modifier) is left
alone. Pin it with test_isaltgr_false_when_altgraph_set_but_not_ctrl_alt.
Collapse the 12 per-test node skipif marks into one module-level
pytestmark, and note in platform.js why IS_MAC intentionally covers
iPad/iPhone and mirrors the isMac checks in calendar.js / sessions.js.
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
* }} deps
|
||||
*/
|
||||
import { state } from './state.js';
|
||||
import { isAltGrEvent } from '../platform.js';
|
||||
|
||||
export function wireKeyboardShortcuts(deps) {
|
||||
const {
|
||||
@@ -79,7 +80,11 @@ export function wireKeyboardShortcuts(deps) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
// Skip the Ctrl+Alt editor chords for an AltGr keystroke (see platform.js);
|
||||
// only the chord block is skipped, so the layout-character handlers below
|
||||
// still act — AltGr+5 / AltGr+8 stay as the [ ] brush-size shortcut on
|
||||
// AZERTY / QWERTZ.
|
||||
if ((e.ctrlKey || e.metaKey) && !isAltGrEvent(e)) {
|
||||
if (e.key === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); }
|
||||
// Ctrl+Shift+D = Deselect: clears the wand selection (and
|
||||
// lasso if active) without affecting layers.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Keyboard Shortcuts — dynamic keybinds
|
||||
// ============================================
|
||||
|
||||
import { IS_MAC, isAltGrEvent } from './platform.js';
|
||||
|
||||
const _defaultKeybinds = {
|
||||
search: 'ctrl+k', toggle_sidebar: 'ctrl+alt+b', new_session: 'ctrl+alt+n',
|
||||
fav_session: 'ctrl+alt+f', delete_session: 'ctrl+alt+d',
|
||||
@@ -13,8 +15,11 @@ const _defaultKeybinds = {
|
||||
open_notes: '', open_tasks: '', open_theme: '',
|
||||
};
|
||||
|
||||
function _matchesCombo(e, combo) {
|
||||
export function _matchesCombo(e, combo, isMac = IS_MAC) {
|
||||
if (!combo) return false;
|
||||
// Drop AltGr keystrokes so typing characters on non-US layouts can't fire a
|
||||
// Ctrl+Alt shortcut — e.g. the destructive delete_session. See platform.js.
|
||||
if (isAltGrEvent(e, isMac)) return false;
|
||||
const parts = combo.split('+');
|
||||
const needCtrl = parts.includes('ctrl');
|
||||
const needAlt = parts.includes('alt');
|
||||
|
||||
47
static/js/platform.js
Normal file
47
static/js/platform.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// ============================================
|
||||
// Platform detection + AltGr-keystroke helper
|
||||
// ============================================
|
||||
// Shared by the keybind code: root keyboard-shortcuts.js, the editor's
|
||||
// keyboard-shortcuts.js, and settings.js. Single source of truth so the three
|
||||
// guards can't drift.
|
||||
|
||||
// AltGr (right Alt on AZERTY/QWERTZ and most non-US layouts, used to type
|
||||
// @ # { } [ ] | \ and €) is reported by browsers as Ctrl+Alt. macOS is the
|
||||
// exception: there the Option key — a normal part of Mac shortcuts — also sets
|
||||
// the AltGraph modifier state, so it must NOT be treated as AltGr.
|
||||
//
|
||||
// IS_MAC covers all Apple platforms, iPad/iPhone included: a Magic Keyboard's
|
||||
// Option key sets AltGraph exactly like a Mac's, so they need the same carve-out
|
||||
// — narrowing to macOS-only would re-break them. The name and the
|
||||
// /Mac|iPhone|iPad/ test deliberately mirror the existing isMac checks in
|
||||
// calendar.js and sessions.js; this is their single shared source of truth.
|
||||
export const IS_MAC =
|
||||
/Mac|iPhone|iPad/.test((typeof navigator !== 'undefined' && navigator.platform) || '') ||
|
||||
/Mac/.test((typeof navigator !== 'undefined' && navigator.userAgent) || '');
|
||||
|
||||
// True when `e` is an AltGr keystroke we should ignore for Ctrl+Alt shortcut
|
||||
// purposes. getModifierState('AltGraph') is true for AltGr but false for a
|
||||
// genuine left Ctrl+Alt, so real shortcuts still work. Always false on macOS,
|
||||
// where Option legitimately sets AltGraph.
|
||||
//
|
||||
// We also require ctrlKey+altKey: the collision we defend against is precisely
|
||||
// "AltGr reported AS Ctrl+Alt", so an event that asserts AltGraph WITHOUT
|
||||
// presenting as Ctrl+Alt (a Linux ISO_Level3_Shift layout, a stray modifier
|
||||
// state) is left alone instead of being swallowed.
|
||||
//
|
||||
// Trade-off: on Windows AltGr *is* Ctrl+right-Alt, so a deliberate
|
||||
// Ctrl+Alt+<char> shortcut typed via AltGr is unreachable too — accepted; use
|
||||
// the left Ctrl+Alt.
|
||||
//
|
||||
// NOTE: the AltGr -> AltGraph mapping is taken from the UI Events spec / MDN,
|
||||
// not proven by our tests. Older Firefox and some Linux setups historically did
|
||||
// not report AltGraph; where a browser sets ctrlKey+altKey without it this
|
||||
// guard is simply a no-op (the pre-fix behaviour) rather than a regression.
|
||||
export function isAltGrEvent(e, isMac = IS_MAC) {
|
||||
return (
|
||||
!isMac &&
|
||||
!!e.ctrlKey &&
|
||||
!!e.altKey &&
|
||||
!!(e.getModifierState && e.getModifierState('AltGraph'))
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import searchModule from './search.js';
|
||||
import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { clearDockSide } from './modalSnap.js';
|
||||
import { sortModelIds } from './modelSort.js';
|
||||
import { isAltGrEvent } from './platform.js';
|
||||
|
||||
let initialized = false;
|
||||
let modalEl = null;
|
||||
@@ -1710,6 +1711,10 @@ function _formatKeyCaps(combo) {
|
||||
}
|
||||
|
||||
function _comboFromEvent(e) {
|
||||
// Drop a stray AltGr keystroke (e.g. AltGr+E to type €) so it isn't recorded
|
||||
// as a bogus ctrl+alt+<char> binding — onKey ignores empty combos. See
|
||||
// platform.js for the macOS carve-out and Windows trade-off.
|
||||
if (isAltGrEvent(e)) return '';
|
||||
const parts = [];
|
||||
if (e.ctrlKey || e.metaKey) parts.push('ctrl');
|
||||
if (e.altKey) parts.push('alt');
|
||||
|
||||
Reference in New Issue
Block a user