Compare commits

..

1 Commits

7 changed files with 101 additions and 127 deletions
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed macOS Yomitan popup focus after card mining or popup reload while still allowing click-away to close the popup without a hide/reappear cycle.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Kept the visible overlay active while mpv advances to the next playlist item, even when the next episode loads after the warm transition delay.
+12
View File
@@ -51,6 +51,15 @@ function M.create(ctx)
return reason == "reload" or reason == "redirect" return reason == "reload" or reason == "redirect"
end end
local function has_next_playlist_item()
local playlist_count = mp.get_property_number("playlist-count")
local playlist_pos = mp.get_property_number("playlist-pos")
if type(playlist_count) ~= "number" or type(playlist_pos) ~= "number" then
return false
end
return playlist_count > 0 and playlist_pos >= 0 and playlist_pos < playlist_count - 1
end
local function clear_pending_visible_overlay_hide() local function clear_pending_visible_overlay_hide()
local timer = state.pending_visible_overlay_hide_timer local timer = state.pending_visible_overlay_hide_timer
if timer and timer.kill then if timer and timer.kill then
@@ -63,6 +72,9 @@ function M.create(ctx)
local resolve_auto_start_visible_overlay_enabled local resolve_auto_start_visible_overlay_enabled
local function hide_visible_overlay_after_end_file() local function hide_visible_overlay_after_end_file()
if has_next_playlist_item() then
return
end
if state.visible_overlay_requested == true and not resolve_auto_start_visible_overlay_enabled() then if state.visible_overlay_requested == true and not resolve_auto_start_visible_overlay_enabled() then
return return
end end
+46
View File
@@ -69,6 +69,12 @@ local function run_plugin_scenario(config)
if name == "osd-height" then if name == "osd-height" then
return config.osd_height or 720 return config.osd_height or 720
end end
if name == "playlist-count" then
return config.playlist_count
end
if name == "playlist-pos" then
return config.playlist_pos
end
return nil return nil
end end
@@ -627,6 +633,46 @@ do
) )
end end
do
local scenario = {
process_list = "",
defer_timeouts = true,
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
path = "/media/slow-episode-01.mkv",
media_title = "Slow Episode 1",
playlist_count = 2,
playlist_pos = 0,
files = {
[binary_path] = true,
},
}
local recorded, err = run_plugin_scenario(scenario)
assert_true(recorded ~= nil, "plugin failed to load for slow warm playlist visibility scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
recorded.script_messages["subminer-autoplay-ready"]()
fire_event(recorded, "end-file", { reason = "eof" })
fire_pending_timeouts(recorded)
scenario.path = "/media/slow-episode-02.mkv"
scenario.media_title = "Slow Episode 2"
scenario.playlist_pos = 1
fire_event(recorded, "file-loaded")
assert_true(
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 0,
"slow playlist advance should preserve visible overlay state while the next episode is pending"
)
assert_true(
count_start_calls(recorded.async_calls) == 1,
"slow playlist visibility reuse should not issue another --start command"
)
end
do do
local scenario = { local scenario = {
process_list = "", process_list = "",
-9
View File
@@ -5307,15 +5307,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
focusMainWindow: () => { focusMainWindow: () => {
const mainWindow = overlayManager.getMainWindow(); const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
if (process.platform === 'darwin') {
focusMacOSOverlayWindow({
platform: process.platform,
getOverlayWindow: () => mainWindow,
stealAppFocus: () => app.focus({ steal: true }),
warn: (message, details) => logger.warn(message, details),
});
return;
}
if (!mainWindow.isFocused()) { if (!mainWindow.isFocused()) {
mainWindow.focus(); mainWindow.focus();
} }
+36 -107
View File
@@ -842,12 +842,7 @@ test('nested popup close reasserts interactive state and focus when another popu
} }
}); });
function setupYomitanPopupFocusHarness( test('window blur reclaims overlay focus while a yomitan popup remains visible on Windows', async () => {
options: {
isMacOSPlatform?: boolean;
visiblePopupHost?: boolean;
} = {},
) {
const ctx = createMouseTestContext(); const ctx = createMouseTestContext();
const previousWindow = (globalThis as { window?: unknown }).window; const previousWindow = (globalThis as { window?: unknown }).window;
const previousDocument = (globalThis as { document?: unknown }).document; const previousDocument = (globalThis as { document?: unknown }).document;
@@ -860,7 +855,6 @@ function setupYomitanPopupFocusHarness(
let overlayFocusCalls = 0; let overlayFocusCalls = 0;
ctx.platform.shouldToggleMouseIgnore = true; ctx.platform.shouldToggleMouseIgnore = true;
ctx.platform.isMacOSPlatform = options.isMacOSPlatform === true;
(ctx.dom.overlay as { focus?: (options?: { preventScroll?: boolean }) => void }).focus = () => { (ctx.dom.overlay as { focus?: (options?: { preventScroll?: boolean }) => void }).focus = () => {
overlayFocusCalls += 1; overlayFocusCalls += 1;
}; };
@@ -908,8 +902,8 @@ function setupYomitanPopupFocusHarness(
querySelector: () => null, querySelector: () => null,
querySelectorAll: (selector: string) => { querySelectorAll: (selector: string) => {
if ( if (
(options.visiblePopupHost === true && selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR) || selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR ||
(options.visiblePopupHost === true && selector === YOMITAN_POPUP_HOST_SELECTOR) selector === YOMITAN_POPUP_HOST_SELECTOR
) { ) {
return [visiblePopupHost]; return [visiblePopupHost];
} }
@@ -933,111 +927,46 @@ function setupYomitanPopupFocusHarness(
}, },
}); });
const handlers = createMouseHandlers(ctx as never, {
modalStateReader: {
isAnySettingsModalOpen: () => false,
isAnyModalOpen: () => false,
},
applyYPercent: () => {},
getCurrentYPercent: () => 10,
persistSubtitlePositionPatch: () => {},
getSubtitleHoverAutoPauseEnabled: () => false,
getYomitanPopupAutoPauseEnabled: () => false,
getPlaybackPaused: async () => false,
sendMpvCommand: () => {},
});
handlers.setupYomitanObserver();
return {
ctx,
windowListeners,
ignoreCalls,
focusMainWindowCalls: () => focusMainWindowCalls,
windowFocusCalls: () => windowFocusCalls,
overlayFocusCalls: () => overlayFocusCalls,
restore: () => {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: previousDocument,
});
Object.defineProperty(globalThis, 'MutationObserver', {
configurable: true,
value: previousMutationObserver,
});
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
},
};
}
test('window blur reclaims overlay focus while a yomitan popup remains visible on Windows', async () => {
const harness = setupYomitanPopupFocusHarness({ visiblePopupHost: true });
try { try {
assert.equal(harness.ctx.state.yomitanPopupVisible, true); const handlers = createMouseHandlers(ctx as never, {
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true); modalStateReader: {
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]); isAnySettingsModalOpen: () => false,
harness.ignoreCalls.length = 0; isAnyModalOpen: () => false,
},
applyYPercent: () => {},
getCurrentYPercent: () => 10,
persistSubtitlePositionPatch: () => {},
getSubtitleHoverAutoPauseEnabled: () => false,
getYomitanPopupAutoPauseEnabled: () => false,
getPlaybackPaused: async () => false,
sendMpvCommand: () => {},
});
for (const listener of harness.windowListeners.get('blur') ?? []) { handlers.setupYomitanObserver();
assert.equal(ctx.state.yomitanPopupVisible, true);
assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
assert.deepEqual(ignoreCalls, [{ ignore: false, forward: undefined }]);
ignoreCalls.length = 0;
for (const listener of windowListeners.get('blur') ?? []) {
listener(); listener();
} }
await Promise.resolve(); await Promise.resolve();
assert.equal(harness.ctx.state.yomitanPopupVisible, true); assert.equal(ctx.state.yomitanPopupVisible, true);
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true); assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]); assert.deepEqual(ignoreCalls, [{ ignore: false, forward: undefined }]);
assert.equal(harness.focusMainWindowCalls(), 1); assert.equal(focusMainWindowCalls, 1);
assert.equal(harness.windowFocusCalls(), 1); assert.equal(windowFocusCalls, 1);
assert.equal(harness.overlayFocusCalls(), 1); assert.equal(overlayFocusCalls, 1);
} finally { } finally {
harness.restore(); Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
} Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}); Object.defineProperty(globalThis, 'MutationObserver', {
configurable: true,
test('window blur on macOS keeps yomitan popup interactive without stealing click-away focus', async () => { value: previousMutationObserver,
const harness = setupYomitanPopupFocusHarness({ });
isMacOSPlatform: true, Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
visiblePopupHost: true,
});
try {
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
harness.ignoreCalls.length = 0;
for (const listener of harness.windowListeners.get('blur') ?? []) {
listener();
}
await Promise.resolve();
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
assert.equal(harness.focusMainWindowCalls(), 0);
assert.equal(harness.windowFocusCalls(), 0);
assert.equal(harness.overlayFocusCalls(), 0);
} finally {
harness.restore();
}
});
test('popup shown reclaims overlay focus on macOS', () => {
const harness = setupYomitanPopupFocusHarness({ isMacOSPlatform: true });
try {
harness.ignoreCalls.length = 0;
for (const listener of harness.windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
listener();
}
assert.equal(harness.ctx.state.yomitanPopupVisible, true);
assert.equal(harness.ctx.dom.overlay.classList.contains('interactive'), true);
assert.deepEqual(harness.ignoreCalls, [{ ignore: false, forward: undefined }]);
assert.equal(harness.focusMainWindowCalls(), 1);
assert.equal(harness.windowFocusCalls(), 1);
assert.equal(harness.overlayFocusCalls(), 1);
} finally {
harness.restore();
} }
}); });
+3 -7
View File
@@ -67,7 +67,7 @@ export function createMouseHandlers(
if (!ctx.platform.shouldToggleMouseIgnore) { if (!ctx.platform.shouldToggleMouseIgnore) {
return; return;
} }
if (ctx.platform.isLinuxPlatform) { if (ctx.platform.isMacOSPlatform || ctx.platform.isLinuxPlatform) {
return; return;
} }
@@ -467,11 +467,7 @@ export function createMouseHandlers(
reconcilePopupInteraction({ allowPause: true }); reconcilePopupInteraction({ allowPause: true });
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => { window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
reconcilePopupInteraction({ reconcilePopupInteraction({ assumeVisible: true, allowPause: true });
assumeVisible: true,
allowPause: true,
reclaimFocus: ctx.platform.isMacOSPlatform,
});
}); });
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => { window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
@@ -495,7 +491,7 @@ export function createMouseHandlers(
if (typeof document === 'undefined' || document.visibilityState !== 'visible') { if (typeof document === 'undefined' || document.visibilityState !== 'visible') {
return; return;
} }
reconcilePopupInteraction({ reclaimFocus: !ctx.platform.isMacOSPlatform }); reconcilePopupInteraction({ reclaimFocus: true });
}); });
}); });