mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
fix(overlay): forward imported mpv input while Hachidori only claims attention
Hachidori's popup-shown event also fires for any press that may start a selection, so treating it as an open popup blocked imported mpv mouse buttons and routed every key to the popup keybind handler. Once Hachidori is the reader, forwarding, popup key routing and the controller lookup state now require an unhidden popup pane. Wheel events over an in-document dictionary popup stay with the popup. Bump vendor/hachidori so presses outside a text box no longer start a glyph drag that prevented the press.
This commit is contained in:
8 files changed
+73
-12
No files matched your search
@@ -479,7 +479,12 @@ function installKeyboardTestGlobals() {
|
||||
createDictionaryPopupHostTarget: () => {
|
||||
const target = new TestElement();
|
||||
target.closest = (selector: string) =>
|
||||
selector === YOMITAN_POPUP_HOST_SELECTOR ? target : null;
|
||||
selector
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.includes(YOMITAN_POPUP_HOST_SELECTOR)
|
||||
? target
|
||||
: null;
|
||||
return target;
|
||||
},
|
||||
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
|
||||
@@ -1203,7 +1208,13 @@ test('imported mpv wheel bindings forward from the overlay but not over modals',
|
||||
|
||||
testGlobals.dispatchWheel({ deltaY: 120 });
|
||||
testGlobals.dispatchWheel({ deltaY: -120, target: testGlobals.createInteractiveTarget() });
|
||||
// A Hachidori popup scrolls itself; its events arrive retargeted to its host.
|
||||
const popupScrolled = !testGlobals.dispatchWheel({
|
||||
deltaY: -120,
|
||||
target: testGlobals.createDictionaryPopupHostTarget(),
|
||||
});
|
||||
|
||||
assert.equal(popupScrolled, true);
|
||||
assert.deepEqual(testGlobals.mpvCommands, [['keypress', 'WHEEL_DOWN', 1]]);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
@@ -1248,6 +1259,35 @@ test('imported mpv mouse buttons forward only after SubMiner mouse handling', as
|
||||
}
|
||||
});
|
||||
|
||||
test('Hachidori attention without an open popup pane still forwards imported mpv input', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
testGlobals.setGetMpvInputBindings(async () => ({
|
||||
keys: ['0', 'MBTN_LEFT_DBL'],
|
||||
blockedKeys: [],
|
||||
}));
|
||||
await handlers.setupMpvInputForwarding();
|
||||
await wait(0);
|
||||
// Hachidori claims attention for any press that may start a selection.
|
||||
ctx.state.hachidoriReaderSeen = true;
|
||||
ctx.state.yomitanPopupVisible = true;
|
||||
|
||||
testGlobals.dispatchKeydown({ key: '0', code: 'Digit0' });
|
||||
testGlobals.dispatchMousedown({ button: 0 });
|
||||
testGlobals.dispatchMousedown({ button: 0, type: 'mouseup' });
|
||||
|
||||
assert.deepEqual(testGlobals.commandEvents, []);
|
||||
assert.deepEqual(testGlobals.mpvCommands, [
|
||||
['keydown', '0'],
|
||||
['keydown', 'MBTN_LEFT'],
|
||||
['keyup', 'MBTN_LEFT'],
|
||||
]);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { MPV_MOUSE_BUTTON_BY_BUTTON, wheelEventToMpvWheel } from '../../shared/m
|
||||
import { dispatchConfiguredMpvCommand } from '../utils/mpv-command-dispatch';
|
||||
import {
|
||||
registerDictionaryPopupVisibilityListener,
|
||||
HACHIDORI_HOST_SELECTOR,
|
||||
YOMITAN_POPUP_COMMAND_EVENT,
|
||||
YOMITAN_POPUP_HOST_SELECTOR,
|
||||
isLookupPopupOpen,
|
||||
isYomitanPopupVisible,
|
||||
isYomitanPopupIframe,
|
||||
} from '../yomitan-popup.js';
|
||||
@@ -108,11 +110,13 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
// Overlay UI that handles its own mouse input (menus, sidebar, notifications, controls).
|
||||
// Hachidori's popup lives in the overlay document, unlike Yomitan's iframe, so its
|
||||
// wheel events reach these handlers retargeted to its host.
|
||||
function isOverlayControlTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return Boolean(
|
||||
target.closest(
|
||||
'.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea',
|
||||
`.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea, ${YOMITAN_POPUP_HOST_SELECTOR}, ${HACHIDORI_HOST_SELECTOR}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -124,8 +128,7 @@ export function createKeyboardHandlers(
|
||||
!ctx.state.playlistBrowserModalOpen &&
|
||||
!ctx.state.youtubePickerModalOpen &&
|
||||
!ctx.state.subtitleSidebarModalOpen &&
|
||||
!ctx.state.yomitanPopupVisible &&
|
||||
!isYomitanPopupVisible(document) &&
|
||||
!isLookupPopupOpen(ctx.state, document) &&
|
||||
!isInteractiveTarget(target)
|
||||
);
|
||||
}
|
||||
@@ -1262,7 +1265,7 @@ export function createKeyboardHandlers(
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
|
||||
if (isLookupPopupOpen(ctx.state, document)) {
|
||||
if (handleYomitanPopupKeybind(e)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
|
||||
@@ -36,7 +36,6 @@ export function createMouseHandlers(
|
||||
let yomitanPopupVisible = false;
|
||||
let hoverPauseRequestId = 0;
|
||||
let popupPauseRequestId = 0;
|
||||
let hachidoriReaderSeen = false;
|
||||
let pausedBySubtitleHover = false;
|
||||
let pausedByYomitanPopup = false;
|
||||
let lastPointerPosition: { clientX: number; clientY: number } | null = null;
|
||||
@@ -266,7 +265,8 @@ export function createMouseHandlers(
|
||||
return (
|
||||
yomitanPopupVisible &&
|
||||
options.getYomitanPopupAutoPauseEnabled() &&
|
||||
(!hachidoriReaderSeen || (typeof document !== 'undefined' && isHachidoriPopupOpen(document)))
|
||||
(!ctx.state.hachidoriReaderSeen ||
|
||||
(typeof document !== 'undefined' && isHachidoriPopupOpen(document)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ export function createMouseHandlers(
|
||||
reconcilePopupInteraction({ allowPause: true });
|
||||
|
||||
registerDictionaryPopupVisibilityListener('shown', (reader) => {
|
||||
if (reader === 'hachidori') hachidoriReaderSeen = true;
|
||||
if (reader === 'hachidori') ctx.state.hachidoriReaderSeen = true;
|
||||
reconcilePopupInteraction({
|
||||
assumeVisible: true,
|
||||
allowPause: true,
|
||||
|
||||
@@ -61,7 +61,7 @@ import { createOverlayNotificationHistoryPanel } from './overlay-notification-hi
|
||||
import { createRendererState } from './state.js';
|
||||
import { createModalRegistry, type ModalDescriptor } from './modal-registry.js';
|
||||
import { createSubtitleRenderer } from './subtitle-render.js';
|
||||
import { isYomitanPopupVisible, registerYomitanLookupListener } from './yomitan-popup.js';
|
||||
import { isLookupPopupOpen, registerYomitanLookupListener } from './yomitan-popup.js';
|
||||
import {
|
||||
createRendererRecoveryController,
|
||||
registerRendererGlobalErrorHandlers,
|
||||
@@ -457,7 +457,7 @@ function startControllerPolling(): void {
|
||||
profiles: {},
|
||||
},
|
||||
getKeyboardModeEnabled: () => ctx.state.keyboardDrivenModeEnabled,
|
||||
getLookupWindowOpen: () => ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document),
|
||||
getLookupWindowOpen: () => isLookupPopupOpen(ctx.state, document),
|
||||
getInteractionBlocked: () => isControllerInputBlocked(),
|
||||
toggleKeyboardMode: () => keyboardHandlers.handleKeyboardModeToggleRequested(),
|
||||
toggleLookup: () => keyboardHandlers.handleLookupWindowToggleRequested(),
|
||||
|
||||
@@ -161,6 +161,10 @@ export type RendererState = {
|
||||
keyboardSelectionVisible: boolean;
|
||||
keyboardSelectedWordIndex: number | null;
|
||||
yomitanPopupVisible: boolean;
|
||||
// Set once Hachidori's attention events identify it as the reader. Its attention
|
||||
// also covers presses that may start a selection, so yomitanPopupVisible alone
|
||||
// does not mean a popup is open.
|
||||
hachidoriReaderSeen: boolean;
|
||||
isOverYomitanPopup: boolean;
|
||||
primarySubtitleMode: PrimarySubMode;
|
||||
};
|
||||
@@ -296,6 +300,7 @@ export function createRendererState(): RendererState {
|
||||
keyboardSelectionVisible: false,
|
||||
keyboardSelectedWordIndex: null,
|
||||
yomitanPopupVisible: false,
|
||||
hachidoriReaderSeen: false,
|
||||
isOverYomitanPopup: false,
|
||||
primarySubtitleMode: 'visible',
|
||||
};
|
||||
|
||||
@@ -122,6 +122,19 @@ export function isHachidoriPopupOpen(root: ParentNode | null | undefined = docum
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a lookup popup is open, for input that should go to it instead of mpv.
|
||||
* Once Hachidori is the reader only an open pane counts, because its attention
|
||||
* (the shown event and the host's visible marker) also covers a plain press.
|
||||
*/
|
||||
export function isLookupPopupOpen(
|
||||
state: { yomitanPopupVisible: boolean; hachidoriReaderSeen: boolean },
|
||||
root: ParentNode | null | undefined = document,
|
||||
): boolean {
|
||||
if (state.hachidoriReaderSeen) return isHachidoriPopupOpen(root);
|
||||
return state.yomitanPopupVisible || isYomitanPopupVisible(root);
|
||||
}
|
||||
|
||||
export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean {
|
||||
const visiblePopupHosts = queryPopupElements<HTMLElement>(
|
||||
root,
|
||||
|
||||
Reference in new issue
Block a user