entangled-worlds-orbiters-shared 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/clock/Transport.d.ts +8 -2
- package/dist/clock/Transport.js +46 -7
- package/dist/clock/musical.js +13 -8
- package/dist/clock/sync.js +15 -2
- package/dist/loaders/glbLoader.js +17 -4
- package/dist/shortcuts/chord.d.ts +35 -0
- package/dist/shortcuts/chord.js +99 -0
- package/dist/shortcuts/entangledWorlds.d.ts +21 -0
- package/dist/shortcuts/entangledWorlds.js +40 -0
- package/dist/shortcuts/format.d.ts +44 -0
- package/dist/shortcuts/format.js +132 -0
- package/dist/shortcuts/index.d.ts +9 -0
- package/dist/shortcuts/index.js +13 -0
- package/dist/shortcuts/labels.d.ts +10 -0
- package/dist/shortcuts/labels.js +130 -0
- package/dist/shortcuts/match.d.ts +65 -0
- package/dist/shortcuts/match.js +145 -0
- package/dist/shortcuts/orbiters.d.ts +55 -0
- package/dist/shortcuts/orbiters.js +109 -0
- package/dist/shortcuts/overrides.d.ts +30 -0
- package/dist/shortcuts/overrides.js +81 -0
- package/dist/shortcuts/sections.d.ts +23 -0
- package/dist/shortcuts/sections.js +19 -0
- package/dist/shortcuts/site.d.ts +9 -0
- package/dist/shortcuts/site.js +25 -0
- package/package.json +5 -1
- package/src/clock/Transport.ts +37 -6
- package/src/clock/musical.ts +18 -8
- package/src/clock/sync.ts +15 -2
- package/src/loaders/glbLoader.ts +15 -6
- package/src/shortcuts/chord.ts +122 -0
- package/src/shortcuts/entangledWorlds.ts +51 -0
- package/src/shortcuts/format.ts +143 -0
- package/src/shortcuts/index.ts +69 -0
- package/src/shortcuts/labels.ts +139 -0
- package/src/shortcuts/match.ts +195 -0
- package/src/shortcuts/orbiters.ts +132 -0
- package/src/shortcuts/overrides.ts +110 -0
- package/src/shortcuts/sections.ts +36 -0
- package/src/shortcuts/site.ts +33 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// A key string for people: how a chord is shown (`⌘⇧Z`, `Ctrl+Shift+Z`, `Q`), and the key string a
|
|
2
|
+
// press makes when someone sets a key ("press a key to change").
|
|
3
|
+
import { isModifierCode, parseChord } from './chord.js';
|
|
4
|
+
/** The platform of this browser, by its own report; `other` outside a browser. */
|
|
5
|
+
export function shortcutPlatform() {
|
|
6
|
+
const nav = globalThis.navigator;
|
|
7
|
+
const name = nav?.userAgentData?.platform ?? nav?.platform ?? '';
|
|
8
|
+
return /mac|iphone|ipad|ipod/i.test(name) ? 'apple' : 'other';
|
|
9
|
+
}
|
|
10
|
+
const NAMED = {
|
|
11
|
+
Space: 'Space', Enter: 'Enter', Escape: 'Esc', Tab: 'Tab', Backspace: 'Backspace', Delete: 'Delete',
|
|
12
|
+
Insert: 'Insert', Home: 'Home', End: 'End', PageUp: 'Page up', PageDown: 'Page down',
|
|
13
|
+
ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',
|
|
14
|
+
Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']', Backslash: '\\', Semicolon: ';', Quote: "'",
|
|
15
|
+
Backquote: '`', Comma: ',', Period: '.', Slash: '/', IntlBackslash: '§',
|
|
16
|
+
NumpadAdd: 'Num +', NumpadSubtract: 'Num -', NumpadMultiply: 'Num *', NumpadDivide: 'Num /',
|
|
17
|
+
NumpadDecimal: 'Num .', NumpadEnter: 'Num Enter',
|
|
18
|
+
};
|
|
19
|
+
/** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
|
|
20
|
+
export function keyLabel(code, layout) {
|
|
21
|
+
const printed = layout?.get(code);
|
|
22
|
+
if (printed && printed.trim())
|
|
23
|
+
return printed.length === 1 ? printed.toUpperCase() : printed;
|
|
24
|
+
if (/^Key[A-Z]$/.test(code))
|
|
25
|
+
return code.slice(3);
|
|
26
|
+
if (/^Digit\d$/.test(code))
|
|
27
|
+
return code.slice(5);
|
|
28
|
+
if (/^Numpad\d$/.test(code))
|
|
29
|
+
return `Num ${code.slice(6)}`;
|
|
30
|
+
return NAMED[code] ?? code;
|
|
31
|
+
}
|
|
32
|
+
const SYMBOLS = { ctrl: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' };
|
|
33
|
+
const WORDS = { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Meta' };
|
|
34
|
+
/**
|
|
35
|
+
* The parts a chord is shown as, one per key to press: modifiers first, in the order the platform
|
|
36
|
+
* writes them, then the key. A modifier the chord merely allows (`Shift?`) is not shown — it is not
|
|
37
|
+
* a key anyone has to press. `null` for a key string that names no chord.
|
|
38
|
+
*/
|
|
39
|
+
export function chordParts(keys, platform = shortcutPlatform(), layout) {
|
|
40
|
+
// A sequence: each key's parts, one after the other (`G`, `H`).
|
|
41
|
+
if (typeof keys === 'string' && keys.includes(' ')) {
|
|
42
|
+
const steps = keys.split(' ').map((step) => (step.includes(' ') ? null : chordParts(step, platform, layout)));
|
|
43
|
+
return steps.length === 2 && steps.every(Boolean) ? steps.flat() : null;
|
|
44
|
+
}
|
|
45
|
+
const chord = parseChord(keys);
|
|
46
|
+
if (!chord)
|
|
47
|
+
return null;
|
|
48
|
+
const held = (rule) => rule === 'held';
|
|
49
|
+
const ctrl = held(chord.ctrl) || (held(chord.mod) && platform === 'other');
|
|
50
|
+
const meta = held(chord.meta) || (held(chord.mod) && platform === 'apple');
|
|
51
|
+
const names = platform === 'apple' ? SYMBOLS : WORDS;
|
|
52
|
+
// Apple's order is ⌃⌥⇧⌘; elsewhere Ctrl, Alt, Shift (and the rare Meta last).
|
|
53
|
+
const parts = [];
|
|
54
|
+
if (ctrl)
|
|
55
|
+
parts.push(names.ctrl);
|
|
56
|
+
if (held(chord.alt))
|
|
57
|
+
parts.push(platform === 'apple' ? names.alt : 'Alt');
|
|
58
|
+
if (held(chord.shift))
|
|
59
|
+
parts.push(names.shift);
|
|
60
|
+
if (meta)
|
|
61
|
+
parts.push(names.meta);
|
|
62
|
+
// The help key is `?` wherever it lets Shift be held (its place is `/`): show what people press.
|
|
63
|
+
parts.push(chord.code === 'Slash' && chord.shift === 'any' && !layout ? '?' : keyLabel(chord.code, layout));
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
/** One line for a chord: `⌘⇧Z` on Apple keyboards, `Ctrl+Shift+Z` elsewhere. */
|
|
67
|
+
export function formatChord(keys, platform = shortcutPlatform(), layout) {
|
|
68
|
+
if (keys.includes(' '))
|
|
69
|
+
return keys.split(' ').map((step) => formatChord(step, platform, layout)).join(' ');
|
|
70
|
+
const parts = chordParts(keys, platform, layout);
|
|
71
|
+
if (!parts)
|
|
72
|
+
return keys;
|
|
73
|
+
return parts.join(platform === 'apple' ? '' : '+');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The key string a press makes, for someone setting a key: exactly the modifiers held, the command
|
|
77
|
+
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. `null`
|
|
78
|
+
* while only modifiers are down, or for a press with no key place.
|
|
79
|
+
*/
|
|
80
|
+
export function chordFromEvent(event, platform = shortcutPlatform()) {
|
|
81
|
+
if (!event.code || isModifierCode(event.code))
|
|
82
|
+
return null;
|
|
83
|
+
const parts = [];
|
|
84
|
+
// ⌘ (Ctrl elsewhere) is written Mod so the key works on both — unless Ctrl and ⌘ are both held,
|
|
85
|
+
// which Mod cannot say: then both by name.
|
|
86
|
+
if (event.ctrlKey && event.metaKey)
|
|
87
|
+
parts.push('Ctrl', 'Meta');
|
|
88
|
+
else if (platform === 'apple' ? event.metaKey : event.ctrlKey)
|
|
89
|
+
parts.push('Mod');
|
|
90
|
+
else if (event.ctrlKey)
|
|
91
|
+
parts.push('Ctrl');
|
|
92
|
+
else if (event.metaKey)
|
|
93
|
+
parts.push('Meta');
|
|
94
|
+
if (event.altKey)
|
|
95
|
+
parts.push('Alt');
|
|
96
|
+
if (event.shiftKey)
|
|
97
|
+
parts.push('Shift');
|
|
98
|
+
parts.push(event.code);
|
|
99
|
+
const keys = parts.join('+');
|
|
100
|
+
return parseChord(keys) ? keys : null;
|
|
101
|
+
}
|
|
102
|
+
const sameModifiers = (a, b) => a.shift === b.shift && a.alt === b.alt && a.ctrl === b.ctrl && a.meta === b.meta && a.mod === b.mod;
|
|
103
|
+
/**
|
|
104
|
+
* The keys of several actions shown as one ("1 2 3", "Q W E", "⌥ 0–9"): when each has exactly one
|
|
105
|
+
* key and they differ only in the last key, the shared modifiers once and then the keys. Otherwise
|
|
106
|
+
* every key of every action, as it is. Read from the keys in force, so a person's own keys show.
|
|
107
|
+
*/
|
|
108
|
+
export function mergedKeyParts(keysPerAction, platform = shortcutPlatform(), layout) {
|
|
109
|
+
const chords = keysPerAction.map((keys) => (keys.length === 1 ? parseChord(keys[0]) : null));
|
|
110
|
+
const first = chords[0];
|
|
111
|
+
if (keysPerAction.length > 1 && first && chords.every((c) => c && sameModifiers(c, first))) {
|
|
112
|
+
const modifiers = (chordParts(keysPerAction[0][0], platform, layout) ?? []).slice(0, -1);
|
|
113
|
+
const labels = chords.map((c) => keyLabel(c.code, layout));
|
|
114
|
+
const digits = labels.join('');
|
|
115
|
+
// The whole number row, in the row's order: one range.
|
|
116
|
+
const tail = digits === '1234567890' ? ['0–9'] : [labels.join(' ')];
|
|
117
|
+
return [[...modifiers, ...tail]];
|
|
118
|
+
}
|
|
119
|
+
return keysPerAction.flatMap((keys) => keys.map((k) => chordParts(k, platform, layout) ?? [k]));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A key someone just pressed for an action, as it is stored: when every default key of the action
|
|
123
|
+
* lets Shift be held (`Shift?`, the play keys that work while selecting several Orbiters) and the
|
|
124
|
+
* press had no Shift, the new key lets it be held too, so the action keeps working the same way.
|
|
125
|
+
*/
|
|
126
|
+
export function recordedKeys(defaultKeys, pressed) {
|
|
127
|
+
const chord = parseChord(pressed);
|
|
128
|
+
if (!chord || chord.shift !== 'free' || defaultKeys.length === 0)
|
|
129
|
+
return pressed;
|
|
130
|
+
const shiftFree = defaultKeys.every((keys) => parseChord(keys)?.shift === 'any');
|
|
131
|
+
return shiftFree ? `Shift?+${pressed}` : pressed;
|
|
132
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { parseChord, parseKeys, keysOverlap, chordMatches, chordsOverlap, isCommandChord, type Chord, type ModifierRule, type ModifierState, } from './chord.js';
|
|
2
|
+
export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, type ShortcutAction, type ShortcutBindings, type ShortcutOverrides, type ShortcutEvent, } from './match.js';
|
|
3
|
+
export { ORBITERS_PLAY_SHORTCUTS, ORBITERS_STUDIO_SHORTCUTS, ORBITERS_PLAY_SECTION, ORBITERS_STUDIO_SECTION, ORBITERS_KEYBOARD_SURFACE_KEYS, ORBITERS_COMMAND_GROUPS, ORBITERS_PLAY_SHEET, type OrbitersShortcut, type OrbitersShortcutScope, } from './orbiters.js';
|
|
4
|
+
export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, type ShortcutPlatform, type KeyLayout, } from './format.js';
|
|
5
|
+
export { SHORTCUT_SECTIONS, SHORTCUT_APPS, type ShortcutSection, type ShortcutAppId } from './sections.js';
|
|
6
|
+
export { SITE_SHORTCUTS, SITE_SECTION, type SiteShortcut } from './site.js';
|
|
7
|
+
export { EW_STUDIO_SHORTCUTS, EW_PLAYER_SHORTCUTS, EW_STUDIO_SECTION, EW_PLAYER_SECTION, EW_STUDIO_COMMAND_GROUPS, type EntangledWorldsShortcut, } from './entangledWorlds.js';
|
|
8
|
+
export { shortcutLabel, shortcutSectionLabel, shortcutName } from './labels.js';
|
|
9
|
+
export { createShortcutOverridesSource, parseOverrides, type ShortcutOverridesSource, type ReadAccountSettings, } from './overrides.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Keyboard shortcuts, one system for every app: each app's catalogue of actions (id, section, label,
|
|
2
|
+
// default keys, scope) and one matcher that says which action a key press is. Keys are named by
|
|
3
|
+
// their place on the keyboard (`KeyboardEvent.code`). Framework-free: no React, no DOM beyond reading
|
|
4
|
+
// a key event.
|
|
5
|
+
export { parseChord, parseKeys, keysOverlap, chordMatches, chordsOverlap, isCommandChord, } from './chord.js';
|
|
6
|
+
export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, } from './match.js';
|
|
7
|
+
export { ORBITERS_PLAY_SHORTCUTS, ORBITERS_STUDIO_SHORTCUTS, ORBITERS_PLAY_SECTION, ORBITERS_STUDIO_SECTION, ORBITERS_KEYBOARD_SURFACE_KEYS, ORBITERS_COMMAND_GROUPS, ORBITERS_PLAY_SHEET, } from './orbiters.js';
|
|
8
|
+
export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, } from './format.js';
|
|
9
|
+
export { SHORTCUT_SECTIONS, SHORTCUT_APPS } from './sections.js';
|
|
10
|
+
export { SITE_SHORTCUTS, SITE_SECTION } from './site.js';
|
|
11
|
+
export { EW_STUDIO_SHORTCUTS, EW_PLAYER_SHORTCUTS, EW_STUDIO_SECTION, EW_PLAYER_SECTION, EW_STUDIO_COMMAND_GROUPS, } from './entangledWorlds.js';
|
|
12
|
+
export { shortcutLabel, shortcutSectionLabel, shortcutName } from './labels.js';
|
|
13
|
+
export { createShortcutOverridesSource, parseOverrides, } from './overrides.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShortcutAction } from './match.js';
|
|
2
|
+
/** An action's name in a language (`es`, `es-AR`), falling back to the catalogue's English. */
|
|
3
|
+
export declare function shortcutLabel(action: Pick<ShortcutAction, 'id' | 'label'>, locale?: string): string;
|
|
4
|
+
/** A section's name in a language, falling back to the English. */
|
|
5
|
+
export declare function shortcutSectionLabel(section: {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}, locale?: string): string;
|
|
9
|
+
/** Any other named thing of the shortcuts (a command group `group:<id>`, a sheet row `row:<id>`). */
|
|
10
|
+
export declare function shortcutName(key: string, english: string, locale?: string): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// What the actions and sections are called in each language the platform speaks, beside the
|
|
2
|
+
// English in the catalogues. One table, read by every app and the settings page, so a name is
|
|
3
|
+
// translated once.
|
|
4
|
+
const ES = Object.freeze({
|
|
5
|
+
"ew.player.playPause": "Reproducir o pausar",
|
|
6
|
+
"ew.studio.commands": "Comandos",
|
|
7
|
+
"ew.studio.copy": "Copiar la selección",
|
|
8
|
+
"ew.studio.duplicate": "Duplicar la selección",
|
|
9
|
+
"ew.studio.help": "Atajos de teclado",
|
|
10
|
+
"ew.studio.paste": "Pegar",
|
|
11
|
+
"ew.studio.playPause": "Reproducir o pausar",
|
|
12
|
+
"ew.studio.redo": "Rehacer",
|
|
13
|
+
"ew.studio.remove": "Quitar la selección",
|
|
14
|
+
"ew.studio.step.animations": "Paso de animaciones",
|
|
15
|
+
"ew.studio.step.audio": "Paso de audio",
|
|
16
|
+
"ew.studio.step.release": "Paso de publicación",
|
|
17
|
+
"ew.studio.step.third": "Paso de exportación (Publicación donde no hay exportación)",
|
|
18
|
+
"ew.studio.undo": "Deshacer",
|
|
19
|
+
"orbiters.play.body.moon": "Luna",
|
|
20
|
+
"orbiters.play.body.star": "Estrella",
|
|
21
|
+
"orbiters.play.body.world": "Mundo",
|
|
22
|
+
"orbiters.play.dimension.1": "Primera dimensión",
|
|
23
|
+
"orbiters.play.dimension.2": "Segunda dimensión",
|
|
24
|
+
"orbiters.play.dimension.3": "Tercera dimensión",
|
|
25
|
+
"orbiters.play.help": "Atajos de teclado",
|
|
26
|
+
"orbiters.play.history.redo": "Rehacer",
|
|
27
|
+
"orbiters.play.history.undo": "Deshacer",
|
|
28
|
+
"orbiters.play.midiLearn": "MIDI learn",
|
|
29
|
+
"orbiters.play.panel.cosmicLfo": "Cosmic LFO",
|
|
30
|
+
"orbiters.play.panel.jamming": "Jamming",
|
|
31
|
+
"orbiters.play.panel.playback": "Reproducción",
|
|
32
|
+
"orbiters.play.panel.sensors": "Sensores",
|
|
33
|
+
"orbiters.play.performanceMode": "Modo rendimiento",
|
|
34
|
+
"orbiters.play.rec.hold": "Grabar mientras se mantiene",
|
|
35
|
+
"orbiters.play.rec.toggle": "Activar o desactivar Rec",
|
|
36
|
+
"orbiters.play.snapshot.recall.0": "Recuperar instantánea 0",
|
|
37
|
+
"orbiters.play.snapshot.recall.1": "Recuperar instantánea 1",
|
|
38
|
+
"orbiters.play.snapshot.recall.2": "Recuperar instantánea 2",
|
|
39
|
+
"orbiters.play.snapshot.recall.3": "Recuperar instantánea 3",
|
|
40
|
+
"orbiters.play.snapshot.recall.4": "Recuperar instantánea 4",
|
|
41
|
+
"orbiters.play.snapshot.recall.5": "Recuperar instantánea 5",
|
|
42
|
+
"orbiters.play.snapshot.recall.6": "Recuperar instantánea 6",
|
|
43
|
+
"orbiters.play.snapshot.recall.7": "Recuperar instantánea 7",
|
|
44
|
+
"orbiters.play.snapshot.recall.8": "Recuperar instantánea 8",
|
|
45
|
+
"orbiters.play.snapshot.recall.9": "Recuperar instantánea 9",
|
|
46
|
+
"orbiters.play.snapshot.store.0": "Guardar instantánea 0",
|
|
47
|
+
"orbiters.play.snapshot.store.1": "Guardar instantánea 1",
|
|
48
|
+
"orbiters.play.snapshot.store.2": "Guardar instantánea 2",
|
|
49
|
+
"orbiters.play.snapshot.store.3": "Guardar instantánea 3",
|
|
50
|
+
"orbiters.play.snapshot.store.4": "Guardar instantánea 4",
|
|
51
|
+
"orbiters.play.snapshot.store.5": "Guardar instantánea 5",
|
|
52
|
+
"orbiters.play.snapshot.store.6": "Guardar instantánea 6",
|
|
53
|
+
"orbiters.play.snapshot.store.7": "Guardar instantánea 7",
|
|
54
|
+
"orbiters.play.snapshot.store.8": "Guardar instantánea 8",
|
|
55
|
+
"orbiters.play.snapshot.store.9": "Guardar instantánea 9",
|
|
56
|
+
"orbiters.play.transport.restart": "Reproducir desde el principio",
|
|
57
|
+
"orbiters.play.transport.toggle": "Reproducir o pausar",
|
|
58
|
+
"orbiters.studio.map": "Mapear",
|
|
59
|
+
"orbiters.studio.redo": "Rehacer",
|
|
60
|
+
"orbiters.studio.undo": "Deshacer",
|
|
61
|
+
"site.commands": "Comandos",
|
|
62
|
+
"site.search": "Buscar",
|
|
63
|
+
"site.go.home": "Inicio",
|
|
64
|
+
"site.go.playground": "Playground",
|
|
65
|
+
"site.go.notifications": "Notificaciones",
|
|
66
|
+
"site.go.audios": "Mis audios",
|
|
67
|
+
"site.go.orbiters": "Mis Orbiters",
|
|
68
|
+
"site.go.worlds": "Mis Entangled Worlds",
|
|
69
|
+
"site.go.collections": "Mis colecciones",
|
|
70
|
+
"site.go.stars": "Mis estrellas",
|
|
71
|
+
"site.new.orbiter": "Nuevo Orbiter",
|
|
72
|
+
"site.new.audio": "Nuevo audio",
|
|
73
|
+
"site.new.world": "Nuevo Entangled World",
|
|
74
|
+
"site.settings": "Abrir la configuración",
|
|
75
|
+
"section:site": "Plantasia Space",
|
|
76
|
+
"app:plantasia-space": "Plantasia Space",
|
|
77
|
+
"app:orbiters": "Orbiters",
|
|
78
|
+
"app:entangled-worlds": "Entangled Worlds",
|
|
79
|
+
"part:orbiters-play": "Tocar",
|
|
80
|
+
"part:orbiters-studio": "Estudio",
|
|
81
|
+
"part:ew-studio": "Estudio",
|
|
82
|
+
"part:ew-player": "Reproductor",
|
|
83
|
+
"section:orbiters-play": "Orbiters: tocar",
|
|
84
|
+
"section:orbiters-studio": "Orbiter Studio",
|
|
85
|
+
"section:ew-studio": "Estudio de Entangled Worlds",
|
|
86
|
+
"section:ew-player": "Reproductor de Entangled Worlds",
|
|
87
|
+
"orbiters.play.screenCapture": "Captura de pantalla",
|
|
88
|
+
"orbiters.play.share": "Compartir",
|
|
89
|
+
"orbiters.play.fullscreen": "Pantalla completa",
|
|
90
|
+
"orbiters.play.commands": "Comandos",
|
|
91
|
+
"group:edit": "Edición",
|
|
92
|
+
"group:playback": "Reproducción",
|
|
93
|
+
"group:steps": "Pasos",
|
|
94
|
+
"group:help": "Ayuda",
|
|
95
|
+
"group:transport": "Transporte",
|
|
96
|
+
"group:view": "Vista",
|
|
97
|
+
"group:snapshots": "Instantáneas",
|
|
98
|
+
"group:panels": "Paneles",
|
|
99
|
+
"group:orbiter": "Orbiter",
|
|
100
|
+
"group:studio": "Estudio",
|
|
101
|
+
"row:playPause": "Reproducir / pausar",
|
|
102
|
+
"row:playFromStart": "Reproducir desde el inicio",
|
|
103
|
+
"row:recHeld": "REC mientras se mantiene",
|
|
104
|
+
"row:recLatch": "REC fijo",
|
|
105
|
+
"row:undo": "Deshacer",
|
|
106
|
+
"row:redo": "Rehacer",
|
|
107
|
+
"row:dimensions": "Dimensión I · II · III",
|
|
108
|
+
"row:bodies": "Mundo · Luna · Estrella",
|
|
109
|
+
"row:snapshotRecall": "Recuperar instantánea",
|
|
110
|
+
"row:snapshotStore": "Guardar instantánea",
|
|
111
|
+
"row:jamming": "Jamming",
|
|
112
|
+
"row:playback": "Reproducción",
|
|
113
|
+
"row:cosmicLfo": "Cosmic LFO",
|
|
114
|
+
"row:sensors": "Sensores",
|
|
115
|
+
"row:midiLearn": "MIDI learn",
|
|
116
|
+
"row:performanceMode": "Modo rendimiento",
|
|
117
|
+
});
|
|
118
|
+
const NAMES = Object.freeze({ es: ES });
|
|
119
|
+
/** An action's name in a language (`es`, `es-AR`), falling back to the catalogue's English. */
|
|
120
|
+
export function shortcutLabel(action, locale) {
|
|
121
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[action.id] ?? action.label;
|
|
122
|
+
}
|
|
123
|
+
/** A section's name in a language, falling back to the English. */
|
|
124
|
+
export function shortcutSectionLabel(section, locale) {
|
|
125
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[`section:${section.id}`] ?? section.label;
|
|
126
|
+
}
|
|
127
|
+
/** Any other named thing of the shortcuts (a command group `group:<id>`, a sheet row `row:<id>`). */
|
|
128
|
+
export function shortcutName(key, english, locale) {
|
|
129
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[key] ?? english;
|
|
130
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ModifierState } from './chord.js';
|
|
2
|
+
/** One thing a key can do, as an app's catalogue declares it. */
|
|
3
|
+
export interface ShortcutAction<Scope extends string = string> {
|
|
4
|
+
/** `<app>.<area>.<action>`, e.g. `orbiters.play.panel.jamming`: the key a person's override is stored under. */
|
|
5
|
+
readonly id: string;
|
|
6
|
+
/** The group it is shown in, one per app surface (`orbiters-play`). Keys clash only inside a section. */
|
|
7
|
+
readonly section: string;
|
|
8
|
+
/** What it does, in sentence case. */
|
|
9
|
+
readonly label: string;
|
|
10
|
+
/** The keys it has until the person changes them (chord strings, see `chord.ts`). */
|
|
11
|
+
readonly defaultKeys: readonly string[];
|
|
12
|
+
/** What it acts on, in the app's own terms (Orbiters: one deck, or this machine). */
|
|
13
|
+
readonly scope: Scope;
|
|
14
|
+
/** Where a command menu lists it (`panels`, `snapshots`); unset, under the section itself. */
|
|
15
|
+
readonly group?: string;
|
|
16
|
+
/** `false`: a key only, never a command to pick from a list (a key held down, the menu itself). */
|
|
17
|
+
readonly command?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The keys of each action, in the order they are tried: the first action a press matches is the one. */
|
|
20
|
+
export type ShortcutBindings = Iterable<readonly [actionId: string, keys: readonly string[]]>;
|
|
21
|
+
/**
|
|
22
|
+
* A person's changes, as their settings store them: an action's own keys (`[]` = no key). An action
|
|
23
|
+
* left out keeps its defaults, so a default changed in code reaches everyone who never touched it.
|
|
24
|
+
*/
|
|
25
|
+
export type ShortcutOverrides = ReadonlyArray<{
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly keys: readonly string[];
|
|
28
|
+
}>;
|
|
29
|
+
/** The key press as the matcher reads it: a `KeyboardEvent` has all of it. */
|
|
30
|
+
export interface ShortcutEvent extends ModifierState {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly target?: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every action's keys: its defaults, or the person's override (unknown actions and keys that name no
|
|
37
|
+
* chord are dropped). Catalogue order is kept, so the earlier catalogue wins where two match one press.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveBindings(catalogue: Iterable<ShortcutAction>, overrides?: ShortcutOverrides): Map<string, readonly string[]>;
|
|
40
|
+
/** Two actions one press could be: `[first, second, the key of the first that clashes]`. */
|
|
41
|
+
export declare function findClashes(bindings: ShortcutBindings): Array<readonly [string, string, string]>;
|
|
42
|
+
/**
|
|
43
|
+
* Take letters away from the shortcuts while a surface is on screen, until the returned release is
|
|
44
|
+
* called. An on-screen keyboard plays from the computer keyboard, and a note key that also switched a
|
|
45
|
+
* panel would take the keyboard away mid-phrase. Keys are `event.key` values, lower-cased (the
|
|
46
|
+
* character, as the keyboard surface reads them).
|
|
47
|
+
*/
|
|
48
|
+
export declare function claimKeys(keys: Iterable<string>): () => void;
|
|
49
|
+
/** Whether a surface holds this key. A key with Ctrl, ⌘ or Option is never claimed (⌘Z stays undo). */
|
|
50
|
+
export declare function isKeyClaimed(event: Pick<ShortcutEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean;
|
|
51
|
+
/** A text field: every key typed there is the field's, ⌘Z included (it undoes the typing). */
|
|
52
|
+
export declare function isTypingTarget(target: unknown): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
|
|
55
|
+
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
|
|
56
|
+
*/
|
|
57
|
+
export declare function matchShortcut(event: ShortcutEvent, bindings: ShortcutBindings): string | null;
|
|
58
|
+
/** How long the second key of a sequence (`G`, then `H`) may follow the first. */
|
|
59
|
+
export declare const SEQUENCE_WAIT_MS = 1500;
|
|
60
|
+
/**
|
|
61
|
+
* A matcher that also knows sequences: it remembers the first key of one for a moment and answers
|
|
62
|
+
* the action when the second follows. One per listener (it holds that moment). A first key answers
|
|
63
|
+
* nothing; any other key, a typed one or a wait past `waitMs` forgets it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createShortcutMatcher(waitMs?: number, now?: () => number): (event: ShortcutEvent, bindings: ShortcutBindings) => string | null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Which action a key press is. Each app keeps what its actions DO; this only answers which one a
|
|
2
|
+
// press names, so every app reads keys the same way: the modifiers, a field being typed in, and a
|
|
3
|
+
// surface that holds letters of its own are decided here, once.
|
|
4
|
+
import { chordMatches, isCommandChord, isModifierCode, keysOverlap, parseChord, parseKeys } from './chord.js';
|
|
5
|
+
/**
|
|
6
|
+
* Every action's keys: its defaults, or the person's override (unknown actions and keys that name no
|
|
7
|
+
* chord are dropped). Catalogue order is kept, so the earlier catalogue wins where two match one press.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveBindings(catalogue, overrides = []) {
|
|
10
|
+
const own = new Map();
|
|
11
|
+
for (const entry of Array.isArray(overrides) ? overrides : []) {
|
|
12
|
+
if (entry && typeof entry.id === 'string' && Array.isArray(entry.keys))
|
|
13
|
+
own.set(entry.id, entry.keys);
|
|
14
|
+
}
|
|
15
|
+
const bindings = new Map();
|
|
16
|
+
for (const action of catalogue) {
|
|
17
|
+
const keys = own.get(action.id) ?? action.defaultKeys;
|
|
18
|
+
bindings.set(action.id, keys.filter((key) => typeof key === 'string' && parseKeys(key) !== null));
|
|
19
|
+
}
|
|
20
|
+
return bindings;
|
|
21
|
+
}
|
|
22
|
+
/** Two actions one press could be: `[first, second, the key of the first that clashes]`. */
|
|
23
|
+
export function findClashes(bindings) {
|
|
24
|
+
const entries = [...bindings];
|
|
25
|
+
const clashes = [];
|
|
26
|
+
entries.forEach(([a, aKeys], i) => {
|
|
27
|
+
for (const [b, bKeys] of entries.slice(i + 1)) {
|
|
28
|
+
const key = aKeys.find((ak) => bKeys.some((bk) => keysOverlap(ak, bk)));
|
|
29
|
+
if (key !== undefined)
|
|
30
|
+
clashes.push([a, b, key]);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return clashes;
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------------------------
|
|
36
|
+
// Surface claims
|
|
37
|
+
const claims = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Take letters away from the shortcuts while a surface is on screen, until the returned release is
|
|
40
|
+
* called. An on-screen keyboard plays from the computer keyboard, and a note key that also switched a
|
|
41
|
+
* panel would take the keyboard away mid-phrase. Keys are `event.key` values, lower-cased (the
|
|
42
|
+
* character, as the keyboard surface reads them).
|
|
43
|
+
*/
|
|
44
|
+
export function claimKeys(keys) {
|
|
45
|
+
const claim = new Set([...keys].map((key) => key.toLowerCase()));
|
|
46
|
+
claims.add(claim);
|
|
47
|
+
return () => {
|
|
48
|
+
claims.delete(claim);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Whether a surface holds this key. A key with Ctrl, ⌘ or Option is never claimed (⌘Z stays undo). */
|
|
52
|
+
export function isKeyClaimed(event) {
|
|
53
|
+
if (!claims.size || typeof event.key !== 'string' || event.metaKey || event.ctrlKey || event.altKey)
|
|
54
|
+
return false;
|
|
55
|
+
const key = event.key.toLowerCase();
|
|
56
|
+
for (const claim of claims)
|
|
57
|
+
if (claim.has(key))
|
|
58
|
+
return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const asTarget = (target) => target !== null && typeof target === 'object' ? target : null;
|
|
62
|
+
const roleOf = (el) => (typeof el.getAttribute === 'function' ? el.getAttribute('role') : null);
|
|
63
|
+
/** Inputs that take no typing: a slider keeps the shortcuts working while it holds focus. */
|
|
64
|
+
const UNTYPED_INPUTS = new Set(['range', 'color', 'file']);
|
|
65
|
+
/** A text field: every key typed there is the field's, ⌘Z included (it undoes the typing). */
|
|
66
|
+
export function isTypingTarget(target) {
|
|
67
|
+
const el = asTarget(target);
|
|
68
|
+
if (!el)
|
|
69
|
+
return false;
|
|
70
|
+
if (el.isContentEditable === true || roleOf(el) === 'textbox')
|
|
71
|
+
return true;
|
|
72
|
+
const tag = typeof el.tagName === 'string' ? el.tagName.toUpperCase() : '';
|
|
73
|
+
if (tag === 'INPUT')
|
|
74
|
+
return !UNTYPED_INPUTS.has(typeof el.type === 'string' ? el.type.toLowerCase() : '');
|
|
75
|
+
return tag === 'TEXTAREA' || tag === 'SELECT';
|
|
76
|
+
}
|
|
77
|
+
/** A widget that reads typed characters (a combobox's type-to-find), though it is no text field. */
|
|
78
|
+
function readsCharacters(target) {
|
|
79
|
+
const el = asTarget(target);
|
|
80
|
+
return el !== null && roleOf(el) === 'combobox';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
|
|
84
|
+
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
|
|
85
|
+
*/
|
|
86
|
+
export function matchShortcut(event, bindings) {
|
|
87
|
+
if (!event.code || isTypingTarget(event.target) || isKeyClaimed(event))
|
|
88
|
+
return null;
|
|
89
|
+
const characters = readsCharacters(event.target);
|
|
90
|
+
for (const [actionId, keys] of bindings) {
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
const chord = parseChord(key);
|
|
93
|
+
if (!chord || !chordMatches(chord, event.code, event))
|
|
94
|
+
continue;
|
|
95
|
+
if (characters && !isCommandChord(chord))
|
|
96
|
+
return null;
|
|
97
|
+
return actionId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------------------------
|
|
103
|
+
// Sequences
|
|
104
|
+
/** How long the second key of a sequence (`G`, then `H`) may follow the first. */
|
|
105
|
+
export const SEQUENCE_WAIT_MS = 1500;
|
|
106
|
+
// The sequences of a set of bindings, read once per bindings object (they change with the overrides).
|
|
107
|
+
const sequencesOf = new WeakMap();
|
|
108
|
+
function sequences(bindings) {
|
|
109
|
+
const known = typeof bindings === 'object' ? sequencesOf.get(bindings) : undefined;
|
|
110
|
+
if (known)
|
|
111
|
+
return known;
|
|
112
|
+
const found = [...bindings].flatMap(([id, keys]) => keys.map((k) => [id, parseKeys(k)]).filter((entry) => entry[1]?.length === 2));
|
|
113
|
+
if (typeof bindings === 'object')
|
|
114
|
+
sequencesOf.set(bindings, found);
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A matcher that also knows sequences: it remembers the first key of one for a moment and answers
|
|
119
|
+
* the action when the second follows. One per listener (it holds that moment). A first key answers
|
|
120
|
+
* nothing; any other key, a typed one or a wait past `waitMs` forgets it.
|
|
121
|
+
*/
|
|
122
|
+
export function createShortcutMatcher(waitMs = SEQUENCE_WAIT_MS, now = Date.now) {
|
|
123
|
+
let first = null;
|
|
124
|
+
return function match(event, bindings) {
|
|
125
|
+
if (event.code && isModifierCode(event.code))
|
|
126
|
+
return null;
|
|
127
|
+
const pending = first && now() - first.at <= waitMs ? first : null;
|
|
128
|
+
first = null;
|
|
129
|
+
if (!event.code || isTypingTarget(event.target) || isKeyClaimed(event))
|
|
130
|
+
return null;
|
|
131
|
+
// A widget that reads characters keeps them: no sequence starts or ends there.
|
|
132
|
+
const characters = readsCharacters(event.target);
|
|
133
|
+
const known = characters ? [] : sequences(bindings);
|
|
134
|
+
if (pending) {
|
|
135
|
+
for (const [id, [one, two]] of known)
|
|
136
|
+
if (chordMatches(one, pending.code, pending) && chordMatches(two, event.code, event))
|
|
137
|
+
return id;
|
|
138
|
+
}
|
|
139
|
+
if (known.some(([, [one]]) => chordMatches(one, event.code, event))) {
|
|
140
|
+
first = { code: event.code, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, at: now() };
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return matchShortcut(event, bindings);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ShortcutAction } from './match.js';
|
|
2
|
+
export type OrbitersShortcutScope = 'voice' | 'client';
|
|
3
|
+
export type OrbitersShortcut = ShortcutAction<OrbitersShortcutScope>;
|
|
4
|
+
export declare const ORBITERS_PLAY_SECTION = "orbiters-play";
|
|
5
|
+
export declare const ORBITERS_STUDIO_SECTION = "orbiters-studio";
|
|
6
|
+
/** How a command menu groups the actions, in its order. */
|
|
7
|
+
export declare const ORBITERS_COMMAND_GROUPS: readonly [{
|
|
8
|
+
readonly id: "transport";
|
|
9
|
+
readonly label: "Transport";
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: "view";
|
|
12
|
+
readonly label: "View";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "snapshots";
|
|
15
|
+
readonly label: "Snapshots";
|
|
16
|
+
}, {
|
|
17
|
+
readonly id: "panels";
|
|
18
|
+
readonly label: "Panels";
|
|
19
|
+
}, {
|
|
20
|
+
readonly id: "orbiter";
|
|
21
|
+
readonly label: "Orbiter";
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "studio";
|
|
24
|
+
readonly label: "Studio";
|
|
25
|
+
}];
|
|
26
|
+
/**
|
|
27
|
+
* Playing an Orbiter. Shift is free on most keys (`Shift?`): it is held to select several decks,
|
|
28
|
+
* and a key pressed while selecting means what it means alone.
|
|
29
|
+
*/
|
|
30
|
+
export declare const ORBITERS_PLAY_SHORTCUTS: readonly OrbitersShortcut[];
|
|
31
|
+
/**
|
|
32
|
+
* The keys sheet (`?`): what to look up at a glance, in three columns. A row may stand for several
|
|
33
|
+
* actions whose keys differ only in the last key ("Dimension I · II · III — 1 2 3"); the keys
|
|
34
|
+
* shown are always this person's (`mergedKeyParts`).
|
|
35
|
+
*/
|
|
36
|
+
export declare const ORBITERS_PLAY_SHEET: readonly Readonly<{
|
|
37
|
+
rows: readonly Readonly<{
|
|
38
|
+
actions: readonly string[];
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
}>[];
|
|
42
|
+
id: string;
|
|
43
|
+
label: string;
|
|
44
|
+
}>[];
|
|
45
|
+
/**
|
|
46
|
+
* The Studio. It is open over play, so the play keys above still work there; where both name a key,
|
|
47
|
+
* the Studio's action is the one.
|
|
48
|
+
*/
|
|
49
|
+
export declare const ORBITERS_STUDIO_SHORTCUTS: readonly OrbitersShortcut[];
|
|
50
|
+
/**
|
|
51
|
+
* The letters the on-screen keyboard plays while it is open (`event.key`, lower-case): the piano row,
|
|
52
|
+
* then Z and X for the octave and C and V for velocity. While it is up they are its notes, and the
|
|
53
|
+
* shortcuts on them wait (`claimKeys`); a settings page says so beside those keys.
|
|
54
|
+
*/
|
|
55
|
+
export declare const ORBITERS_KEYBOARD_SURFACE_KEYS: readonly string[];
|