mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 15:13:32 -07:00
feat(keybindings): add mouse button support for mpv keybindings (#103)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Fixed settings keybinding capture and runtime handling for mouse buttons, including side-button bindings like `MBTN_BACK` and `MBTN_FORWARD`.
|
||||||
@@ -571,13 +571,15 @@ See `config.example.jsonc` for detailed configuration options and more examples.
|
|||||||
{ "key": "ArrowRight", "command": ["seek", 5] },
|
{ "key": "ArrowRight", "command": ["seek", 5] },
|
||||||
{ "key": "ArrowLeft", "command": ["seek", -5] },
|
{ "key": "ArrowLeft", "command": ["seek", -5] },
|
||||||
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
||||||
|
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
|
||||||
|
{ "key": "MBTN_FORWARD", "command": ["sub-seek", 1] },
|
||||||
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
|
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
|
||||||
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
|
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
|
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`). Mouse buttons use mpv button names: `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`.
|
||||||
|
|
||||||
**Disable a default binding:** Set command to `null`:
|
**Disable a default binding:** Set command to `null`:
|
||||||
|
|
||||||
|
|||||||
@@ -152,9 +152,13 @@ The `keybindings` array overrides or extends the overlay's built-in key handling
|
|||||||
"keybindings": [
|
"keybindings": [
|
||||||
{ "key": "f", "command": ["cycle", "fullscreen"] },
|
{ "key": "f", "command": ["cycle", "fullscreen"] },
|
||||||
{ "key": "m", "command": ["cycle", "mute"] },
|
{ "key": "m", "command": ["cycle", "mute"] },
|
||||||
|
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
|
||||||
|
{ "key": "MBTN_FORWARD", "command": ["sub-seek", 1] },
|
||||||
{ "key": "Space", "command": null }, // disable default Space → pause
|
{ "key": "Space", "command": null }, // disable default Space → pause
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Mouse keybinding names are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`.
|
||||||
|
|
||||||
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ local KEY_NAME_MAP = {
|
|||||||
BracketLeft = "[",
|
BracketLeft = "[",
|
||||||
BracketRight = "]",
|
BracketRight = "]",
|
||||||
Backquote = "`",
|
Backquote = "`",
|
||||||
|
MBTN_LEFT = "MBTN_LEFT",
|
||||||
|
MBTN_MID = "MBTN_MID",
|
||||||
|
MBTN_RIGHT = "MBTN_RIGHT",
|
||||||
|
MBTN_BACK = "MBTN_BACK",
|
||||||
|
MBTN_FORWARD = "MBTN_FORWARD",
|
||||||
}
|
}
|
||||||
|
|
||||||
local MODIFIER_MAP = {
|
local MODIFIER_MAP = {
|
||||||
|
|||||||
@@ -229,6 +229,14 @@ local ctx = {
|
|||||||
actionType = "mpv-command",
|
actionType = "mpv-command",
|
||||||
command = { "quit" },
|
command = { "quit" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key = {
|
||||||
|
code = "MBTN_BACK",
|
||||||
|
modifiers = {},
|
||||||
|
},
|
||||||
|
actionType = "mpv-command",
|
||||||
|
command = { "sub-seek", -1 },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key = {
|
key = {
|
||||||
code = "KeyW",
|
code = "KeyW",
|
||||||
@@ -317,6 +325,7 @@ local expected_mpv_bindings = {
|
|||||||
{ keys = "L", command = { "sub-seek", 1 } },
|
{ keys = "L", command = { "sub-seek", 1 } },
|
||||||
{ keys = "q", command = { "quit" } },
|
{ keys = "q", command = { "quit" } },
|
||||||
{ keys = "Ctrl+w", command = { "quit" } },
|
{ keys = "Ctrl+w", command = { "quit" } },
|
||||||
|
{ keys = "MBTN_BACK", command = { "sub-seek", -1 } },
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, expected in ipairs(expected_mpv_bindings) do
|
for _, expected in ipairs(expected_mpv_bindings) do
|
||||||
|
|||||||
@@ -162,6 +162,46 @@ test('compileSessionBindings resolves CommandOrControl in DOM key strings per pl
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('compileSessionBindings supports mpv mouse button keybindings', () => {
|
||||||
|
const result = compileSessionBindings({
|
||||||
|
shortcuts: createShortcuts(),
|
||||||
|
keybindings: [
|
||||||
|
createKeybinding('MBTN_BACK', ['sub-seek', -1]),
|
||||||
|
createKeybinding('Shift+MBTN_FORWARD', ['sub-seek', 1]),
|
||||||
|
],
|
||||||
|
platform: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result.warnings, []);
|
||||||
|
assert.deepEqual(
|
||||||
|
result.bindings.map((binding) => ({
|
||||||
|
code: binding.key.code,
|
||||||
|
modifiers: binding.key.modifiers,
|
||||||
|
command: binding.actionType === 'mpv-command' ? binding.command : null,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{ code: 'MBTN_BACK', modifiers: [], command: ['sub-seek', -1] },
|
||||||
|
{ code: 'MBTN_FORWARD', modifiers: ['shift'], command: ['sub-seek', 1] },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('compileSessionBindings keeps mouse buttons scoped to keybindings', () => {
|
||||||
|
const result = compileSessionBindings({
|
||||||
|
shortcuts: createShortcuts({
|
||||||
|
openJimaku: 'MBTN_BACK',
|
||||||
|
}),
|
||||||
|
keybindings: [createKeybinding('MBTN_BACK', ['sub-seek', -1])],
|
||||||
|
platform: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result.bindings.map((binding) => binding.sourcePath), ['keybindings[0].key']);
|
||||||
|
assert.deepEqual(
|
||||||
|
result.warnings.map((warning) => `${warning.kind}:${warning.path}`),
|
||||||
|
['unsupported:shortcuts.openJimaku'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('compileSessionBindings drops conflicting bindings that canonicalize to the same key', () => {
|
test('compileSessionBindings drops conflicting bindings that canonicalize to the same key', () => {
|
||||||
const result = compileSessionBindings({
|
const result = compileSessionBindings({
|
||||||
shortcuts: createShortcuts({
|
shortcuts: createShortcuts({
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ type DraftBinding = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MODIFIER_ORDER: SessionKeyModifier[] = ['ctrl', 'alt', 'shift', 'meta'];
|
const MODIFIER_ORDER: SessionKeyModifier[] = ['ctrl', 'alt', 'shift', 'meta'];
|
||||||
|
const MPV_MOUSE_BUTTON_CODES = new Set([
|
||||||
|
'MBTN_LEFT',
|
||||||
|
'MBTN_MID',
|
||||||
|
'MBTN_RIGHT',
|
||||||
|
'MBTN_BACK',
|
||||||
|
'MBTN_FORWARD',
|
||||||
|
]);
|
||||||
|
|
||||||
const SESSION_SHORTCUT_ACTIONS: Array<{
|
const SESSION_SHORTCUT_ACTIONS: Array<{
|
||||||
key: keyof Omit<ConfiguredShortcuts, 'multiCopyTimeoutMs'>;
|
key: keyof Omit<ConfiguredShortcuts, 'multiCopyTimeoutMs'>;
|
||||||
@@ -64,9 +71,18 @@ function isValidCommandEntry(value: unknown): value is string | number {
|
|||||||
return typeof value === 'string' || typeof value === 'number';
|
return typeof value === 'string' || typeof value === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCodeToken(token: string): string | null {
|
function normalizeCodeToken(
|
||||||
|
token: string,
|
||||||
|
options: { allowMouseButtons?: boolean } = {},
|
||||||
|
): string | null {
|
||||||
const normalized = token.trim();
|
const normalized = token.trim();
|
||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
|
if (options.allowMouseButtons === true) {
|
||||||
|
const normalizedMouse = normalized.toUpperCase();
|
||||||
|
if (MPV_MOUSE_BUTTON_CODES.has(normalizedMouse)) {
|
||||||
|
return normalizedMouse;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (/^[a-z]$/i.test(normalized)) {
|
if (/^[a-z]$/i.test(normalized)) {
|
||||||
return `Key${normalized.toUpperCase()}`;
|
return `Key${normalized.toUpperCase()}`;
|
||||||
}
|
}
|
||||||
@@ -238,7 +254,7 @@ function parseDomKeyString(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = normalizeCodeToken(keyToken);
|
const code = normalizeCodeToken(keyToken, { allowMouseButtons: true });
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return {
|
return {
|
||||||
key: null,
|
key: null,
|
||||||
|
|||||||
@@ -322,18 +322,31 @@ function installKeyboardTestGlobals() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchDocumentMouseDown(event: { button: number; target?: unknown }): void {
|
function dispatchMousedown(event: {
|
||||||
|
button: number;
|
||||||
|
ctrlKey?: boolean;
|
||||||
|
metaKey?: boolean;
|
||||||
|
altKey?: boolean;
|
||||||
|
shiftKey?: boolean;
|
||||||
|
target?: unknown;
|
||||||
|
}): void {
|
||||||
const listeners = documentListeners.get('mousedown') ?? [];
|
const listeners = documentListeners.get('mousedown') ?? [];
|
||||||
const mouseEvent = {
|
const mouseEvent = {
|
||||||
button: event.button,
|
button: event.button,
|
||||||
target: event.target ?? null,
|
ctrlKey: event.ctrlKey ?? false,
|
||||||
|
metaKey: event.metaKey ?? false,
|
||||||
|
altKey: event.altKey ?? false,
|
||||||
|
shiftKey: event.shiftKey ?? false,
|
||||||
preventDefault: () => {},
|
preventDefault: () => {},
|
||||||
|
target: event.target ?? null,
|
||||||
};
|
};
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener(mouseEvent);
|
listener(mouseEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dispatchDocumentMouseDown = dispatchMousedown;
|
||||||
|
|
||||||
function dispatchFocusInOnPopup(): void {
|
function dispatchFocusInOnPopup(): void {
|
||||||
const listeners = documentListeners.get('focusin') ?? [];
|
const listeners = documentListeners.get('focusin') ?? [];
|
||||||
const focusEvent = {
|
const focusEvent = {
|
||||||
@@ -389,6 +402,7 @@ function installKeyboardTestGlobals() {
|
|||||||
windowFocusCalls: () => windowFocusCalls,
|
windowFocusCalls: () => windowFocusCalls,
|
||||||
dispatchKeydown,
|
dispatchKeydown,
|
||||||
dispatchDocumentMouseDown,
|
dispatchDocumentMouseDown,
|
||||||
|
dispatchMousedown,
|
||||||
dispatchFocusInOnPopup,
|
dispatchFocusInOnPopup,
|
||||||
dispatchWindowEvent,
|
dispatchWindowEvent,
|
||||||
setPopupVisible: (value: boolean) => {
|
setPopupVisible: (value: boolean) => {
|
||||||
@@ -1036,6 +1050,31 @@ test('paused configured subtitle-jump keybinding re-applies pause after backward
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('configured mouse button keybinding dispatches through overlay mouse handling', async () => {
|
||||||
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
handlers.updateSessionBindings([
|
||||||
|
{
|
||||||
|
sourcePath: 'keybindings[0].key',
|
||||||
|
originalKey: 'MBTN_BACK',
|
||||||
|
key: { code: 'MBTN_BACK', modifiers: [] },
|
||||||
|
actionType: 'mpv-command',
|
||||||
|
command: ['sub-seek', -1],
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
testGlobals.setPlaybackPausedResponse(false);
|
||||||
|
|
||||||
|
testGlobals.dispatchMousedown({ button: 3 });
|
||||||
|
await wait(0);
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands.slice(-1), [['sub-seek', -1]]);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => {
|
test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => {
|
||||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ export function createKeyboardHandlers(
|
|||||||
const CHORD_TIMEOUT_MS = 1000;
|
const CHORD_TIMEOUT_MS = 1000;
|
||||||
const MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS = 50;
|
const MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS = 50;
|
||||||
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
||||||
|
const MOUSE_BUTTON_CODE_BY_BUTTON: Record<number, string> = {
|
||||||
|
0: 'MBTN_LEFT',
|
||||||
|
1: 'MBTN_MID',
|
||||||
|
2: 'MBTN_RIGHT',
|
||||||
|
3: 'MBTN_BACK',
|
||||||
|
4: 'MBTN_FORWARD',
|
||||||
|
};
|
||||||
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
|
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
|
||||||
let pendingLookupRefreshAfterSubtitleSeek = false;
|
let pendingLookupRefreshAfterSubtitleSeek = false;
|
||||||
let resetSelectionToStartOnNextSubtitleSync = false;
|
let resetSelectionToStartOnNextSubtitleSync = false;
|
||||||
@@ -81,6 +88,19 @@ export function createKeyboardHandlers(
|
|||||||
return parts.join('+');
|
return parts.join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mouseEventToString(e: MouseEvent): string | null {
|
||||||
|
const code = MOUSE_BUTTON_CODE_BY_BUTTON[e.button];
|
||||||
|
if (!code) return null;
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (e.ctrlKey) parts.push('Ctrl');
|
||||||
|
if (e.altKey) parts.push('Alt');
|
||||||
|
if (e.shiftKey) parts.push('Shift');
|
||||||
|
if (e.metaKey) parts.push('Meta');
|
||||||
|
parts.push(code);
|
||||||
|
return parts.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
function updateConfiguredShortcuts(
|
function updateConfiguredShortcuts(
|
||||||
shortcuts: Required<ShortcutsConfig>,
|
shortcuts: Required<ShortcutsConfig>,
|
||||||
statsToggleKey?: string,
|
statsToggleKey?: string,
|
||||||
@@ -1216,6 +1236,14 @@ export function createKeyboardHandlers(
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mousedown', (e: MouseEvent) => {
|
document.addEventListener('mousedown', (e: MouseEvent) => {
|
||||||
|
const mouseString = mouseEventToString(e);
|
||||||
|
const binding = mouseString ? ctx.state.sessionBindingMap.get(mouseString) : undefined;
|
||||||
|
if (binding) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatchSessionBinding(binding);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.button === 2 && !isInteractiveTarget(e.target)) {
|
if (e.button === 2 && !isInteractiveTarget(e.target)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void window.electronAPI
|
void window.electronAPI
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ const KEY_NAME_MAP: Record<string, string> = {
|
|||||||
Super: 'Meta',
|
Super: 'Meta',
|
||||||
Meta: 'Meta',
|
Meta: 'Meta',
|
||||||
Backspace: 'Backspace',
|
Backspace: 'Backspace',
|
||||||
|
MBTN_LEFT: 'Mouse Left',
|
||||||
|
MBTN_MID: 'Mouse Middle',
|
||||||
|
MBTN_RIGHT: 'Mouse Right',
|
||||||
|
MBTN_BACK: 'Mouse Back',
|
||||||
|
MBTN_FORWARD: 'Mouse Forward',
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeKeyToken(token: string): string {
|
function normalizeKeyToken(token: string): string {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
buildMpvKeybindingConfigValue,
|
buildMpvKeybindingConfigValue,
|
||||||
createMpvKeybindingRows,
|
createMpvKeybindingRows,
|
||||||
keyboardEventToConfigKey,
|
keyboardEventToConfigKey,
|
||||||
|
mouseEventToConfigKey,
|
||||||
} from './key-input';
|
} from './key-input';
|
||||||
|
|
||||||
test('keyboardEventToConfigKey formats Electron accelerators from learned input', () => {
|
test('keyboardEventToConfigKey formats Electron accelerators from learned input', () => {
|
||||||
@@ -75,6 +76,23 @@ test('keyboardEventToConfigKey formats mpv key bindings from learned input', ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mouseEventToConfigKey formats mpv mouse buttons from learned input', () => {
|
||||||
|
assert.equal(
|
||||||
|
mouseEventToConfigKey(
|
||||||
|
{ button: 3, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false },
|
||||||
|
'dom-code',
|
||||||
|
),
|
||||||
|
'MBTN_BACK',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
mouseEventToConfigKey(
|
||||||
|
{ button: 4, ctrlKey: false, altKey: true, shiftKey: true, metaKey: false },
|
||||||
|
'dom-code',
|
||||||
|
),
|
||||||
|
'Alt+Shift+MBTN_FORWARD',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('MPV keybinding rows save default key moves as a disable plus replacement', () => {
|
test('MPV keybinding rows save default key moves as a disable plus replacement', () => {
|
||||||
const defaults: Keybinding[] = [{ key: 'Space', command: ['cycle', 'pause'] }];
|
const defaults: Keybinding[] = [{ key: 'Space', command: ['cycle', 'pause'] }];
|
||||||
const rows = createMpvKeybindingRows(defaults, []);
|
const rows = createMpvKeybindingRows(defaults, []);
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export interface KeyboardInputLike {
|
|||||||
metaKey: boolean;
|
metaKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MouseInputLike {
|
||||||
|
button: number;
|
||||||
|
ctrlKey: boolean;
|
||||||
|
altKey: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
metaKey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MpvKeybindingRow {
|
export interface MpvKeybindingRow {
|
||||||
defaultKey: string;
|
defaultKey: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -79,6 +87,14 @@ const MPV_KEY_BY_CODE: Record<string, string> = {
|
|||||||
Tab: 'TAB',
|
Tab: 'TAB',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MPV_MOUSE_BUTTON_BY_BUTTON: Record<number, string> = {
|
||||||
|
0: 'MBTN_LEFT',
|
||||||
|
1: 'MBTN_MID',
|
||||||
|
2: 'MBTN_RIGHT',
|
||||||
|
3: 'MBTN_BACK',
|
||||||
|
4: 'MBTN_FORWARD',
|
||||||
|
};
|
||||||
|
|
||||||
function commandEquals(a: Keybinding['command'], b: Keybinding['command']): boolean {
|
function commandEquals(a: Keybinding['command'], b: Keybinding['command']): boolean {
|
||||||
return JSON.stringify(a) === JSON.stringify(b);
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
}
|
}
|
||||||
@@ -152,6 +168,24 @@ export function keyboardEventToConfigKey(
|
|||||||
return [...parts, input.code].join('+');
|
return [...parts, input.code].join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mouseEventToConfigKey(input: MouseInputLike, mode: KeyInputMode): string | null {
|
||||||
|
if (mode !== 'dom-code') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = MPV_MOUSE_BUTTON_BY_BUTTON[input.button];
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (input.ctrlKey) parts.push('Ctrl');
|
||||||
|
if (input.altKey) parts.push('Alt');
|
||||||
|
if (input.shiftKey) parts.push('Shift');
|
||||||
|
if (input.metaKey) parts.push('Meta');
|
||||||
|
return [...parts, key].join('+');
|
||||||
|
}
|
||||||
|
|
||||||
export function createMpvKeybindingRows(
|
export function createMpvKeybindingRows(
|
||||||
defaultBindings: Keybinding[],
|
defaultBindings: Keybinding[],
|
||||||
userBindings: unknown,
|
userBindings: unknown,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { shouldUseLearnedMouseBinding } from './settings-keybinding-controls';
|
||||||
|
|
||||||
|
test('mouse key learning ignores primary left clicks in DOM-code mode', () => {
|
||||||
|
const learnButton = {};
|
||||||
|
const outsideTarget = {};
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
shouldUseLearnedMouseBinding(
|
||||||
|
'MBTN_LEFT',
|
||||||
|
'dom-code',
|
||||||
|
{ button: 0, target: outsideTarget } as MouseEvent,
|
||||||
|
learnButton as HTMLButtonElement,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldUseLearnedMouseBinding(
|
||||||
|
'MBTN_BACK',
|
||||||
|
'dom-code',
|
||||||
|
{ button: 3, target: outsideTarget } as MouseEvent,
|
||||||
|
learnButton as HTMLButtonElement,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mouse key learning still ignores primary learn-button activation', () => {
|
||||||
|
const learnButton = {};
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
shouldUseLearnedMouseBinding(
|
||||||
|
'MBTN_LEFT',
|
||||||
|
'dom-code',
|
||||||
|
{ button: 0, target: learnButton } as MouseEvent,
|
||||||
|
learnButton as HTMLButtonElement,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldUseLearnedMouseBinding(
|
||||||
|
'MBTN_BACK',
|
||||||
|
'dom-code',
|
||||||
|
{ button: 3, target: learnButton } as MouseEvent,
|
||||||
|
learnButton as HTMLButtonElement,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
buildMpvKeybindingConfigValue,
|
buildMpvKeybindingConfigValue,
|
||||||
createMpvKeybindingRows,
|
createMpvKeybindingRows,
|
||||||
keyboardEventToConfigKey,
|
keyboardEventToConfigKey,
|
||||||
|
mouseEventToConfigKey,
|
||||||
parseMpvCommandText,
|
parseMpvCommandText,
|
||||||
type KeyInputMode,
|
type KeyInputMode,
|
||||||
type MpvKeybindingRow,
|
type MpvKeybindingRow,
|
||||||
@@ -18,6 +19,18 @@ export function configureKeybindingControls(options: { requestRender: () => void
|
|||||||
requestRender = options.requestRender;
|
requestRender = options.requestRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldUseLearnedMouseBinding(
|
||||||
|
next: string,
|
||||||
|
mode: KeyInputMode,
|
||||||
|
event: MouseEvent,
|
||||||
|
button: HTMLButtonElement,
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
!(event.target === button && event.button === 0) &&
|
||||||
|
!(mode === 'dom-code' && event.button === 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function startKeyLearning(
|
function startKeyLearning(
|
||||||
button: HTMLButtonElement,
|
button: HTMLButtonElement,
|
||||||
mode: KeyInputMode,
|
mode: KeyInputMode,
|
||||||
@@ -58,6 +71,15 @@ function startKeyLearning(
|
|||||||
};
|
};
|
||||||
onBlur = (): void => stop();
|
onBlur = (): void => stop();
|
||||||
onMouseDown = (event: MouseEvent): void => {
|
onMouseDown = (event: MouseEvent): void => {
|
||||||
|
const next = mouseEventToConfigKey(event, mode);
|
||||||
|
if (next && shouldUseLearnedMouseBinding(next, mode, event, button)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
stop();
|
||||||
|
onValue(next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.target !== button) {
|
if (event.target !== button) {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user