mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Fix default shortcuts that fail on some layouts and browsers (#12506)
Fixes the problems found after #12326 and listed in #12327. Mousetrap now tries several names for each key press, in order, and fires the first one with a binding: letters and digits by keyCode when a modifier is held, punctuation by position (layout map, or a US table when the browser has none), the typed glyph, and the typed glyph without the Shift it needed. With AltGr the typed glyph comes before the position. The shortcut recorder asks Mousetrap for the key it stores, so a recorded shortcut always fires. This makes "?" and "+" work on every layout, Shift and Ctrl+Shift with punctuation work in Firefox and Safari, Option+letter work on macOS, and AltGr+º hide the UI on Linux with a Spanish layout. Chrome on Linux returns the map of the first configured layout, not the active one. Mousetrap now drops the map when an unmodified punctuation key types something else. The layout twins are no longer needed and are removed: "shift+'" made "?" zoom to the selection on Spanish layouts. The Add node shortcut is removed too: on US layouts its key is the "+" zoom key, and on Firefox with a Spanish layout it cannot fire. The Firefox and Safari warning in the shortcuts settings is gone, since those shortcuts now work there. The user guide and translations follow these changes. AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
3cd073fc6a
commit
68ecfb1234
@ -35,9 +35,9 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
|
||||
<p class="advice"><strong>Paste</strong> cannot be customized.</p>
|
||||
|
||||
<h3 id="customize-shortcuts-layouts">Keyboard layouts</h3>
|
||||
<p>Letters are recorded by the character they type, so they follow your layout (on AZERTY, the <kbd>A</kbd> key records <kbd>a</kbd>). With <kbd>Shift</kbd>, digit keys record the digit itself (<kbd>Shift</kbd><kbd>0</kbd>, not the shifted symbol), on every layout and in every browser.</p>
|
||||
<p>Punctuation keys (such as <kbd>-</kbd>, <kbd>[</kbd>, <kbd>/</kbd> or <kbd>'</kbd>) are recorded by their physical position on the keyboard, so the same key works on AZERTY, QWERTZ and other layouts — but only in browsers that expose the keyboard layout (Chromium). There, <kbd>Shift</kbd> with the <kbd>+</kbd> key records <kbd>Shift</kbd><kbd>+</kbd>. In browsers without that support, such as Firefox or Safari, punctuation falls back to the typed character (<kbd>Shift</kbd><kbd>*</kbd> instead): those shortcuts still work in the browser where you recorded them, but <kbd>Shift</kbd> combinations with punctuation may stop working if you switch browsers. If that affects you, record those shortcuts in a Chromium browser.</p>
|
||||
<p>Two known limitations: number keys set opacity (<kbd>0</kbd>–<kbd>9</kbd>) only where digits are typed without <kbd>Shift</kbd>, so AZERTY layouts need the numeric keypad for them; and <kbd>\</kbd> needs <kbd>AltGr</kbd> on some layouts (Spanish), where it is recorded with <kbd>Alt</kbd> and its exact form depends on the browser.</p>
|
||||
<p>Letters are recorded by the character they type, so they follow your layout (on AZERTY, the <kbd>A</kbd> key records <kbd>a</kbd>). With <kbd>Shift</kbd>, <kbd>Alt</kbd> or another modifier, letter and digit keys record the key itself, not the character the modifier makes (<kbd>Shift</kbd><kbd>0</kbd>, not <kbd>Shift</kbd><kbd>)</kbd>; <kbd>Option</kbd><kbd>M</kbd> on macOS, not <kbd>µ</kbd>).</p>
|
||||
<p>Punctuation keys (such as <kbd>-</kbd>, <kbd>[</kbd>, <kbd>/</kbd> or <kbd>'</kbd>) are recorded by their physical position on the keyboard, so <kbd>Shift</kbd> with the <kbd>+</kbd> key records <kbd>Shift</kbd><kbd>+</kbd> on a Spanish layout. In browsers that do not expose the keyboard layout, such as Firefox or Safari, Penpot records the character you type instead; the shortcut still works there. Keys typed with <kbd>AltGr</kbd> record the character they type (<kbd>\</kbd> on a Spanish layout).</p>
|
||||
<p>One known limitation: number keys set opacity (<kbd>0</kbd>–<kbd>9</kbd>) only where digits are typed without <kbd>Shift</kbd>, so AZERTY layouts need the numeric keypad for them.</p>
|
||||
|
||||
<h3 id="customize-shortcuts-reset">Reset to default</h3>
|
||||
<p>When you have custom shortcuts, a <strong>Reset all</strong> button appears next to the search field. It lists every shortcut that will go back to its default before you confirm.</p>
|
||||
@ -617,11 +617,6 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Add node</td>
|
||||
<td style="text-align: center;"><kbd>Shift</kbd><kbd>+</kbd></td>
|
||||
<td style="text-align: center;"><kbd>⇧</kbd><kbd>+</kbd></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete node, segment or handle</td>
|
||||
<td style="text-align: center;"><kbd>Delete</kbd> or <kbd>Backspace</kbd></td>
|
||||
|
||||
@ -94,8 +94,8 @@ const _SPECIAL_ALIASES = {
|
||||
/**
|
||||
* Physical codes treated positionally. Letters and digits resolve by
|
||||
* glyph (`event.key`), but punctuation resolves by position (`event.code`)
|
||||
* through the layout map, so the same physical key matches on every
|
||||
* layout with and without `Shift`. Without a map the typed glyph applies.
|
||||
* through the layout map, or `_US_PUNCTUATION` without one, so the same
|
||||
* physical key matches with and without `Shift`. See `_candidates`.
|
||||
*
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
@ -113,6 +113,45 @@ const _PUNCTUATION_CODES = new Set([
|
||||
"Slash",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Primary character of each punctuation key on a US layout. Used when
|
||||
* the browser has no layout map (Firefox, Safari), so punctuation still
|
||||
* resolves by position and every browser names a key the same way.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const _US_PUNCTUATION = {
|
||||
Minus: "-",
|
||||
Equal: "=",
|
||||
BracketLeft: "[",
|
||||
BracketRight: "]",
|
||||
Backslash: "\\",
|
||||
Semicolon: ";",
|
||||
Quote: "'",
|
||||
Backquote: "`",
|
||||
Comma: ",",
|
||||
Period: ".",
|
||||
Slash: "/",
|
||||
};
|
||||
|
||||
/**
|
||||
* Letter or digit named by `keyCode`. With a modifier held, `event.key`
|
||||
* carries the glyph the modifier makes (`!` for Shift+1, `µ` for
|
||||
* Option+M on macOS, or `Dead`), while `keyCode` still names the key.
|
||||
* Unlike `event.code` it follows the layout, so Ctrl+Z stays `z` on
|
||||
* AZERTY.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {string|null}
|
||||
*/
|
||||
function _letterOrDigitFromKeyCode(e) {
|
||||
const keyCode = e.keyCode;
|
||||
if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90)) {
|
||||
return String.fromCharCode(keyCode).toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an event to its canonical key. Implemented as
|
||||
* `Mousetrap#_characterFromEvent` so punctuation can consult the
|
||||
@ -130,27 +169,38 @@ function _modifiersMatch(modifiers1, modifiers2) {
|
||||
return modifiers1.sort().join(",") === modifiers2.sort().join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* whether the `AltGraph` modifier is active for the event
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isAltGraph(e) {
|
||||
return (
|
||||
typeof e.getModifierState === "function" && e.getModifierState("AltGraph")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* takes a key event and figures out what the modifiers are
|
||||
*
|
||||
* On Windows `AltGr` reports `ctrlKey + altKey` as well. When the
|
||||
* `AltGraph` modifier is active the phantom `ctrl` is dropped so typing
|
||||
* (e.g. `]` on Spanish keyboards) does not fire `ctrl+alt` shortcuts.
|
||||
* `AltGraph` modifier is active both phantom modifiers are dropped, so
|
||||
* typing (e.g. `]` or `\` on Spanish keyboards) neither fires `ctrl+alt`
|
||||
* shortcuts nor hides the plain glyph binding.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {Array}
|
||||
*/
|
||||
function _eventModifiers(e) {
|
||||
const modifiers = [];
|
||||
const altGraph =
|
||||
typeof e.getModifierState === "function" &&
|
||||
e.getModifierState("AltGraph");
|
||||
const altGraph = _isAltGraph(e);
|
||||
|
||||
if (e.shiftKey) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if (e.altKey) {
|
||||
if (e.altKey && !altGraph) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
@ -383,22 +433,209 @@ class Mousetrap {
|
||||
}
|
||||
|
||||
/**
|
||||
* takes the event and returns the canonical key
|
||||
* drops the layout map when it does not describe the active layout
|
||||
*
|
||||
* Letters, digits and named keys resolve by glyph: single characters
|
||||
* lowercase so `Shift+Z` matches a `ctrl+shift+z` bind, `" "` maps to
|
||||
* `space`, and named keys map through `_KEY_NAMES`. The exception is a
|
||||
* digit row key held with `Shift`, which resolves to the digit itself
|
||||
* (`Digit0` types ")" on US layouts but "0" on AZERTY, so `shift+0`
|
||||
* must mean the physical digit key everywhere). Punctuation
|
||||
* resolves positionally via the layout map (see `setLayoutMap`),
|
||||
* falling back to the glyph when no map is set. Numpad digits stay
|
||||
* positional (`num0`..`num9`) so they keep their own bindings.
|
||||
* Chrome on Linux returns the map of the first configured layout, not
|
||||
* the active one: with Spanish first and US active, the map names the
|
||||
* `=` key `¡`. A punctuation key pressed without modifiers types the
|
||||
* primary character of its position, so when that differs from the
|
||||
* map, punctuation goes back to the glyph and US-table fallback for the
|
||||
* rest of the session.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns void
|
||||
*/
|
||||
_checkLayoutMap(e) {
|
||||
if (
|
||||
!this._layoutMap ||
|
||||
e.type !== "keydown" ||
|
||||
!e.code ||
|
||||
!_PUNCTUATION_CODES.has(e.code) ||
|
||||
typeof e.key !== "string" ||
|
||||
e.key.length !== 1 ||
|
||||
e.shiftKey ||
|
||||
e.ctrlKey ||
|
||||
e.altKey ||
|
||||
e.metaKey ||
|
||||
_isAltGraph(e)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mapped = null;
|
||||
try {
|
||||
mapped = this._layoutMap.get(e.code);
|
||||
} catch {
|
||||
mapped = null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof mapped === "string" &&
|
||||
mapped !== "" &&
|
||||
mapped.toLowerCase() !== e.key.toLowerCase()
|
||||
) {
|
||||
this._layoutMap = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* takes the event and returns the canonical key: the first candidate
|
||||
* of `_candidates`, which is also what the shortcut recorder stores
|
||||
* (see `keyForRecording`).
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {string|null}
|
||||
*/
|
||||
_characterFromEvent(e) {
|
||||
const candidates = this._candidates(e);
|
||||
return candidates.length ? candidates[0][0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* public entry point for the shortcut recorder: the key token to store
|
||||
* for this event, always one of `_candidates`, so this instance matches
|
||||
* the recorded shortcut. Returns `null` for modifiers alone.
|
||||
*
|
||||
* Letters and digits with a modifier use `keyCode`. Punctuation uses
|
||||
* its position when there is a layout map, since the map names the
|
||||
* printed key. Without a map (Firefox, Safari) the typed glyph is a
|
||||
* better label than the US key at that position, so the Spanish `+`
|
||||
* records `+`, not `]`. With AltGr the typed glyph wins too: AltGr+º
|
||||
* on a Spanish layout records `\`, the character the user typed.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {string|null}
|
||||
*/
|
||||
keyForRecording(e) {
|
||||
this._checkLayoutMap(e);
|
||||
|
||||
const modifiers = _eventModifiers(e);
|
||||
const numpad = e.code && e.code.indexOf("Numpad") === 0;
|
||||
const keyCodeKey =
|
||||
modifiers.length && !numpad ? _letterOrDigitFromKeyCode(e) : null;
|
||||
const position = this._positionFromEvent(e);
|
||||
const glyph = this._glyphFromEvent(e);
|
||||
|
||||
let key;
|
||||
if (keyCodeKey) {
|
||||
key = keyCodeKey;
|
||||
} else if (this._layoutMap && !_isAltGraph(e)) {
|
||||
key = position || glyph;
|
||||
} else {
|
||||
key = glyph || position;
|
||||
}
|
||||
|
||||
return key && !_isModifier(key) ? key : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* the `[key, modifiers]` pairs an event may match, most specific
|
||||
* first. `_handleKeyEvent` dispatches the first pair that has a
|
||||
* binding:
|
||||
*
|
||||
* 1. With a modifier held, letters and digits by `keyCode`, so
|
||||
* `Shift+1` is `shift+1` and `Option+M` is `alt+m`.
|
||||
* 2. Punctuation by position: the primary character from the layout
|
||||
* map (see `setLayoutMap`), or from a US table when the browser has
|
||||
* no map, so `Shift+*` on a Spanish layout is `shift++`.
|
||||
* 3. The typed glyph (`event.key`), so `+` on a US layout (typed with
|
||||
* Shift) matches a `shift++` binding.
|
||||
* 4. The typed glyph without the Shift it needed, so `?` and `+`
|
||||
* bindings fire.
|
||||
*
|
||||
* With AltGr, 3 comes before 2 (see `keyForRecording`).
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {Array}
|
||||
*/
|
||||
_candidates(e) {
|
||||
const modifiers = _eventModifiers(e);
|
||||
const candidates = [];
|
||||
const add = (key, mods) => {
|
||||
if (key) {
|
||||
candidates.push([key, mods]);
|
||||
}
|
||||
};
|
||||
|
||||
const numpad = e.code && e.code.indexOf("Numpad") === 0;
|
||||
if (modifiers.length && !numpad) {
|
||||
add(_letterOrDigitFromKeyCode(e), modifiers);
|
||||
}
|
||||
|
||||
// With AltGr the typed glyph comes before the position: AltGr+º
|
||||
// types `\` on a Spanish layout, and the key at that position is `º`.
|
||||
const altGraph = _isAltGraph(e);
|
||||
const position = this._positionFromEvent(e);
|
||||
const glyph = this._glyphFromEvent(e);
|
||||
if (altGraph) {
|
||||
add(glyph, modifiers);
|
||||
add(position, modifiers);
|
||||
} else {
|
||||
add(position, modifiers);
|
||||
add(glyph, modifiers);
|
||||
}
|
||||
|
||||
if (
|
||||
glyph &&
|
||||
glyph.length === 1 &&
|
||||
modifiers.includes("shift") &&
|
||||
!/[a-z0-9]/.test(glyph)
|
||||
) {
|
||||
add(
|
||||
glyph,
|
||||
modifiers.filter((m) => m !== "shift"),
|
||||
);
|
||||
}
|
||||
|
||||
// On Windows Ctrl+Alt is AltGr: Ctrl+Alt+E types `€` on a Spanish
|
||||
// layout. When nothing is bound to the glyph, fall back to the
|
||||
// `ctrl+alt` binding of the key.
|
||||
if (altGraph && e.ctrlKey && e.altKey && !numpad) {
|
||||
add(_letterOrDigitFromKeyCode(e), [...modifiers, "alt", "ctrl"]);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* primary character of a punctuation key by its position, or `null`
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {string|null}
|
||||
*/
|
||||
_positionFromEvent(e) {
|
||||
if (!e.code || !_PUNCTUATION_CODES.has(e.code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mapped = null;
|
||||
if (this._layoutMap) {
|
||||
try {
|
||||
mapped = this._layoutMap.get(e.code);
|
||||
} catch {
|
||||
mapped = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof mapped !== "string" || mapped === "") {
|
||||
mapped = _US_PUNCTUATION[e.code];
|
||||
}
|
||||
|
||||
return mapped ? mapped.toLowerCase() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* the typed glyph of the event as a canonical key
|
||||
*
|
||||
* Single characters lowercase so `Shift+Z` matches a `ctrl+shift+z`
|
||||
* bind, `" "` maps to `space`, and named keys map through
|
||||
* `_KEY_NAMES`. Numpad digits stay positional (`num0`..`num9`) so they
|
||||
* keep their own bindings.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @return {string|null}
|
||||
*/
|
||||
_glyphFromEvent(e) {
|
||||
// Numpad digits as "num0".."num9" — keeps them separate from main-row bindings across NumLock states and event types.
|
||||
if (e.code && e.code.indexOf("Numpad") === 0) {
|
||||
const suffix = e.code.substring(6);
|
||||
@ -407,35 +644,11 @@ class Mousetrap {
|
||||
}
|
||||
}
|
||||
|
||||
// Punctuation is positional: with a layout map the primary
|
||||
// (unshifted) character of the physical key wins, so `Shift+*` on a
|
||||
// Spanish layout (code `BracketRight`) resolves to `+` like the
|
||||
// recorder stores. Without a map the typed glyph applies.
|
||||
if (e.code && _PUNCTUATION_CODES.has(e.code) && this._layoutMap) {
|
||||
let mapped = null;
|
||||
try {
|
||||
mapped = this._layoutMap.get(e.code);
|
||||
} catch {
|
||||
mapped = null;
|
||||
}
|
||||
if (typeof mapped === "string" && mapped !== "") {
|
||||
return mapped.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const key = e.key;
|
||||
if (typeof key !== "string" || key === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Shifted digits resolve to the digit itself: with `Shift` held,
|
||||
// `Digit0` types ")" on US layouts but "0" on AZERTY, so the glyph
|
||||
// can't be the token. `shift+0` means the physical digit key
|
||||
// everywhere, like `shift+z` means `z` regardless of `Shift`.
|
||||
if (e.shiftKey && e.code && /^Digit[0-9]$/.test(e.code) && key.length === 1) {
|
||||
return e.code.charAt(5);
|
||||
}
|
||||
|
||||
if (_KEY_NAMES[key]) {
|
||||
return _KEY_NAMES[key];
|
||||
}
|
||||
@ -862,18 +1075,35 @@ class Mousetrap {
|
||||
* @returns void
|
||||
*/
|
||||
_handleKeyEvent(e) {
|
||||
const character = this._characterFromEvent(e);
|
||||
this._checkLayoutMap(e);
|
||||
|
||||
const candidates = this._candidates(e);
|
||||
// no character found then stop
|
||||
if (!character) {
|
||||
if (!candidates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// dispatch the first candidate that has a binding, or the first one
|
||||
// so sequences still reset on unbound keys
|
||||
let picked = candidates[0];
|
||||
for (const candidate of candidates) {
|
||||
if (
|
||||
this._callbacks[candidate[0]] &&
|
||||
this._getMatches(candidate[0], candidate[1], e).length
|
||||
) {
|
||||
picked = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const [character, modifiers] = picked;
|
||||
|
||||
if (e.type == "keyup" && this._ignoreNextKeyup === character) {
|
||||
this._ignoreNextKeyup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleKey(character, _eventModifiers(e), e);
|
||||
this.handleKey(character, modifiers, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,11 +36,15 @@ describe("bind and dispatch", () => {
|
||||
shiftPlus++;
|
||||
});
|
||||
fire(target, makeKeyEvent({ key: "+", code: "Equal" }));
|
||||
// Without a layout map Shift + the Spanish "+" key types "*" and
|
||||
// nothing says the key is "+", so "shift++" does not fire.
|
||||
fire(target, makeKeyEvent({ key: "*", code: "BracketRight", shiftKey: true }));
|
||||
// "*" normalizes by glyph, so "shift++" (shift + "+" key) does not fire;
|
||||
// this pins the layout-independent behavior for the "+" physical key.
|
||||
assert.equal(plus, 1);
|
||||
assert.equal(shiftPlus, 0);
|
||||
// A layout map names the key, so it does.
|
||||
mousetrap.setLayoutMap({ get: (code) => (code === "BracketRight" ? "+" : undefined) });
|
||||
fire(target, makeKeyEvent({ key: "*", code: "BracketRight", shiftKey: true }));
|
||||
assert.equal(shiftPlus, 1);
|
||||
});
|
||||
|
||||
it("resolves bind-string aliases to canonical tokens", () => {
|
||||
|
||||
@ -23,7 +23,16 @@ describe("layout map", () => {
|
||||
it("resolves punctuation by position, so Shift+* fires a shift++ bind", () => {
|
||||
const event = { key: "*", code: "BracketRight", shiftKey: true };
|
||||
assert.equal(firedWithMap(spanishMap, "shift++", event), 1);
|
||||
assert.equal(firedWithMap(spanishMap, "shift+*", event), 0);
|
||||
});
|
||||
|
||||
it("prefers the position over the glyph when both are bound", () => {
|
||||
const { mousetrap, target } = freshInstance();
|
||||
mousetrap.setLayoutMap(spanishMap);
|
||||
const calls = [];
|
||||
mousetrap.bind("shift++", () => calls.push("shift++"));
|
||||
mousetrap.bind("shift+*", () => calls.push("shift+*"));
|
||||
fire(target, makeKeyEvent({ key: "*", code: "BracketRight", shiftKey: true }));
|
||||
assert.deepEqual(calls, ["shift++"]);
|
||||
});
|
||||
|
||||
it("matches the unshifted primary on the same position", () => {
|
||||
@ -92,4 +101,74 @@ describe("layout map", () => {
|
||||
fire(target, makeKeyEvent({ key: "a", code: "KeyQ" }));
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
// Spanish layout as Chrome reports it: the "'" key sits on Minus and
|
||||
// "º" on Backquote.
|
||||
const fullSpanishMap = {
|
||||
get(code) {
|
||||
return { BracketRight: "+", Minus: "'", Backquote: "º", Equal: "¡" }[code];
|
||||
},
|
||||
};
|
||||
|
||||
const altGraph = (over) => ({
|
||||
getModifierState: (name) => name === "AltGraph",
|
||||
...over,
|
||||
});
|
||||
|
||||
it("fires ? on Spanish, where it is Shift + the ' key", () => {
|
||||
const event = { key: "?", code: "Minus", shiftKey: true };
|
||||
assert.equal(firedWithMap(fullSpanishMap, "?", event), 1);
|
||||
});
|
||||
|
||||
it("matches the AltGr glyph before the key position (Linux)", () => {
|
||||
// AltGr+º types "\" with no ctrl or alt on Linux.
|
||||
const { mousetrap, target } = freshInstance();
|
||||
mousetrap.setLayoutMap(fullSpanishMap);
|
||||
const calls = [];
|
||||
mousetrap.bind("\\", () => calls.push("\\"));
|
||||
mousetrap.bind("º", () => calls.push("º"));
|
||||
fire(target, makeKeyEvent(altGraph({ key: "\\", code: "Backquote" })));
|
||||
assert.deepEqual(calls, ["\\"]);
|
||||
});
|
||||
|
||||
it("matches the AltGr glyph when AltGr reports ctrl+alt (Windows)", () => {
|
||||
const event = altGraph({ key: "\\", code: "Backquote", ctrlKey: true, altKey: true });
|
||||
assert.equal(firedWithMap(fullSpanishMap, "\\", event), 1);
|
||||
});
|
||||
|
||||
it("records the AltGr glyph, not the key position", () => {
|
||||
const { mousetrap } = freshInstance();
|
||||
mousetrap.setLayoutMap(fullSpanishMap);
|
||||
const event = makeKeyEvent(altGraph({ key: "\\", code: "Backquote" }));
|
||||
assert.equal(mousetrap.keyForRecording(event), "\\");
|
||||
});
|
||||
|
||||
it("drops a map that disagrees with an unmodified punctuation key", () => {
|
||||
// Chrome on Linux keeps the map of the first layout (Spanish) while
|
||||
// US is active: the "=" key types "=" but the map says "¡".
|
||||
const { mousetrap, target } = freshInstance();
|
||||
mousetrap.setLayoutMap(fullSpanishMap);
|
||||
const calls = [];
|
||||
mousetrap.bind("=", () => calls.push("="));
|
||||
mousetrap.bind("-", () => calls.push("-"));
|
||||
fire(target, makeKeyEvent({ key: "=", code: "Equal" }));
|
||||
fire(target, makeKeyEvent({ key: "-", code: "Minus" }));
|
||||
assert.deepEqual(calls, ["=", "-"]);
|
||||
assert.equal(mousetrap._layoutMap, null);
|
||||
});
|
||||
|
||||
it("drops a disagreeing map before recording", () => {
|
||||
const { mousetrap } = freshInstance();
|
||||
mousetrap.setLayoutMap(fullSpanishMap);
|
||||
assert.equal(mousetrap.keyForRecording(makeKeyEvent({ key: "=", code: "Equal" })), "=");
|
||||
});
|
||||
|
||||
it("keeps a map that agrees with the typed key", () => {
|
||||
const { mousetrap, target } = freshInstance();
|
||||
mousetrap.setLayoutMap(fullSpanishMap);
|
||||
fire(target, makeKeyEvent({ key: "+", code: "BracketRight" }));
|
||||
fire(target, makeKeyEvent({ key: "*", code: "BracketRight", shiftKey: true }));
|
||||
fire(target, makeKeyEvent({ key: "Dead", code: "Quote" }));
|
||||
assert.equal(mousetrap._layoutMap, fullSpanishMap);
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,7 +31,7 @@ describe("modifiers", () => {
|
||||
assert.equal(fired("shift+ctrl+alt+q", event), 1);
|
||||
});
|
||||
|
||||
it("drops the phantom ctrl while AltGraph is active", () => {
|
||||
it("drops the phantom ctrl and alt while AltGraph is active", () => {
|
||||
const altgr = {
|
||||
key: "]",
|
||||
code: "BracketRight",
|
||||
@ -40,7 +40,38 @@ describe("modifiers", () => {
|
||||
getModifierState: (mod) => mod === "AltGraph",
|
||||
};
|
||||
assert.equal(fired("ctrl+alt+]", altgr), 0);
|
||||
assert.equal(fired("alt+]", altgr), 1);
|
||||
assert.equal(fired("alt+]", altgr), 0);
|
||||
assert.equal(fired("]", altgr), 1);
|
||||
});
|
||||
|
||||
it("falls back to ctrl+alt for a letter that AltGr turns into a glyph", () => {
|
||||
// Spanish layout on Windows: Ctrl+Alt+E types "€".
|
||||
const event = {
|
||||
key: "€",
|
||||
code: "KeyE",
|
||||
keyCode: 69,
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
getModifierState: (mod) => mod === "AltGraph",
|
||||
};
|
||||
assert.equal(fired("ctrl+alt+e", event), 1);
|
||||
assert.equal(fired("€", event), 1);
|
||||
});
|
||||
|
||||
it("matches letters and digits by keyCode when a modifier changes the glyph", () => {
|
||||
// Shift+1 types "!" on US and Spanish layouts.
|
||||
assert.equal(fired("shift+1", { key: "!", code: "Digit1", keyCode: 49, shiftKey: true }), 1);
|
||||
// Spanish layout: Shift+0 types "=".
|
||||
assert.equal(fired("shift+0", { key: "=", code: "Digit0", keyCode: 48, shiftKey: true }), 1);
|
||||
// macOS: Option+M types "µ" and Option+E is a dead key.
|
||||
assert.equal(fired("alt+m", { key: "µ", code: "KeyM", keyCode: 77, altKey: true }), 1);
|
||||
assert.equal(fired("alt+e", { key: "Dead", code: "KeyE", keyCode: 69, altKey: true }), 1);
|
||||
assert.equal(fired("alt+shift+5", { key: "%", code: "Digit5", keyCode: 53, altKey: true, shiftKey: true }), 1);
|
||||
});
|
||||
|
||||
it("keeps AZERTY letters by layout, not by position", () => {
|
||||
// AZERTY: the key at the US "W" position types "z" and has keyCode 90.
|
||||
assert.equal(fired("ctrl+z", { key: "z", code: "KeyW", keyCode: 90, ctrlKey: true }), 1);
|
||||
});
|
||||
|
||||
it("keeps a real ctrl+alt without AltGraph", () => {
|
||||
|
||||
@ -54,30 +54,54 @@ describe("canonical key normalization", () => {
|
||||
|
||||
it("resolves a shifted digit row key to the digit itself", () => {
|
||||
// US Shift+0 types ")", AZERTY Shift+0 types "0": shift+0 must mean
|
||||
// the physical digit key on every layout (production keyCode behavior).
|
||||
assert.equal(fired("shift+0", { key: ")", code: "Digit0", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+1", { key: "!", code: "Digit1", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+2", { key: "@", code: "Digit2", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+8", { key: "*", code: "Digit8", shiftKey: true }), 1);
|
||||
// ...so the shifted glyphs are not bindable tokens for digit codes.
|
||||
assert.equal(fired("shift+)", { key: ")", code: "Digit0", shiftKey: true }), 0);
|
||||
assert.equal(fired("shift+*", { key: "*", code: "Digit8", shiftKey: true }), 0);
|
||||
// the physical digit key on every layout, named by keyCode.
|
||||
assert.equal(fired("shift+0", { key: ")", code: "Digit0", keyCode: 48, shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+1", { key: "!", code: "Digit1", keyCode: 49, shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+2", { key: "@", code: "Digit2", keyCode: 50, shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+8", { key: "*", code: "Digit8", keyCode: 56, shiftKey: true }), 1);
|
||||
// AZERTY shifted digits already type the digit: unchanged.
|
||||
assert.equal(fired("shift+0", { key: "0", code: "Digit0", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+0", { key: "0", code: "Digit0", keyCode: 48, shiftKey: true }), 1);
|
||||
// Unshifted digits are untouched.
|
||||
assert.equal(fired("0", { key: "0", code: "Digit0" }), 1);
|
||||
assert.equal(fired("&", { key: "&", code: "Digit1" }), 1);
|
||||
assert.equal(fired("0", { key: "0", code: "Digit0", keyCode: 48 }), 1);
|
||||
});
|
||||
|
||||
it("without a layout map, punctuation resolves by glyph", () => {
|
||||
it("prefers the digit over the shifted glyph when both are bound", () => {
|
||||
const { mousetrap, target } = freshInstance();
|
||||
const calls = [];
|
||||
mousetrap.bind("shift+0", () => calls.push("shift+0"));
|
||||
mousetrap.bind("shift+)", () => calls.push("shift+)"));
|
||||
fire(target, makeKeyEvent({ key: ")", code: "Digit0", keyCode: 48, shiftKey: true }));
|
||||
assert.deepEqual(calls, ["shift+0"]);
|
||||
});
|
||||
|
||||
it("without a layout map, punctuation resolves by US position or by glyph", () => {
|
||||
// Spanish layout: Shift + the physical "+" key (BracketRight) types "*".
|
||||
assert.equal(fired("shift+]", { key: "*", code: "BracketRight", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+*", { key: "*", code: "BracketRight", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+]", { key: "*", code: "BracketRight", shiftKey: true }), 0);
|
||||
assert.equal(fired("+", { key: "+", code: "BracketRight" }), 1);
|
||||
// US layout: Shift + BracketRight types "}".
|
||||
// US layout: Shift + BracketRight types "}"; the glyph still matches.
|
||||
assert.equal(fired("shift+}", { key: "}", code: "BracketRight", shiftKey: true }), 1);
|
||||
assert.equal(fired("shift+]", { key: "}", code: "BracketRight", shiftKey: true }), 0);
|
||||
assert.equal(fired("?", { key: "?", code: "Slash" }), 1);
|
||||
// US layout: Shift + Comma types "<".
|
||||
assert.equal(fired("shift+,", { key: "<", code: "Comma", shiftKey: true }), 1);
|
||||
assert.equal(fired("ctrl+shift+.", { key: ">", code: "Period", ctrlKey: true, shiftKey: true }), 1);
|
||||
});
|
||||
|
||||
it("matches a glyph binding without the Shift the glyph needs", () => {
|
||||
// US layout: "?" is Shift+Slash and "+" is Shift+Equal.
|
||||
assert.equal(fired("?", { key: "?", code: "Slash", shiftKey: true }), 1);
|
||||
assert.equal(fired("+", { key: "+", code: "Equal", shiftKey: true }), 1);
|
||||
// Spanish layout: "?" is Shift+Minus.
|
||||
assert.equal(fired("?", { key: "?", code: "Minus", shiftKey: true }), 1);
|
||||
});
|
||||
|
||||
it("dispatches only the most specific match", () => {
|
||||
const { mousetrap, target } = freshInstance();
|
||||
const calls = [];
|
||||
mousetrap.bind("shift+/", () => calls.push("shift+/"));
|
||||
mousetrap.bind("?", () => calls.push("?"));
|
||||
fire(target, makeKeyEvent({ key: "?", code: "Slash", shiftKey: true }));
|
||||
assert.deepEqual(calls, ["shift+/"]);
|
||||
});
|
||||
|
||||
it("lowercases function keys", () => {
|
||||
@ -107,3 +131,60 @@ describe("canonical key normalization", () => {
|
||||
assert.equal(fired("ñ", { key: "Ñ", code: "Semicolon" }), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyForRecording", () => {
|
||||
const record = (eventOver, layoutMap = null) => {
|
||||
const { mousetrap } = freshInstance();
|
||||
mousetrap.setLayoutMap(layoutMap);
|
||||
return mousetrap.keyForRecording(makeKeyEvent(eventOver));
|
||||
};
|
||||
const spanishMap = { get: (code) => ({ BracketRight: "+", Minus: "'" })[code] };
|
||||
|
||||
it("records letters and digits with a modifier by keyCode", () => {
|
||||
assert.equal(record({ key: "!", code: "Digit1", keyCode: 49, shiftKey: true }), "1");
|
||||
assert.equal(record({ key: "µ", code: "KeyM", keyCode: 77, altKey: true }), "m");
|
||||
assert.equal(record({ key: "Dead", code: "KeyE", keyCode: 69, altKey: true }), "e");
|
||||
});
|
||||
|
||||
it("records punctuation by position when there is a layout map", () => {
|
||||
assert.equal(record({ key: "*", code: "BracketRight", shiftKey: true }, spanishMap), "+");
|
||||
assert.equal(record({ key: "+", code: "BracketRight" }, spanishMap), "+");
|
||||
});
|
||||
|
||||
it("records the typed glyph when there is no layout map", () => {
|
||||
assert.equal(record({ key: "+", code: "BracketRight" }), "+");
|
||||
assert.equal(record({ key: "*", code: "BracketRight", shiftKey: true }), "*");
|
||||
});
|
||||
|
||||
it("falls back to the US position for a dead key without a map", () => {
|
||||
assert.equal(record({ key: "Dead", code: "Quote" }), "'");
|
||||
});
|
||||
|
||||
it("returns null for a modifier pressed alone", () => {
|
||||
assert.equal(record({ key: "Shift", code: "ShiftLeft", shiftKey: true }), null);
|
||||
});
|
||||
|
||||
it("records a key the dispatcher then matches", () => {
|
||||
const cases = [
|
||||
[{ key: "!", code: "Digit1", keyCode: 49, shiftKey: true }, null],
|
||||
[{ key: "µ", code: "KeyM", keyCode: 77, altKey: true }, null],
|
||||
[{ key: "*", code: "BracketRight", shiftKey: true }, null],
|
||||
[{ key: "*", code: "BracketRight", shiftKey: true }, spanishMap],
|
||||
[{ key: "?", code: "Minus", shiftKey: true, ctrlKey: true }, null],
|
||||
[{ key: "?", code: "Minus", shiftKey: true, ctrlKey: true }, spanishMap],
|
||||
];
|
||||
for (const [eventOver, map] of cases) {
|
||||
const { mousetrap, target } = freshInstance();
|
||||
mousetrap.setLayoutMap(map);
|
||||
const event = makeKeyEvent(eventOver);
|
||||
const mods = ["ctrl", "alt", "shift"].filter((m) => event[m + "Key"]);
|
||||
const combo = [...mods, mousetrap.keyForRecording(event)].join("+");
|
||||
let calls = 0;
|
||||
mousetrap.bind(combo.replace(/\+\+$/, "++"), () => {
|
||||
calls++;
|
||||
});
|
||||
fire(target, event);
|
||||
assert.equal(calls, 1, `${combo} should fire`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -280,7 +280,6 @@ lectores
|
||||
letras
|
||||
manifiesto
|
||||
menos
|
||||
navegadores
|
||||
nosotros
|
||||
otras
|
||||
pero
|
||||
|
||||
@ -17,36 +17,18 @@
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
;; Keyboard layout map shared by the shortcut recorder
|
||||
;; (`app.main.ui.shortcuts`) and the Mousetrap dispatcher: punctuation
|
||||
;; resolves by physical position through this map, so recording and
|
||||
;; dispatch use the same token. `nil` until `getLayoutMap` resolves (or
|
||||
;; when the browser has no keyboard API); both sides then fall back to
|
||||
;; the typed glyph, which keeps them consistent with and without a map.
|
||||
(defonce layout-map (atom nil))
|
||||
|
||||
(defn layout-map-supported?
|
||||
"Synchronous keyboard-layout API probe: true when the browser exposes
|
||||
navigator.keyboard.getLayoutMap (Chromium). Punctuation then resolves
|
||||
by physical position; without it both sides fall back to the typed
|
||||
glyph, so Shift+punctuation shortcuts may not fire."
|
||||
([] (layout-map-supported?
|
||||
(when (exists? js/navigator) js/navigator)))
|
||||
([navigator]
|
||||
(let [keyboard (when (some? navigator) (.-keyboard ^js navigator))]
|
||||
(boolean
|
||||
(and (some? keyboard)
|
||||
(exists? (.-getLayoutMap ^js keyboard)))))))
|
||||
|
||||
(let [navigator (when (exists? js/navigator) js/navigator)]
|
||||
(when (layout-map-supported? navigator)
|
||||
(-> (.getLayoutMap ^js (.-keyboard ^js navigator))
|
||||
(.then (fn [m]
|
||||
(cljs.core/reset! layout-map m)
|
||||
(.setLayoutMap mousetrap m))
|
||||
(fn [_]
|
||||
(cljs.core/reset! layout-map nil)
|
||||
(.setLayoutMap mousetrap nil))))))
|
||||
;; Keyboard layout map for Mousetrap: punctuation resolves by physical
|
||||
;; position through it. The shortcut recorder (`app.main.ui.shortcuts`)
|
||||
;; asks Mousetrap for the key it stores, so both use the same map. Absent
|
||||
;; until `getLayoutMap` resolves, and in browsers without the keyboard API
|
||||
;; (Firefox, Safari).
|
||||
(let [navigator (when (exists? js/navigator) js/navigator)
|
||||
keyboard (when (some? navigator) (.-keyboard ^js navigator))]
|
||||
(when (and (some? keyboard)
|
||||
(exists? (.-getLayoutMap ^js keyboard)))
|
||||
(-> (.getLayoutMap ^js keyboard)
|
||||
(.then (fn [m] (.setLayoutMap mousetrap m))
|
||||
(fn [_] (.setLayoutMap mousetrap nil))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Helpers
|
||||
|
||||
@ -55,7 +55,6 @@
|
||||
;; Path tools
|
||||
(dm/export tools/make-curve)
|
||||
(dm/export tools/make-corner)
|
||||
(dm/export tools/add-node)
|
||||
(dm/export tools/remove-node)
|
||||
(dm/export tools/delete-selected)
|
||||
(dm/export tools/delete-selected-with-segments)
|
||||
|
||||
@ -57,14 +57,6 @@
|
||||
:overwrite true
|
||||
:fn #(st/emit! (drp/change-edit-mode :draw))}
|
||||
|
||||
:add-node {:tooltip (ds/shift "+")
|
||||
:label (fn [] (tr "shortcuts.add-node"))
|
||||
:command ["shift++" "shift+="]
|
||||
:show-command "shift++"
|
||||
:subsections [:path-editor]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (drp/add-node))}
|
||||
|
||||
:delete-node {:tooltip (ds/supr)
|
||||
:label (fn [] (tr "shortcuts.delete-node"))
|
||||
:command ["del" "backspace"]
|
||||
|
||||
@ -121,9 +121,6 @@
|
||||
(fn [ht] (reduce #(assoc %1 %2 type) (or ht {}) nodes)))))
|
||||
state)))))
|
||||
|
||||
(defn add-node []
|
||||
(process-path-tool (fn [content points] (path/split-segments content points 0.5))))
|
||||
|
||||
(defn remove-node
|
||||
"Removes nodes and heals the gap with a fitted curve."
|
||||
([]
|
||||
|
||||
@ -679,8 +679,7 @@
|
||||
|
||||
:hide-ui {:tooltip "\\"
|
||||
:label (fn [] (tr "shortcuts.hide-ui"))
|
||||
:command ["\\" "alt+\\" "alt+º"]
|
||||
:show-command "\\"
|
||||
:command "\\"
|
||||
:subsections [:panels :basics]
|
||||
:section [:workspace :basics]
|
||||
:fn #(st/emit! (toggle-layout-flag :hide-ui))}
|
||||
@ -717,8 +716,7 @@
|
||||
|
||||
:zoom-selected {:tooltip (ds/shift "2")
|
||||
:label (fn [] (tr "shortcuts.zoom-selected"))
|
||||
:command ["shift+2" "shift+num2" "shift+'" "shift+\""]
|
||||
:show-command "shift+2"
|
||||
:command ["shift+2" "shift+num2"]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! dw/zoom-to-selected-shape)}
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.settings.import-shortcuts-diff-modal]
|
||||
[app.main.ui.settings.restore-shortcuts-modal]
|
||||
@ -529,10 +528,6 @@
|
||||
[:> icon* {:icon-id i/info
|
||||
:class (stl/css :shortcuts-description-icon)}]
|
||||
(tr "shortcuts.reload-hint")]
|
||||
(when-not (ds/layout-map-supported?)
|
||||
[:> context-notification* {:level :warning
|
||||
:class (stl/css :layout-map-warning)}
|
||||
(tr "shortcuts.layout-map-missing")])
|
||||
|
||||
[:> (mf/provider ctx/shortcuts-ctx) {:value shortcuts-ctx-value}
|
||||
[:div {:class (stl/css :shortcuts-content)}
|
||||
|
||||
@ -314,7 +314,3 @@
|
||||
padding: var(--sp-s) 0;
|
||||
margin-block-end: var(--sp-l);
|
||||
}
|
||||
|
||||
.layout-map-warning {
|
||||
margin-block-end: var(--sp-m);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.shortcuts
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
["@penpot/mousetrap$default" :as mousetrap]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
@ -34,10 +35,6 @@
|
||||
(def ^:private modifier-keys
|
||||
#{"Control" "Shift" "Alt" "Meta"})
|
||||
|
||||
(def ^:private punctuation-codes
|
||||
#{"Minus" "Equal" "BracketLeft" "BracketRight" "Backslash"
|
||||
"Semicolon" "Quote" "Backquote" "Comma" "Period" "Slash"})
|
||||
|
||||
(def ^:private key-name-map
|
||||
{"ArrowUp" "up"
|
||||
"Up" "up"
|
||||
@ -202,71 +199,16 @@
|
||||
(fn? (.-getModifierState native)))
|
||||
(.getModifierState native "AltGraph"))))))
|
||||
|
||||
(defn- numpad-code?
|
||||
[code]
|
||||
(and (string? code)
|
||||
(str/starts-with? code "Numpad")
|
||||
(= 1 (count (subs code 6)))))
|
||||
|
||||
(defn- numpad-alias
|
||||
[code]
|
||||
(let [suffix (subs code 6)]
|
||||
(when (re-matches #"[0-9]" suffix)
|
||||
(str "num" suffix))))
|
||||
|
||||
(defn- punctuation-alias
|
||||
"Resolves punctuation by physical position through the shared layout
|
||||
map (`app.main.data.shortcuts/layout-map`, also pushed into Mousetrap),
|
||||
so recording and dispatch use the same token. Without a map the typed
|
||||
glyph applies, which keeps both sides consistent."
|
||||
[code key]
|
||||
(let [mapped (when (some? @ds/layout-map)
|
||||
(.get ^js @ds/layout-map code))]
|
||||
(if (string? mapped)
|
||||
(str/lower mapped)
|
||||
(str/lower key))))
|
||||
|
||||
(defn- digit-code?
|
||||
[code]
|
||||
(and (string? code)
|
||||
(boolean (re-matches #"Digit[0-9]" code))))
|
||||
|
||||
(defn- digit-alias
|
||||
"Shifted digits resolve to the digit itself: with Shift held, Digit0
|
||||
types ) on US layouts but 0 on AZERTY, so shift+0 must mean the
|
||||
physical digit key everywhere, like shift+z means z."
|
||||
[^js event code key]
|
||||
(when (and (.-shiftKey ^js event)
|
||||
(digit-code? code)
|
||||
(= 1 (count key)))
|
||||
(subs code 5)))
|
||||
|
||||
(defn- normalize-key
|
||||
"Normalizes `event.key` to the lowercase canonical token mousetrap
|
||||
matches on. Letters and unshifted digits resolve by glyph (`a`, not
|
||||
`q` on AZERTY); a digit row key held with Shift resolves to the digit
|
||||
itself (`shift+0`, never `shift+)`); punctuation resolves by physical
|
||||
position through the layout map, so `Shift+*` on a Spanish layout
|
||||
records `shift++`, the primary key the user sees."
|
||||
"Returns the key token Mousetrap matches for `event`, asking Mousetrap
|
||||
itself (`keyForRecording`) so a recorded shortcut always fires: letters
|
||||
and digits with a modifier by keyCode (`shift+1`, `alt+m`), punctuation
|
||||
by position when the browser has a layout map, the typed glyph
|
||||
otherwise."
|
||||
[^js event]
|
||||
(let [native (event->native event)
|
||||
key (.-key ^js event)
|
||||
code (.-code ^js native)]
|
||||
(let [key (.-key event)]
|
||||
(when (and (string? key) (not (contains? modifier-keys key)))
|
||||
(cond
|
||||
;; Numpad digits keep their own `numN` bindings, like mousetrap.
|
||||
(numpad-code? code)
|
||||
(numpad-alias code)
|
||||
|
||||
;; Punctuation: primary key of the physical position.
|
||||
(contains? punctuation-codes code)
|
||||
(punctuation-alias code key)
|
||||
|
||||
:else
|
||||
(or (get key-name-map key)
|
||||
;; Shifted digit row: the digit itself, on every layout.
|
||||
(digit-alias event code key)
|
||||
(str/lower key))))))
|
||||
(.keyForRecording ^js mousetrap (event->native event)))))
|
||||
|
||||
(defn- event->modifier-parts
|
||||
[^js event]
|
||||
@ -281,7 +223,8 @@
|
||||
(cf/check-platform? :macos))
|
||||
(conj "command")
|
||||
|
||||
(.-altKey event)
|
||||
(and (.-altKey event)
|
||||
(not alt-graph?))
|
||||
(conj "alt")
|
||||
|
||||
(.-shiftKey event)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
(ns frontend-tests.ui.settings-shortcuts-test
|
||||
(:require
|
||||
["@penpot/mousetrap$default" :as mousetrap]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard.shortcuts :as dsc]
|
||||
[app.main.data.profile :as du]
|
||||
@ -495,17 +496,25 @@
|
||||
|
||||
(defn- mock-wrapped-keydown
|
||||
"Simulates a goog.events.BrowserEvent-wrapped keydown: `key` and modifiers
|
||||
are forwarded directly, and `code` is only reachable via `getBrowserEvent`.
|
||||
`key` is passed through as-is (not defaulted) so callers can simulate a
|
||||
missing `event.key` with `:key nil` - substituting \"\" would defeat that,
|
||||
since an empty string is truthy in ClojureScript."
|
||||
[{:keys [key code shift ctrl alt meta]}]
|
||||
#js {:key key
|
||||
:shiftKey (boolean shift)
|
||||
:ctrlKey (boolean ctrl)
|
||||
:altKey (boolean alt)
|
||||
:metaKey (boolean meta)
|
||||
:getBrowserEvent (fn [] #js {:code code})})
|
||||
are forwarded directly, and the native event behind `getBrowserEvent`
|
||||
carries every field (`code` and `keyCode` only live there). `key` is
|
||||
passed through as-is (not defaulted) so callers can simulate a missing
|
||||
`event.key` with `:key nil` - substituting \"\" would defeat that, since
|
||||
an empty string is truthy in ClojureScript."
|
||||
[{:keys [key code key-code shift ctrl alt meta]}]
|
||||
(let [native #js {:key key
|
||||
:code code
|
||||
:keyCode key-code
|
||||
:shiftKey (boolean shift)
|
||||
:ctrlKey (boolean ctrl)
|
||||
:altKey (boolean alt)
|
||||
:metaKey (boolean meta)}]
|
||||
#js {:key key
|
||||
:shiftKey (boolean shift)
|
||||
:ctrlKey (boolean ctrl)
|
||||
:altKey (boolean alt)
|
||||
:metaKey (boolean meta)
|
||||
:getBrowserEvent (fn [] native)}))
|
||||
|
||||
(defn- mock-native-keydown
|
||||
"Simulates a plain, unwrapped native KeyboardEvent (no getBrowserEvent),
|
||||
@ -531,7 +540,7 @@
|
||||
even when the typed glyph differs"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(try
|
||||
(reset! ds/layout-map #js {:get (fn [code] (when (= code "Period") "."))})
|
||||
(.setLayoutMap mousetrap #js {:get (fn [code] (when (= code "Period") "."))})
|
||||
(t/is (= "shift+."
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key ">" :code "Period" :shift true}))))
|
||||
@ -539,7 +548,7 @@
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key ":" :code "Period" :shift true}))))
|
||||
(finally
|
||||
(reset! ds/layout-map nil))))))
|
||||
(.setLayoutMap mousetrap nil))))))
|
||||
|
||||
(t/deftest mousetrap-shift-period-on-spanish-iso-layout
|
||||
(t/testing "Spanish (ISO) layout without a layout map: Shift + Period
|
||||
@ -633,28 +642,27 @@
|
||||
(t/deftest mousetrap-shifted-digit-resolves-to-digit
|
||||
(t/testing "a digit row key held with Shift records the digit itself: US
|
||||
Shift+0 types \")\" but records \"shift+0\", so the physical
|
||||
digit key means the same on every layout (production keyCode
|
||||
behavior)"
|
||||
digit key means the same on every layout (named by keyCode)"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(t/is (= "shift+0"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key ")" :code "Digit0" :shift true}))))
|
||||
(mock-wrapped-keydown {:key ")" :code "Digit0" :key-code 48 :shift true}))))
|
||||
(t/is (= "shift+1"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "!" :code "Digit1" :shift true}))))
|
||||
(mock-wrapped-keydown {:key "!" :code "Digit1" :key-code 49 :shift true}))))
|
||||
(t/is (= "shift+2"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "@" :code "Digit2" :shift true}))))
|
||||
(mock-wrapped-keydown {:key "@" :code "Digit2" :key-code 50 :shift true}))))
|
||||
(t/is (= "shift+8"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "*" :code "Digit8" :shift true})))))))
|
||||
(mock-wrapped-keydown {:key "*" :code "Digit8" :key-code 56 :shift true})))))))
|
||||
|
||||
(t/deftest mousetrap-shifted-digit-already-digit-unchanged
|
||||
(t/testing "AZERTY Shift+Digit0 already types \"0\": unchanged"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(t/is (= "shift+0"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "0" :code "Digit0" :shift true})))))))
|
||||
(mock-wrapped-keydown {:key "0" :code "Digit0" :key-code 48 :shift true})))))))
|
||||
|
||||
(t/deftest mousetrap-space-and-named-keys
|
||||
(t/testing "\" \" records as space and named keys stay lowercase"
|
||||
@ -672,15 +680,15 @@
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "Delete" :code "Delete"})))))))
|
||||
|
||||
(t/deftest mousetrap-altgr-drops-phantom-ctrl
|
||||
(t/testing "Spanish AltGr++] types \"]\" with ctrl+alt flags, but the
|
||||
phantom ctrl is dropped so no ctrl+alt shortcut fires"
|
||||
(t/deftest mousetrap-altgr-drops-phantom-modifiers
|
||||
(t/testing "Spanish AltGr+] types \"]\" with ctrl+alt flags, but both
|
||||
phantom modifiers are dropped and it records the glyph"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(let [event #js {:key "]"
|
||||
:shiftKey false :ctrlKey true :altKey true :metaKey false
|
||||
:getModifierState (fn [m] (= m "AltGraph"))
|
||||
:getBrowserEvent (fn [] nil)}]
|
||||
(t/is (= "alt+]"
|
||||
(t/is (= "]"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap event)))))))
|
||||
|
||||
(t/deftest mousetrap-real-ctrl-alt-keeps-both
|
||||
@ -699,7 +707,7 @@
|
||||
what the user sees and what Mousetrap matches"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(try
|
||||
(reset! ds/layout-map #js {:get (fn [code] (when (= code "BracketRight") "+"))})
|
||||
(.setLayoutMap mousetrap #js {:get (fn [code] (when (= code "BracketRight") "+"))})
|
||||
(t/is (= "shift++"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "*" :code "BracketRight" :shift true}))))
|
||||
@ -707,7 +715,7 @@
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "+" :code "BracketRight"}))))
|
||||
(finally
|
||||
(reset! ds/layout-map nil))))))
|
||||
(.setLayoutMap mousetrap nil))))))
|
||||
|
||||
(t/deftest mousetrap-spanish-shift-plus-without-layout-map-records-glyph
|
||||
(t/testing "Spanish layout without a layout map: Shift + the physical
|
||||
@ -725,11 +733,24 @@
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "+" :code "BracketRight"})))))))
|
||||
|
||||
(t/deftest layout-map-supported-matches-keyboard-api
|
||||
(t/testing "false without a navigator or without a keyboard API"
|
||||
(t/is (false? (ds/layout-map-supported? nil)))
|
||||
(t/is (false? (ds/layout-map-supported? #js {})))
|
||||
(t/is (false? (ds/layout-map-supported? #js {:keyboard #js {}}))))
|
||||
(t/testing "true when getLayoutMap exists (Chromium)"
|
||||
(t/is (true? (ds/layout-map-supported?
|
||||
#js {:keyboard #js {:getLayoutMap (fn [] nil)}})))))
|
||||
(t/deftest mousetrap-shift-digit-records-the-digit
|
||||
(t/testing "Shift+1 types \"!\" but records \"shift+1\", the key the user
|
||||
sees and the default zoom shortcuts use"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(t/is (= "shift+1"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "!" :code "Digit1" :key-code 49 :shift true}))))
|
||||
(t/is (= "shift+0"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "=" :code "Digit0" :key-code 48 :shift true})))))))
|
||||
|
||||
(t/deftest mousetrap-option-letter-records-the-letter
|
||||
(t/testing "macOS Option+M types \"µ\" and Option+E is a dead key, but
|
||||
both record the letter"
|
||||
(with-redefs [cf/check-platform? (constantly false)]
|
||||
(t/is (= "alt+m"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "µ" :code "KeyM" :key-code 77 :alt true}))))
|
||||
(t/is (= "alt+e"
|
||||
(#'ui-shortcuts/keyboard-event->mousetrap
|
||||
(mock-wrapped-keydown {:key "Dead" :code "KeyE" :key-code 69 :alt true})))))))
|
||||
|
||||
@ -1727,10 +1727,6 @@ msgstr "مُختلط"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "تعليقات"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "إضافة عقدة"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "محاذاة لأسفل"
|
||||
|
||||
@ -4745,10 +4745,6 @@ msgstr "Selecciona els elements que utilitzen aquest estil"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Comentaris"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Afegeix node"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Alinea a baix"
|
||||
|
||||
@ -2597,10 +2597,6 @@ msgstr "Vyberte položky pomocí tohoto stylu"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komentáře"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Přidat uzel"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Zarovnat dolů"
|
||||
|
||||
@ -3507,10 +3507,6 @@ msgstr "Elemente mit diesem Stil auswählen"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Kommentare"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Punkt hinzufügen"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Unten ausrichten"
|
||||
|
||||
@ -4761,10 +4761,6 @@ msgstr "Select items using this style"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Comments"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Add node"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Align bottom"
|
||||
@ -5041,13 +5037,6 @@ msgstr "Join nodes"
|
||||
msgid "shortcuts.key-combo"
|
||||
msgstr "Press the new keys"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs:484
|
||||
msgid "shortcuts.layout-map-missing"
|
||||
msgstr ""
|
||||
"This browser does not expose the keyboard layout, so shortcuts that "
|
||||
"combine Shift with punctuation (for example Shift+,) may not work here. "
|
||||
"They keep working in Chromium browsers."
|
||||
|
||||
#: src/app/main/data/workspace/text/shortcuts.cljs:249
|
||||
msgid "shortcuts.line-through"
|
||||
msgstr "Toggle line through"
|
||||
|
||||
@ -4815,10 +4815,6 @@ msgstr "Seleccionar elementos que usan este estilo"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Añadir comentarios"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Añadir nodo"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Alinear abajo"
|
||||
@ -5087,13 +5083,6 @@ msgstr "Unir nodos"
|
||||
msgid "shortcuts.key-combo"
|
||||
msgstr "Pulsa las nuevas teclas"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs:484
|
||||
msgid "shortcuts.layout-map-missing"
|
||||
msgstr ""
|
||||
"Este navegador no expone el mapa de teclado, así que los atajos que "
|
||||
"combinan Shift con puntuación (por ejemplo Shift+,) pueden no funcionar "
|
||||
"aquí. Siguen funcionando en navegadores Chromium."
|
||||
|
||||
#: src/app/main/data/workspace/text/shortcuts.cljs:249
|
||||
msgid "shortcuts.line-through"
|
||||
msgstr "Alternar tachado"
|
||||
|
||||
@ -1862,10 +1862,6 @@ msgstr "Batzuk"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Gehitu iruzkinak"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Gehitu nodoa"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Lerrokatu behean"
|
||||
|
||||
@ -4678,10 +4678,6 @@ msgstr "انتخاب موارد با استفاده از این سبک"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "نظرات"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "افزودن گره"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "تراز به پایین"
|
||||
|
||||
@ -4785,10 +4785,6 @@ msgstr "Sélectionnez les éléments qui utilisent ce style"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Ajouter un nœud"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Aligner en bas"
|
||||
|
||||
@ -4509,10 +4509,6 @@ msgstr "Sélectionner les éléments avec ce style"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Ajouter un nœud"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Aligner en bas"
|
||||
|
||||
@ -1957,10 +1957,6 @@ msgstr "gauraya"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Bayyana Ra'ayi"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "kara Girma"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "tsarin kasa"
|
||||
|
||||
@ -3967,10 +3967,6 @@ msgstr "בחירת פריטים שמשתמשים בסגנון הזה"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "הערות"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "הוספת מפרק"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "יישור לתחתית"
|
||||
|
||||
@ -3362,10 +3362,6 @@ msgstr "इस शैली का उपयोग करके आइटम च
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "टिप्पणियाँ"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "नोड जोड़ें"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "नीचे संरेखित करें"
|
||||
|
||||
@ -2596,10 +2596,6 @@ msgstr "Odaberite stavke koristeći ovaj stil"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komentari"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Dodaj čvor"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Poravnaj dno"
|
||||
|
||||
@ -2805,10 +2805,6 @@ msgstr "Pilih item menggunakan gaya ini"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komentar"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Tambahkan simpul"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Sesuaikan ke bawah"
|
||||
|
||||
@ -3875,10 +3875,6 @@ msgstr "Seleziona gli elementi che utilizzano questo stile"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Commenti"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Aggiungi nodo"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Allinea in basso"
|
||||
|
||||
@ -4521,10 +4521,6 @@ msgstr "このスタイルを使用している要素を選択"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "コメント"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "ノードを追加"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "下揃え"
|
||||
|
||||
@ -3200,10 +3200,6 @@ msgstr "이 스타일을 사용하는 항목 선택"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "댓글"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "노드 추가"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "아래 정렬"
|
||||
|
||||
@ -3106,10 +3106,6 @@ msgstr "Atlasīt vienumus, kas izmanto šo stilu"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komentāri"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Pievienot mezglu"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Līdzināt pie apakšas"
|
||||
|
||||
@ -2019,10 +2019,6 @@ msgstr "Bercampur"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komen"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Tambah nota"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Sejajarkan bahagian bawah"
|
||||
|
||||
@ -4755,10 +4755,6 @@ msgstr "Selecteer items in deze stijl"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Commentaar"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Knooppunt toevoegen"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Onderaan uitlijnen"
|
||||
|
||||
@ -1856,10 +1856,6 @@ msgstr "Mieszane"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Komentarze"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Dodaj węzeł"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Wyrównaj do dołu"
|
||||
|
||||
@ -2511,10 +2511,6 @@ msgstr "Misto"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Comentários"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Adicionar ponto"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Alinhar a base"
|
||||
|
||||
@ -2657,10 +2657,6 @@ msgstr "Selecionar itens que usem este estilo"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Comentários"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Adicionar nó"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Alinhar abaixo"
|
||||
|
||||
@ -3189,10 +3189,6 @@ msgstr "Alege elemente folosind acest stil"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Comentarii"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Adaugă nod"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Aliniază jos"
|
||||
|
||||
@ -3227,10 +3227,6 @@ msgstr "Выбрать элементы, использующие этот ст
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Комментарии"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Добавить узел"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Выровнять по нижнему краю"
|
||||
|
||||
@ -2200,10 +2200,6 @@ msgstr "Изаберите ставке користећи овај стил"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Коментари"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Додај чвор"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Поравнај на дно"
|
||||
|
||||
@ -4706,10 +4706,6 @@ msgstr "Välj objekt med denna stil"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Kommentarer"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Lägg till nod"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Justera botten"
|
||||
|
||||
@ -4702,10 +4702,6 @@ msgstr "Bu biçimi kullanan ögeleri seç"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Yorumlar"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Düğüm ekle"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Alta hizala"
|
||||
|
||||
@ -3581,10 +3581,6 @@ msgstr "Оберіть елементи, використовуючи цей с
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Коментарі"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Додати вузол"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Вирівняти по низу"
|
||||
|
||||
@ -1844,10 +1844,6 @@ msgstr "Dàárú"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "Ọ́rọ̀íwòye"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "Fí ípàdé kùn"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "Sópọ sí ísàle"
|
||||
|
||||
@ -3948,10 +3948,6 @@ msgstr "选择使用此样式的项目"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "评论"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "添加节点"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "底部对齐"
|
||||
|
||||
@ -4443,10 +4443,6 @@ msgstr "選取使用此樣式的項目"
|
||||
msgid "shortcuts.add-comment"
|
||||
msgstr "留言"
|
||||
|
||||
#: src/app/main/data/workspace/path/shortcuts.cljs:61
|
||||
msgid "shortcuts.add-node"
|
||||
msgstr "新增節點"
|
||||
|
||||
#: src/app/main/data/workspace/shortcuts.cljs:532
|
||||
msgid "shortcuts.align-bottom"
|
||||
msgstr "靠下對齊"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user