mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 03:13:39 -07:00
refactor(main): extract visible-overlay platform interaction runtime from main.ts
This commit is contained in:
+92
-645
@@ -51,22 +51,7 @@ import {
|
|||||||
resolveLinuxVisibleOverlayWindowModeAction,
|
resolveLinuxVisibleOverlayWindowModeAction,
|
||||||
type LinuxVisibleOverlayWindowMode,
|
type LinuxVisibleOverlayWindowMode,
|
||||||
} from './main/runtime/linux-visible-overlay-window-mode';
|
} from './main/runtime/linux-visible-overlay-window-mode';
|
||||||
import {
|
import { shouldRunLinuxOverlayZOrderKeepAlive } from './main/runtime/linux-overlay-zorder-keepalive';
|
||||||
ensureLinuxOverlayZOrderKeepAliveLoop,
|
|
||||||
shouldRunLinuxOverlayZOrderKeepAlive,
|
|
||||||
tickLinuxOverlayZOrderKeepAlive,
|
|
||||||
} from './main/runtime/linux-overlay-zorder-keepalive';
|
|
||||||
import {
|
|
||||||
applyLinuxOverlayInputShape,
|
|
||||||
applyLinuxOverlayPointerInteractionMousePassthrough,
|
|
||||||
ensureLinuxOverlayPointerInteractionLoop,
|
|
||||||
type ForegroundSuppressionGraceState,
|
|
||||||
mapOverlayMeasurementForPointerInteraction,
|
|
||||||
resolveForegroundSuppressionWithGrace,
|
|
||||||
shouldPrimeLinuxOverlayInteractionFromMeasurement,
|
|
||||||
tickLinuxOverlayPointerInteraction,
|
|
||||||
} from './main/runtime/linux-overlay-pointer-interaction';
|
|
||||||
import { createLinuxX11CursorPointReader } from './main/runtime/linux-x11-cursor-point';
|
|
||||||
import { focusMacOSOverlayWindow } from './main/runtime/macos-overlay-window-focus';
|
import { focusMacOSOverlayWindow } from './main/runtime/macos-overlay-window-focus';
|
||||||
import { restoreMacOSMpvFocusAfterModalClose } from './main/runtime/macos-modal-focus-handoff';
|
import { restoreMacOSMpvFocusAfterModalClose } from './main/runtime/macos-modal-focus-handoff';
|
||||||
import { resolveFreshPlaybackPaused } from './main/runtime/playback-paused-state';
|
import { resolveFreshPlaybackPaused } from './main/runtime/playback-paused-state';
|
||||||
@@ -91,7 +76,7 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { execFile, spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MecabTokenizer } from './mecab-tokenizer';
|
import { MecabTokenizer } from './mecab-tokenizer';
|
||||||
@@ -128,15 +113,7 @@ import {
|
|||||||
resolveDefaultLogFilePath,
|
resolveDefaultLogFilePath,
|
||||||
} from './shared/log-files';
|
} from './shared/log-files';
|
||||||
import { createFatalErrorReporter } from './main/fatal-error';
|
import { createFatalErrorReporter } from './main/fatal-error';
|
||||||
import { createWindowTracker as createWindowTrackerCore } from './window-trackers';
|
import { ensureWindowsOverlayTransparency } from './window-trackers/windows-helper';
|
||||||
import {
|
|
||||||
bindWindowsOverlayAboveMpv,
|
|
||||||
clearWindowsOverlayOwner,
|
|
||||||
ensureWindowsOverlayTransparency,
|
|
||||||
findWindowsMpvTargetWindowHandle,
|
|
||||||
getWindowsForegroundProcessName,
|
|
||||||
setWindowsOverlayOwner,
|
|
||||||
} from './window-trackers/windows-helper';
|
|
||||||
import {
|
import {
|
||||||
commandNeedsOverlayStartupPrereqs,
|
commandNeedsOverlayStartupPrereqs,
|
||||||
commandNeedsOverlayRuntime,
|
commandNeedsOverlayRuntime,
|
||||||
@@ -346,7 +323,6 @@ import {
|
|||||||
setOverlayDebugVisualizationEnabledRuntime,
|
setOverlayDebugVisualizationEnabledRuntime,
|
||||||
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
||||||
showMpvOsdRuntime,
|
showMpvOsdRuntime,
|
||||||
startOverlayWindowTracker as startOverlayWindowTrackerCore,
|
|
||||||
tokenizeSubtitle as tokenizeSubtitleCore,
|
tokenizeSubtitle as tokenizeSubtitleCore,
|
||||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||||
upsertYomitanDictionarySettings,
|
upsertYomitanDictionarySettings,
|
||||||
@@ -503,7 +479,6 @@ import {
|
|||||||
} from './main/jlpt-runtime';
|
} from './main/jlpt-runtime';
|
||||||
import { createMediaRuntimeService } from './main/media-runtime';
|
import { createMediaRuntimeService } from './main/media-runtime';
|
||||||
import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime';
|
import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime';
|
||||||
import { createStatsOverlayVisibilityChangeHandler } from './main/runtime/stats-overlay-visibility';
|
|
||||||
import { createDiscordPresenceRuntime } from './main/runtime/discord-presence-runtime';
|
import { createDiscordPresenceRuntime } from './main/runtime/discord-presence-runtime';
|
||||||
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
|
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
|
||||||
import { createCharacterDictionaryImageLookup } from './main/character-dictionary-runtime/image-lookup';
|
import { createCharacterDictionaryImageLookup } from './main/character-dictionary-runtime/image-lookup';
|
||||||
@@ -519,8 +494,8 @@ import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/characte
|
|||||||
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
|
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
|
||||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||||
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
|
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
|
||||||
import { restoreLinuxOverlayWindowShape } from './main/runtime/linux-overlay-window-shape';
|
|
||||||
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
|
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
|
||||||
|
import { createVisibleOverlayInteractionRuntime } from './main/runtime/visible-overlay-interaction-runtime';
|
||||||
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
||||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||||
import {
|
import {
|
||||||
@@ -545,10 +520,7 @@ import {
|
|||||||
createCreateJellyfinSetupWindowHandler,
|
createCreateJellyfinSetupWindowHandler,
|
||||||
} from './main/runtime/setup-window-factory';
|
} from './main/runtime/setup-window-factory';
|
||||||
import { createConfigSettingsRuntime } from './main/runtime/config-settings-runtime';
|
import { createConfigSettingsRuntime } from './main/runtime/config-settings-runtime';
|
||||||
import {
|
import { shouldSuppressVisibleOverlayRaiseForSeparateWindow } from './main/runtime/settings-window-z-order';
|
||||||
hasLiveSeparateWindow,
|
|
||||||
shouldSuppressVisibleOverlayRaiseForSeparateWindow,
|
|
||||||
} from './main/runtime/settings-window-z-order';
|
|
||||||
import {
|
import {
|
||||||
isSameYoutubeMediaPath,
|
isSameYoutubeMediaPath,
|
||||||
isYoutubeMediaPath,
|
isYoutubeMediaPath,
|
||||||
@@ -1774,7 +1746,6 @@ let linuxVisibleOverlayWindowMode: LinuxVisibleOverlayWindowMode = 'managed';
|
|||||||
let linuxTrackedMpvFullscreen = false;
|
let linuxTrackedMpvFullscreen = false;
|
||||||
let linuxTrackedMpvFullscreenChangedAtMs = 0;
|
let linuxTrackedMpvFullscreenChangedAtMs = 0;
|
||||||
let linuxVisibleOverlayOwnerBindingKey: string | null = null;
|
let linuxVisibleOverlayOwnerBindingKey: string | null = null;
|
||||||
const linuxVisibleOverlayOwnerBindingQueues = new WeakMap<BrowserWindow, Promise<void>>();
|
|
||||||
let linuxVisibleOverlayWindowModeSwitchToken = 0;
|
let linuxVisibleOverlayWindowModeSwitchToken = 0;
|
||||||
let subtitleSidebarRequestedOpen = false;
|
let subtitleSidebarRequestedOpen = false;
|
||||||
const SEEK_THRESHOLD_SECONDS = 3;
|
const SEEK_THRESHOLD_SECONDS = 3;
|
||||||
@@ -2367,14 +2338,18 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
|||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getForceMousePassthrough: () => appState.statsOverlayVisible,
|
getForceMousePassthrough: () => appState.statsOverlayVisible,
|
||||||
getNonNativeInputRegionActive: () =>
|
getNonNativeInputRegionActive: () =>
|
||||||
process.platform === 'linux' && linuxOverlayInputShapeActive,
|
process.platform === 'linux' &&
|
||||||
|
visibleOverlayInteractionRuntime.getLinuxOverlayInputShapeActive(),
|
||||||
getSuspendVisibleOverlay: () => appState.statsOverlayVisible,
|
getSuspendVisibleOverlay: () => appState.statsOverlayVisible,
|
||||||
getOverlayInteractionActive: () => visibleOverlayInteractionActive,
|
getOverlayInteractionActive: () =>
|
||||||
|
visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive(),
|
||||||
getWindowTracker: () => appState.windowTracker,
|
getWindowTracker: () => appState.windowTracker,
|
||||||
getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName,
|
getLastKnownWindowsForegroundProcessName: () =>
|
||||||
|
visibleOverlayInteractionRuntime.getLastWindowsVisibleOverlayForegroundProcessName(),
|
||||||
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
|
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
|
||||||
getWindowsFocusHandoffGraceActive: () => hasWindowsVisibleOverlayFocusHandoffGrace(),
|
getWindowsFocusHandoffGraceActive: () => hasWindowsVisibleOverlayFocusHandoffGrace(),
|
||||||
getMacOSForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
|
getMacOSForegroundProbeActive: () =>
|
||||||
|
visibleOverlayInteractionRuntime.getMacOSVisibleOverlayForegroundProbeActive(),
|
||||||
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
||||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||||
appState.trackerNotReadyWarningShown = shown;
|
appState.trackerNotReadyWarningShown = shown;
|
||||||
@@ -2420,141 +2395,73 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
|||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const;
|
const visibleOverlayInteractionRuntime = createVisibleOverlayInteractionRuntime({
|
||||||
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
overlayManager: {
|
||||||
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500;
|
},
|
||||||
// Ignore transient "neither mpv nor overlay is the active window" blips before suppressing
|
overlayContentMeasurementStore: {
|
||||||
// subtitle pointer interaction. Right after playback starts the overlay can briefly become the
|
clear: (layer) => overlayContentMeasurementStore.clear(layer),
|
||||||
// X11 active window, which would otherwise leave subtitles inert for a poll cycle (~1s).
|
getLatestByLayer: (layer) => overlayContentMeasurementStore.getLatestByLayer(layer),
|
||||||
const LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS = 500;
|
},
|
||||||
const LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS = 1_500;
|
logger: {
|
||||||
const MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS = 1_200;
|
info: (message, ...args) => logger.info(message, ...args),
|
||||||
let visibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
warn: (message, ...args) => logger.warn(message, ...args),
|
||||||
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
debug: (message, ...args) => logger.debug(message, ...args),
|
||||||
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
},
|
||||||
let windowsVisibleOverlayZOrderSyncQueued = false;
|
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||||
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
getModalInputExclusive: () => overlayModalInputState.getModalInputExclusive(),
|
||||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
getStatsOverlayVisible: () => appState.statsOverlayVisible,
|
||||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
setStatsOverlayVisible: (visible) => {
|
||||||
let lastLinuxVisibleOverlayFollowedMpvAtMs = 0;
|
|
||||||
const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = {
|
|
||||||
lossSinceMs: null,
|
|
||||||
};
|
|
||||||
let visibleOverlayInteractionActive = false;
|
|
||||||
let linuxOverlayInputShapeActive = false;
|
|
||||||
let linuxVisibleOverlayStartupInputPrimed = false;
|
|
||||||
let linuxVisibleOverlayStartupInputGraceUntilMs = 0;
|
|
||||||
// Renderer-reported interactive hint (Linux only): true while a Yomitan popup/modal
|
|
||||||
// region is interactive, so the cursor poll keeps the overlay interactive even when the cursor
|
|
||||||
// moves off measured subtitle/sidebar rects onto the popup.
|
|
||||||
let linuxOverlayInteractiveHint = false;
|
|
||||||
let macOSVisibleOverlayForegroundProbeActive = false;
|
|
||||||
let macOSVisibleOverlayForegroundProbeToken = 0;
|
|
||||||
let macOSVisibleOverlayForegroundProbeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({
|
|
||||||
setStatsOverlayVisibleState: (visible) => {
|
|
||||||
appState.statsOverlayVisible = visible;
|
appState.statsOverlayVisible = visible;
|
||||||
},
|
},
|
||||||
resetVisibleOverlayInteraction: () => {
|
getWindowTracker: () => appState.windowTracker,
|
||||||
visibleOverlayInteractionActive = false;
|
setWindowTracker: (tracker) => {
|
||||||
|
appState.windowTracker = tracker;
|
||||||
},
|
},
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
setTrackerNotReadyWarningShown: (shown) => {
|
||||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
appState.trackerNotReadyWarningShown = shown;
|
||||||
|
},
|
||||||
|
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||||
|
getBackendOverride: () => appState.backendOverride,
|
||||||
|
getInitialArgs: () => appState.initialArgs,
|
||||||
|
getOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
getLinuxVisibleOverlayWindowMode: () => linuxVisibleOverlayWindowMode,
|
||||||
|
setLinuxVisibleOverlayOwnerBindingKey: (key) => {
|
||||||
|
linuxVisibleOverlayOwnerBindingKey = key;
|
||||||
|
},
|
||||||
|
bindVisibleOverlayToTrackedX11Window: (window) => bindVisibleOverlayToTrackedX11Window(window),
|
||||||
|
updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry),
|
||||||
|
refreshCurrentSubtitle: () => {
|
||||||
|
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
||||||
|
},
|
||||||
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
|
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||||
|
resetLastOverlayWindowGeometry: () => overlayGeometryRuntime.resetLastOverlayWindowGeometry(),
|
||||||
|
enforceOverlayLayerOrder: () => {
|
||||||
|
enforceOverlayLayerOrder();
|
||||||
|
},
|
||||||
|
getOverlayForegroundSeparateWindows: () => getOverlayForegroundSeparateWindows(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleStatsOverlayVisibilityChanged(visible: boolean): void {
|
||||||
|
visibleOverlayInteractionRuntime.handleStatsOverlayVisibilityChanged(visible);
|
||||||
|
}
|
||||||
|
|
||||||
function resetVisibleOverlayInputState(): void {
|
function resetVisibleOverlayInputState(): void {
|
||||||
visibleOverlayInteractionActive = false;
|
visibleOverlayInteractionRuntime.resetVisibleOverlayInputState();
|
||||||
linuxOverlayInputShapeActive = false;
|
|
||||||
resetLinuxVisibleOverlayStartupInputPrimer();
|
|
||||||
linuxOverlayInteractiveHint = false;
|
|
||||||
overlayContentMeasurementStore.clear('visible');
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (process.platform === 'linux' && mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
restoreLinuxOverlayWindowShape(mainWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreVisibleOverlayWindowShapeForShow(): void {
|
function restoreVisibleOverlayWindowShapeForShow(): void {
|
||||||
if (process.platform !== 'linux') {
|
visibleOverlayInteractionRuntime.restoreVisibleOverlayWindowShapeForShow();
|
||||||
return;
|
|
||||||
}
|
|
||||||
restoreLinuxOverlayWindowShape(overlayManager.getMainWindow());
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearVisibleOverlayBlurRefreshTimeouts(): void {
|
|
||||||
for (const timeout of visibleOverlayBlurRefreshTimeouts) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
visibleOverlayBlurRefreshTimeouts = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
|
|
||||||
for (const timeout of windowsVisibleOverlayZOrderRetryTimeouts) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishMacOSVisibleOverlayForegroundProbe(token: number): void {
|
|
||||||
if (token !== macOSVisibleOverlayForegroundProbeToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
|
||||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
|
||||||
macOSVisibleOverlayForegroundProbeTimeout = null;
|
|
||||||
}
|
|
||||||
if (!macOSVisibleOverlayForegroundProbeActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
macOSVisibleOverlayForegroundProbeActive = false;
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
function startMacOSVisibleOverlayForegroundProbe(): void {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tracker = appState.windowTracker;
|
|
||||||
if (!tracker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
macOSVisibleOverlayForegroundProbeActive = true;
|
|
||||||
const token = ++macOSVisibleOverlayForegroundProbeToken;
|
|
||||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
|
||||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
|
||||||
}
|
|
||||||
macOSVisibleOverlayForegroundProbeTimeout = setTimeout(() => {
|
|
||||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
|
||||||
}, MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS);
|
|
||||||
|
|
||||||
void tracker
|
|
||||||
.refreshNow()
|
|
||||||
.catch((error) => {
|
|
||||||
logger.warn('Failed to refresh macOS frontmost app after overlay blur', error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNativeWindowHandleDecimal(window: BrowserWindow): string {
|
function getNativeWindowHandleDecimal(window: BrowserWindow): string {
|
||||||
const handle = window.getNativeWindowHandle();
|
return visibleOverlayInteractionRuntime.getNativeWindowHandleDecimal(window);
|
||||||
return handle.length >= 8
|
|
||||||
? handle.readBigUInt64LE(0).toString()
|
|
||||||
: BigInt(handle.readUInt32LE(0)).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
|
||||||
return getNativeWindowHandleDecimal(window);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number {
|
function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number {
|
||||||
const handle = window.getNativeWindowHandle();
|
return visibleOverlayInteractionRuntime.getWindowsNativeWindowHandleNumber(window);
|
||||||
return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueueVisibleOverlayX11OwnerBindingOperation(
|
function enqueueVisibleOverlayX11OwnerBindingOperation(
|
||||||
@@ -2562,540 +2469,79 @@ function enqueueVisibleOverlayX11OwnerBindingOperation(
|
|||||||
args: string[],
|
args: string[],
|
||||||
onError?: (error: Error) => void,
|
onError?: (error: Error) => void,
|
||||||
): void {
|
): void {
|
||||||
const previous = linuxVisibleOverlayOwnerBindingQueues.get(window) ?? Promise.resolve();
|
visibleOverlayInteractionRuntime.enqueueVisibleOverlayX11OwnerBindingOperation(
|
||||||
const operation = previous
|
window,
|
||||||
.catch(() => {})
|
args,
|
||||||
.then(
|
onError,
|
||||||
() =>
|
);
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
if (window.isDestroyed()) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
execFile('xprop', args, { timeout: 1500 }, (error) => {
|
|
||||||
if (error) {
|
|
||||||
onError?.(error);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const queued = operation.finally(() => {
|
|
||||||
if (linuxVisibleOverlayOwnerBindingQueues.get(window) === queued) {
|
|
||||||
linuxVisibleOverlayOwnerBindingQueues.delete(window);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
linuxVisibleOverlayOwnerBindingQueues.set(window, queued);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearVisibleOverlayX11OwnerBinding(window: BrowserWindow): void {
|
function clearVisibleOverlayX11OwnerBinding(window: BrowserWindow): void {
|
||||||
if (window.isDestroyed()) return;
|
visibleOverlayInteractionRuntime.clearVisibleOverlayX11OwnerBinding(window);
|
||||||
enqueueVisibleOverlayX11OwnerBindingOperation(window, [
|
|
||||||
'-id',
|
|
||||||
getNativeWindowHandleDecimal(window),
|
|
||||||
'-remove',
|
|
||||||
'WM_TRANSIENT_FOR',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | null): number | null {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (targetMpvSocketPath) {
|
|
||||||
const windowTracker = appState.windowTracker as {
|
|
||||||
getTargetWindowHandle?: () => number | null;
|
|
||||||
} | null;
|
|
||||||
const trackedHandle = windowTracker?.getTargetWindowHandle?.();
|
|
||||||
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
|
||||||
return trackedHandle;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return findWindowsMpvTargetWindowHandle();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindowTracker(override?: string | null, targetMpvSocketPath?: string | null) {
|
function createOverlayWindowTracker(override?: string | null, targetMpvSocketPath?: string | null) {
|
||||||
if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
|
return visibleOverlayInteractionRuntime.createOverlayWindowTracker(override, targetMpvSocketPath);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return createWindowTrackerCore(override, targetMpvSocketPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindVisibleOverlayOwner(): void {
|
function bindVisibleOverlayOwner(): void {
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
visibleOverlayInteractionRuntime.bindVisibleOverlayOwner();
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
bindVisibleOverlayToTrackedX11Window(mainWindow);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (process.platform !== 'win32') return;
|
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
|
||||||
const targetSocketPath = appState.mpvSocketPath;
|
|
||||||
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(targetSocketPath);
|
|
||||||
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (targetSocketPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tracker = appState.windowTracker;
|
|
||||||
const mpvResult = tracker
|
|
||||||
? (() => {
|
|
||||||
try {
|
|
||||||
const win32 =
|
|
||||||
require('./window-trackers/win32') as typeof import('./window-trackers/win32');
|
|
||||||
const poll = win32.findMpvWindows();
|
|
||||||
const focused = poll.matches.find((m) => m.isForeground);
|
|
||||||
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: null;
|
|
||||||
if (!mpvResult) return;
|
|
||||||
if (!setWindowsOverlayOwner(overlayHwnd, mpvResult.hwnd)) {
|
|
||||||
logger.warn('Failed to set overlay owner via koffi');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseVisibleOverlayOwner(): void {
|
function releaseVisibleOverlayOwner(): void {
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
visibleOverlayInteractionRuntime.releaseVisibleOverlayOwner();
|
||||||
if (process.platform === 'linux') {
|
|
||||||
linuxVisibleOverlayOwnerBindingKey = null;
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
clearVisibleOverlayX11OwnerBinding(mainWindow);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
|
||||||
if (!clearWindowsOverlayOwner(overlayHwnd)) {
|
|
||||||
logger.warn('Failed to clear overlay owner via koffi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startOverlayWindowTrackerForCurrentSocket(): void {
|
|
||||||
startOverlayWindowTrackerCore({
|
|
||||||
backendOverride: appState.backendOverride,
|
|
||||||
getMpvSocketPath: () => appState.mpvSocketPath,
|
|
||||||
createWindowTracker: createOverlayWindowTracker,
|
|
||||||
setWindowTracker: (tracker) => {
|
|
||||||
appState.windowTracker = tracker;
|
|
||||||
},
|
|
||||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
|
||||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
|
||||||
refreshCurrentSubtitle: () => {
|
|
||||||
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
|
||||||
},
|
|
||||||
getOverlayWindows: () => getOverlayWindows(),
|
|
||||||
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
|
||||||
bindOverlayOwner: () => bindVisibleOverlayOwner(),
|
|
||||||
releaseOverlayOwner: () => releaseVisibleOverlayOwner(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function retargetOverlayWindowTrackerForMpvSocket(
|
function retargetOverlayWindowTrackerForMpvSocket(
|
||||||
nextSocketPath: string,
|
nextSocketPath: string,
|
||||||
previousSocketPath: string,
|
previousSocketPath: string,
|
||||||
): void {
|
): void {
|
||||||
if (nextSocketPath === previousSocketPath || !appState.overlayRuntimeInitialized) {
|
visibleOverlayInteractionRuntime.retargetOverlayWindowTrackerForMpvSocket(
|
||||||
return;
|
nextSocketPath,
|
||||||
}
|
previousSocketPath,
|
||||||
|
|
||||||
const previousTracker = appState.windowTracker;
|
|
||||||
if (previousTracker) {
|
|
||||||
try {
|
|
||||||
previousTracker.stop();
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Failed to stop previous overlay window tracker before retargeting', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
releaseVisibleOverlayOwner();
|
|
||||||
appState.windowTracker = null;
|
|
||||||
appState.trackerNotReadyWarningShown = false;
|
|
||||||
overlayGeometryRuntime.resetLastOverlayWindowGeometry();
|
|
||||||
startOverlayWindowTrackerForCurrentSocket();
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
overlayShortcutsRuntime.syncOverlayShortcuts();
|
|
||||||
logger.info(
|
|
||||||
`Retargeted overlay window tracker for MPV socket: ${previousSocketPath} -> ${nextSocketPath}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncWindowsVisibleOverlayToMpvZOrder(): Promise<boolean> {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (
|
|
||||||
!mainWindow ||
|
|
||||||
mainWindow.isDestroyed() ||
|
|
||||||
!mainWindow.isVisible() ||
|
|
||||||
!overlayManager.getVisibleOverlayVisible()
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowTracker = appState.windowTracker;
|
|
||||||
if (!windowTracker) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof windowTracker.isTargetWindowMinimized === 'function' &&
|
|
||||||
windowTracker.isTargetWindowMinimized()
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!windowTracker.isTracking() && windowTracker.getGeometry() === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
|
||||||
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath);
|
|
||||||
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
|
||||||
(mainWindow as BrowserWindow & { setOpacity?: (opacity: number) => void }).setOpacity?.(1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestWindowsVisibleOverlayZOrderSync(): void {
|
function requestWindowsVisibleOverlayZOrderSync(): void {
|
||||||
if (process.platform !== 'win32') {
|
visibleOverlayInteractionRuntime.requestWindowsVisibleOverlayZOrderSync();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowsVisibleOverlayZOrderSyncInFlight) {
|
|
||||||
windowsVisibleOverlayZOrderSyncQueued = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
windowsVisibleOverlayZOrderSyncInFlight = true;
|
|
||||||
void syncWindowsVisibleOverlayToMpvZOrder()
|
|
||||||
.catch((error) => {
|
|
||||||
logger.warn('Failed to bind Windows overlay z-order to mpv', error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
windowsVisibleOverlayZOrderSyncInFlight = false;
|
|
||||||
if (!windowsVisibleOverlayZOrderSyncQueued) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
windowsVisibleOverlayZOrderSyncQueued = false;
|
|
||||||
requestWindowsVisibleOverlayZOrderSync();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleWindowsVisibleOverlayZOrderSyncBurst(): void {
|
function scheduleWindowsVisibleOverlayZOrderSyncBurst(): void {
|
||||||
if (process.platform !== 'win32') {
|
visibleOverlayInteractionRuntime.scheduleWindowsVisibleOverlayZOrderSyncBurst();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearWindowsVisibleOverlayZOrderRetryTimeouts();
|
|
||||||
for (const delayMs of WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS) {
|
|
||||||
const retryTimeout = setTimeout(() => {
|
|
||||||
windowsVisibleOverlayZOrderRetryTimeouts = windowsVisibleOverlayZOrderRetryTimeouts.filter(
|
|
||||||
(timeout) => timeout !== retryTimeout,
|
|
||||||
);
|
|
||||||
requestWindowsVisibleOverlayZOrderSync();
|
|
||||||
}, delayMs);
|
|
||||||
windowsVisibleOverlayZOrderRetryTimeouts.push(retryTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasWindowsVisibleOverlayFocusHandoffGrace(): boolean {
|
function hasWindowsVisibleOverlayFocusHandoffGrace(): boolean {
|
||||||
return (
|
return visibleOverlayInteractionRuntime.hasWindowsVisibleOverlayFocusHandoffGrace();
|
||||||
process.platform === 'win32' &&
|
|
||||||
lastWindowsVisibleOverlayBlurredAtMs > 0 &&
|
|
||||||
Date.now() - lastWindowsVisibleOverlayBlurredAtMs <=
|
|
||||||
WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldPollWindowsVisibleOverlayForegroundProcess(): boolean {
|
|
||||||
if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowTracker = appState.windowTracker;
|
|
||||||
if (!windowTracker) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof windowTracker.isTargetWindowMinimized === 'function' &&
|
|
||||||
windowTracker.isTargetWindowMinimized()
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlayFocused = mainWindow.isFocused();
|
|
||||||
const trackerFocused = windowTracker.isTargetWindowFocused?.() ?? false;
|
|
||||||
return !overlayFocused && !trackerFocused;
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybePollWindowsVisibleOverlayForegroundProcess(): void {
|
|
||||||
if (!shouldPollWindowsVisibleOverlayForegroundProcess()) {
|
|
||||||
lastWindowsVisibleOverlayForegroundProcessName = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processName = getWindowsForegroundProcessName();
|
|
||||||
const normalizedProcessName = processName?.trim().toLowerCase() ?? null;
|
|
||||||
const previousProcessName = lastWindowsVisibleOverlayForegroundProcessName;
|
|
||||||
lastWindowsVisibleOverlayForegroundProcessName = normalizedProcessName;
|
|
||||||
|
|
||||||
if (normalizedProcessName !== previousProcessName) {
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
}
|
|
||||||
if (normalizedProcessName === 'mpv' && previousProcessName !== 'mpv') {
|
|
||||||
requestWindowsVisibleOverlayZOrderSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureWindowsVisibleOverlayForegroundPollLoop(): void {
|
|
||||||
if (process.platform !== 'win32' || windowsVisibleOverlayForegroundPollInterval !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
windowsVisibleOverlayForegroundPollInterval = setInterval(() => {
|
|
||||||
maybePollWindowsVisibleOverlayForegroundProcess();
|
|
||||||
}, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearWindowsVisibleOverlayForegroundPollLoop(): void {
|
function clearWindowsVisibleOverlayForegroundPollLoop(): void {
|
||||||
if (windowsVisibleOverlayForegroundPollInterval === null) {
|
visibleOverlayInteractionRuntime.clearWindowsVisibleOverlayForegroundPollLoop();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInterval(windowsVisibleOverlayForegroundPollInterval);
|
|
||||||
windowsVisibleOverlayForegroundPollInterval = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleVisibleOverlayBlurRefresh(): void {
|
function scheduleVisibleOverlayBlurRefresh(): void {
|
||||||
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
visibleOverlayInteractionRuntime.scheduleVisibleOverlayBlurRefresh();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
|
|
||||||
}
|
|
||||||
startMacOSVisibleOverlayForegroundProbe();
|
|
||||||
clearVisibleOverlayBlurRefreshTimeouts();
|
|
||||||
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
|
|
||||||
const refreshTimeout = setTimeout(() => {
|
|
||||||
visibleOverlayBlurRefreshTimeouts = visibleOverlayBlurRefreshTimeouts.filter(
|
|
||||||
(timeout) => timeout !== refreshTimeout,
|
|
||||||
);
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
}, delayMs);
|
|
||||||
visibleOverlayBlurRefreshTimeouts.push(refreshTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureWindowsVisibleOverlayForegroundPollLoop();
|
|
||||||
|
|
||||||
const linuxX11CursorPointReader = createLinuxX11CursorPointReader();
|
|
||||||
|
|
||||||
function getLinuxOverlayPointerMeasurement() {
|
|
||||||
const measurement = overlayContentMeasurementStore.getLatestByLayer('visible');
|
|
||||||
return mapOverlayMeasurementForPointerInteraction(measurement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldSuspendLinuxOverlayPointerInteraction(): boolean {
|
|
||||||
return overlayModalInputState.getModalInputExclusive() || appState.statsOverlayVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldSuppressLinuxOverlayPointerInteraction(): boolean {
|
|
||||||
return resolveForegroundSuppressionWithGrace({
|
|
||||||
hasForegroundSeparateWindow: hasLiveSeparateWindow(getOverlayForegroundSeparateWindows()),
|
|
||||||
isTrackingMpvWindow: Boolean(appState.windowTracker?.isTracking()),
|
|
||||||
isMpvWindowFocused: appState.windowTracker?.isTargetWindowFocused?.() !== false,
|
|
||||||
isOverlayWindowFocused: overlayManager.getMainWindow()?.isFocused() === true,
|
|
||||||
nowMs: Date.now(),
|
|
||||||
graceMs: LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS,
|
|
||||||
state: linuxPointerForegroundSuppressionGrace,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldUseLinuxOverlayInputShape(): boolean {
|
|
||||||
// Electron's setShape is a *bounding* shape: outside the given rects no pixels are drawn, so
|
|
||||||
// it clips the visible subtitle (and makes a dragged subtitle vanish behind the shaped
|
|
||||||
// region). There is no input-only region API on Linux, so selective hit-testing is handled by
|
|
||||||
// the main-process cursor poll instead. Keep this off to avoid clipping the overlay.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasLinuxVisibleOverlayStartupInputGrace(): boolean {
|
|
||||||
return (
|
|
||||||
process.platform === 'linux' &&
|
|
||||||
linuxVisibleOverlayStartupInputGraceUntilMs > 0 &&
|
|
||||||
Date.now() < linuxVisibleOverlayStartupInputGraceUntilMs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLinuxVisibleOverlayStartupInputGrace(): void {
|
|
||||||
linuxVisibleOverlayStartupInputGraceUntilMs = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetLinuxVisibleOverlayStartupInputPrimer(): void {
|
function resetLinuxVisibleOverlayStartupInputPrimer(): void {
|
||||||
linuxVisibleOverlayStartupInputPrimed = false;
|
visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();
|
||||||
clearLinuxVisibleOverlayStartupInputGrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLinuxOverlayInputShapeFromLatestMeasurement(): boolean {
|
function applyLinuxOverlayInputShapeFromLatestMeasurement(): boolean {
|
||||||
if (!shouldUseLinuxOverlayInputShape()) {
|
return visibleOverlayInteractionRuntime.applyLinuxOverlayInputShapeFromLatestMeasurement();
|
||||||
linuxOverlayInputShapeActive = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = applyLinuxOverlayInputShape({
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
|
||||||
getRendererInteractiveHint: () => linuxOverlayInteractiveHint,
|
|
||||||
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
|
||||||
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
|
||||||
});
|
|
||||||
linuxOverlayInputShapeActive = result.active;
|
|
||||||
return result.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLinuxOverlayPointerInteractionActive(active: boolean): void {
|
|
||||||
visibleOverlayInteractionActive = active;
|
|
||||||
if (
|
|
||||||
process.platform === 'linux' &&
|
|
||||||
applyLinuxOverlayPointerInteractionMousePassthrough({
|
|
||||||
active,
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
|
||||||
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
|
||||||
updateVisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function primeLinuxOverlayPointerInteractionAfterFirstMeasurement(): void {
|
function primeLinuxOverlayPointerInteractionAfterFirstMeasurement(): void {
|
||||||
if (process.platform !== 'linux') return;
|
visibleOverlayInteractionRuntime.primeLinuxOverlayPointerInteractionAfterFirstMeasurement();
|
||||||
if (linuxVisibleOverlayStartupInputPrimed) return;
|
|
||||||
if (shouldUseLinuxOverlayInputShape()) return;
|
|
||||||
if (
|
|
||||||
!shouldPrimeLinuxOverlayInteractionFromMeasurement({
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
|
||||||
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
|
||||||
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
linuxVisibleOverlayStartupInputPrimed = true;
|
|
||||||
linuxVisibleOverlayStartupInputGraceUntilMs =
|
|
||||||
Date.now() + LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS;
|
|
||||||
updateLinuxOverlayPointerInteractionActive(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const linuxOverlayZOrderKeepAliveDeps = {
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
isTrackingMpvWindow: () => Boolean(appState.windowTracker?.isTracking()),
|
|
||||||
isMpvWindowFocused: () => appState.windowTracker?.isTargetWindowFocused?.() !== false,
|
|
||||||
isOverlayWindowFocused: () => overlayManager.getMainWindow()?.isFocused() === true,
|
|
||||||
shouldSuppressReassert: () =>
|
|
||||||
overlayModalInputState.getModalInputExclusive() ||
|
|
||||||
appState.statsOverlayVisible ||
|
|
||||||
hasLiveSeparateWindow(getOverlayForegroundSeparateWindows()) ||
|
|
||||||
(visibleOverlayInteractionActive && overlayManager.getMainWindow()?.isFocused() !== true),
|
|
||||||
raiseMpvWindow: () => {
|
|
||||||
if (
|
|
||||||
lastLinuxVisibleOverlayFollowedMpvAtMs > 0 &&
|
|
||||||
Date.now() - lastLinuxVisibleOverlayFollowedMpvAtMs <=
|
|
||||||
LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS
|
|
||||||
) {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
lastLinuxVisibleOverlayFollowedMpvAtMs = Date.now();
|
|
||||||
return appState.windowTracker?.raiseTargetWindow?.() ?? Promise.resolve(false);
|
|
||||||
},
|
|
||||||
releaseOverlayLayerOrder: () => {
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
mainWindow.setAlwaysOnTop(false);
|
|
||||||
mainWindow.setFullScreen?.(false);
|
|
||||||
mainWindow.setVisibleOnAllWorkspaces?.(false, { visibleOnFullScreen: false });
|
|
||||||
if (linuxVisibleOverlayWindowMode === 'fullscreen-override' && mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enforceOverlayLayerOrder: () => {
|
|
||||||
enforceOverlayLayerOrder();
|
|
||||||
},
|
|
||||||
focusOverlayWindow: () => {
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed() || mainWindow.isFocused()) return;
|
|
||||||
mainWindow.focus();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function requestLinuxOverlayZOrderFollow(): void {
|
function requestLinuxOverlayZOrderFollow(): void {
|
||||||
if (!shouldRunLinuxOverlayZOrderKeepAlive()) return;
|
visibleOverlayInteractionRuntime.requestLinuxOverlayZOrderFollow();
|
||||||
void tickLinuxOverlayZOrderKeepAlive(linuxOverlayZOrderKeepAliveDeps).catch((error) => {
|
|
||||||
logger.debug(
|
|
||||||
'Failed to follow tracked mpv behind focused overlay:',
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureLinuxOverlayZOrderKeepAliveLoop(linuxOverlayZOrderKeepAliveDeps);
|
|
||||||
|
|
||||||
const linuxOverlayPointerInteractionDeps = {
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
getCursorScreenPoint: () =>
|
|
||||||
linuxX11CursorPointReader.getCursorScreenPoint(screen.getCursorScreenPoint()),
|
|
||||||
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
|
||||||
getRendererInteractiveHint: () =>
|
|
||||||
linuxOverlayInteractiveHint || hasLinuxVisibleOverlayStartupInputGrace(),
|
|
||||||
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
|
||||||
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
|
||||||
shouldUseInputShape: shouldUseLinuxOverlayInputShape,
|
|
||||||
getInteractionActive: () => visibleOverlayInteractionActive,
|
|
||||||
setInteractionActive: updateLinuxOverlayPointerInteractionActive,
|
|
||||||
};
|
|
||||||
|
|
||||||
function tickLinuxOverlayPointerInteractionNow(): void {
|
function tickLinuxOverlayPointerInteractionNow(): void {
|
||||||
if (applyLinuxOverlayInputShapeFromLatestMeasurement()) {
|
visibleOverlayInteractionRuntime.tickLinuxOverlayPointerInteractionNow();
|
||||||
return;
|
|
||||||
}
|
|
||||||
tickLinuxOverlayPointerInteraction(linuxOverlayPointerInteractionDeps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureLinuxOverlayPointerInteractionLoop(linuxOverlayPointerInteractionDeps);
|
|
||||||
|
|
||||||
const buildGetRuntimeOptionsStateMainDepsHandler = createBuildGetRuntimeOptionsStateMainDepsHandler(
|
const buildGetRuntimeOptionsStateMainDepsHandler = createBuildGetRuntimeOptionsStateMainDepsHandler(
|
||||||
{
|
{
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
@@ -4932,7 +4378,8 @@ const {
|
|||||||
syncVisibleOverlayMpvFullscreenMode: (nextFullscreen) =>
|
syncVisibleOverlayMpvFullscreenMode: (nextFullscreen) =>
|
||||||
syncLinuxVisibleOverlayMpvFullscreenMode(nextFullscreen),
|
syncLinuxVisibleOverlayMpvFullscreenMode(nextFullscreen),
|
||||||
getOverlayInteractionActive: () =>
|
getOverlayInteractionActive: () =>
|
||||||
visibleOverlayInteractionActive || linuxOverlayInputShapeActive,
|
visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive() ||
|
||||||
|
visibleOverlayInteractionRuntime.getLinuxOverlayInputShapeActive(),
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
},
|
},
|
||||||
cancelLinuxMpvFullscreenOverlayRefreshBurst,
|
cancelLinuxMpvFullscreenOverlayRefreshBurst,
|
||||||
@@ -5925,13 +5372,13 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
if (!mainWindow || senderWindow !== mainWindow) {
|
if (!mainWindow || senderWindow !== mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (visibleOverlayInteractionActive === active) {
|
if (visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive() === active) {
|
||||||
if (active && process.platform === 'darwin' && !mainWindow.isFocused()) {
|
if (active && process.platform === 'darwin' && !mainWindow.isFocused()) {
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visibleOverlayInteractionActive = active;
|
visibleOverlayInteractionRuntime.setVisibleOverlayInteractionActive(active);
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
},
|
},
|
||||||
onOverlayInteractiveHint: (interactive, senderWindow) => {
|
onOverlayInteractiveHint: (interactive, senderWindow) => {
|
||||||
@@ -5939,7 +5386,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
if (!mainWindow || senderWindow !== mainWindow) {
|
if (!mainWindow || senderWindow !== mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
linuxOverlayInteractiveHint = interactive;
|
visibleOverlayInteractionRuntime.setLinuxOverlayInteractiveHint(interactive);
|
||||||
applyLinuxOverlayInputShapeFromLatestMeasurement();
|
applyLinuxOverlayInputShapeFromLatestMeasurement();
|
||||||
},
|
},
|
||||||
handleOverlayNotificationAction: (notificationId, actionId, noteId) => {
|
handleOverlayNotificationAction: (notificationId, actionId, noteId) => {
|
||||||
|
|||||||
@@ -373,11 +373,12 @@ test('stats server Yomitan note creation honors configured Anki server override
|
|||||||
|
|
||||||
test('Linux visible overlay recreation clears stale input state before creating replacement window', () => {
|
test('Linux visible overlay recreation clears stale input state before creating replacement window', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
|
const runtimeSource = readSource('src/main/runtime/visible-overlay-interaction-runtime.ts');
|
||||||
const actionBlock = source.match(
|
const actionBlock = source.match(
|
||||||
/function createLinuxVisibleOverlayWindowForCurrentMode\([\s\S]*?\): void \{(?<body>[\s\S]*?)\n\}/,
|
/function createLinuxVisibleOverlayWindowForCurrentMode\([\s\S]*?\): void \{(?<body>[\s\S]*?)\n\}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
const resetBlock = source.match(
|
const resetBlock = runtimeSource.match(
|
||||||
/function resetVisibleOverlayInputState\(\): void \{(?<body>[\s\S]*?)\n\}/,
|
/function resetVisibleOverlayInputState\(\): void \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
|
||||||
assert.ok(actionBlock);
|
assert.ok(actionBlock);
|
||||||
@@ -506,8 +507,9 @@ test('manual visible overlay show primes current subtitle from mpv before relyin
|
|||||||
|
|
||||||
test('Linux visible overlay show/reset does not leave an empty X11 window shape', () => {
|
test('Linux visible overlay show/reset does not leave an empty X11 window shape', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
const resetBlock = source.match(
|
const runtimeSource = readSource('src/main/runtime/visible-overlay-interaction-runtime.ts');
|
||||||
/function resetVisibleOverlayInputState\(\): void \{(?<body>[\s\S]*?)\n\}/,
|
const resetBlock = runtimeSource.match(
|
||||||
|
/function resetVisibleOverlayInputState\(\): void \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
const setBlock = source.match(
|
const setBlock = source.match(
|
||||||
/function setVisibleOverlayVisible\(visible: boolean\): void \{(?<body>[\s\S]*?)\n\}/,
|
/function setVisibleOverlayVisible\(visible: boolean\): void \{(?<body>[\s\S]*?)\n\}/,
|
||||||
@@ -517,6 +519,7 @@ test('Linux visible overlay show/reset does not leave an empty X11 window shape'
|
|||||||
assert.ok(setBlock);
|
assert.ok(setBlock);
|
||||||
assert.match(resetBlock, /restoreLinuxOverlayWindowShape\(mainWindow\);/);
|
assert.match(resetBlock, /restoreLinuxOverlayWindowShape\(mainWindow\);/);
|
||||||
assert.doesNotMatch(source, /setShape\?\.\(\[\]\)|setShape\(\[\]\)/);
|
assert.doesNotMatch(source, /setShape\?\.\(\[\]\)|setShape\(\[\]\)/);
|
||||||
|
assert.doesNotMatch(runtimeSource, /setShape\?\.\(\[\]\)|setShape\(\[\]\)/);
|
||||||
assert.match(
|
assert.match(
|
||||||
setBlock,
|
setBlock,
|
||||||
/if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);/,
|
/if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);/,
|
||||||
|
|||||||
@@ -0,0 +1,810 @@
|
|||||||
|
import { type BrowserWindow, screen } from 'electron';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { startOverlayWindowTracker as startOverlayWindowTrackerCore } from '../../core/services';
|
||||||
|
import { isHeadlessInitialCommand, type CliArgs } from '../../cli/args';
|
||||||
|
import type { OverlayContentMeasurement, WindowGeometry } from '../../types';
|
||||||
|
import { createWindowTracker as createWindowTrackerCore } from '../../window-trackers';
|
||||||
|
import type { BaseWindowTracker } from '../../window-trackers';
|
||||||
|
import {
|
||||||
|
bindWindowsOverlayAboveMpv,
|
||||||
|
clearWindowsOverlayOwner,
|
||||||
|
findWindowsMpvTargetWindowHandle,
|
||||||
|
getWindowsForegroundProcessName,
|
||||||
|
setWindowsOverlayOwner,
|
||||||
|
} from '../../window-trackers/windows-helper';
|
||||||
|
import {
|
||||||
|
applyLinuxOverlayInputShape,
|
||||||
|
applyLinuxOverlayPointerInteractionMousePassthrough,
|
||||||
|
ensureLinuxOverlayPointerInteractionLoop,
|
||||||
|
type ForegroundSuppressionGraceState,
|
||||||
|
mapOverlayMeasurementForPointerInteraction,
|
||||||
|
resolveForegroundSuppressionWithGrace,
|
||||||
|
shouldPrimeLinuxOverlayInteractionFromMeasurement,
|
||||||
|
tickLinuxOverlayPointerInteraction,
|
||||||
|
} from './linux-overlay-pointer-interaction';
|
||||||
|
import { restoreLinuxOverlayWindowShape } from './linux-overlay-window-shape';
|
||||||
|
import {
|
||||||
|
ensureLinuxOverlayZOrderKeepAliveLoop,
|
||||||
|
shouldRunLinuxOverlayZOrderKeepAlive,
|
||||||
|
tickLinuxOverlayZOrderKeepAlive,
|
||||||
|
} from './linux-overlay-zorder-keepalive';
|
||||||
|
import { createLinuxX11CursorPointReader } from './linux-x11-cursor-point';
|
||||||
|
import type { LinuxVisibleOverlayWindowMode } from './linux-visible-overlay-window-mode';
|
||||||
|
import { createStatsOverlayVisibilityChangeHandler } from './stats-overlay-visibility';
|
||||||
|
import { hasLiveSeparateWindow } from './settings-window-z-order';
|
||||||
|
|
||||||
|
export interface VisibleOverlayInteractionRuntimeDeps {
|
||||||
|
overlayManager: {
|
||||||
|
getMainWindow: () => BrowserWindow | null;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
};
|
||||||
|
overlayContentMeasurementStore: {
|
||||||
|
clear: (layer: 'visible') => void;
|
||||||
|
getLatestByLayer: (layer: 'visible') => OverlayContentMeasurement | null;
|
||||||
|
};
|
||||||
|
logger: {
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
getModalInputExclusive: () => boolean;
|
||||||
|
getStatsOverlayVisible: () => boolean;
|
||||||
|
setStatsOverlayVisible: (visible: boolean) => void;
|
||||||
|
getWindowTracker: () => BaseWindowTracker | null;
|
||||||
|
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||||
|
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
|
getBackendOverride: () => string | null;
|
||||||
|
getInitialArgs: () => CliArgs | null;
|
||||||
|
getOverlayRuntimeInitialized: () => boolean;
|
||||||
|
getLinuxVisibleOverlayWindowMode: () => LinuxVisibleOverlayWindowMode;
|
||||||
|
setLinuxVisibleOverlayOwnerBindingKey: (key: string | null) => void;
|
||||||
|
bindVisibleOverlayToTrackedX11Window: (window: BrowserWindow) => void;
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
refreshCurrentSubtitle: () => void;
|
||||||
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
resetLastOverlayWindowGeometry: () => void;
|
||||||
|
enforceOverlayLayerOrder: () => void;
|
||||||
|
getOverlayForegroundSeparateWindows: () => BrowserWindow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInteractionRuntimeDeps) {
|
||||||
|
const { overlayManager, overlayContentMeasurementStore, logger } = deps;
|
||||||
|
|
||||||
|
const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const;
|
||||||
|
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
||||||
|
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
||||||
|
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
||||||
|
const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500;
|
||||||
|
// Ignore transient "neither mpv nor overlay is the active window" blips before suppressing
|
||||||
|
// subtitle pointer interaction. Right after playback starts the overlay can briefly become the
|
||||||
|
// X11 active window, which would otherwise leave subtitles inert for a poll cycle (~1s).
|
||||||
|
const LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS = 500;
|
||||||
|
const LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS = 1_500;
|
||||||
|
const MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS = 1_200;
|
||||||
|
let visibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
|
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
|
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||||
|
let windowsVisibleOverlayZOrderSyncQueued = false;
|
||||||
|
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||||
|
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||||
|
let lastLinuxVisibleOverlayFollowedMpvAtMs = 0;
|
||||||
|
const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = {
|
||||||
|
lossSinceMs: null,
|
||||||
|
};
|
||||||
|
let visibleOverlayInteractionActive = false;
|
||||||
|
let linuxOverlayInputShapeActive = false;
|
||||||
|
let linuxVisibleOverlayStartupInputPrimed = false;
|
||||||
|
let linuxVisibleOverlayStartupInputGraceUntilMs = 0;
|
||||||
|
// Renderer-reported interactive hint (Linux only): true while a Yomitan popup/modal
|
||||||
|
// region is interactive, so the cursor poll keeps the overlay interactive even when the cursor
|
||||||
|
// moves off measured subtitle/sidebar rects onto the popup.
|
||||||
|
let linuxOverlayInteractiveHint = false;
|
||||||
|
let macOSVisibleOverlayForegroundProbeActive = false;
|
||||||
|
let macOSVisibleOverlayForegroundProbeToken = 0;
|
||||||
|
let macOSVisibleOverlayForegroundProbeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const linuxVisibleOverlayOwnerBindingQueues = new WeakMap<BrowserWindow, Promise<void>>();
|
||||||
|
|
||||||
|
const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({
|
||||||
|
setStatsOverlayVisibleState: (visible) => {
|
||||||
|
deps.setStatsOverlayVisible(visible);
|
||||||
|
},
|
||||||
|
resetVisibleOverlayInteraction: () => {
|
||||||
|
visibleOverlayInteractionActive = false;
|
||||||
|
},
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetVisibleOverlayInputState(): void {
|
||||||
|
visibleOverlayInteractionActive = false;
|
||||||
|
linuxOverlayInputShapeActive = false;
|
||||||
|
resetLinuxVisibleOverlayStartupInputPrimer();
|
||||||
|
linuxOverlayInteractiveHint = false;
|
||||||
|
overlayContentMeasurementStore.clear('visible');
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (process.platform === 'linux' && mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
restoreLinuxOverlayWindowShape(mainWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreVisibleOverlayWindowShapeForShow(): void {
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
restoreLinuxOverlayWindowShape(overlayManager.getMainWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVisibleOverlayBlurRefreshTimeouts(): void {
|
||||||
|
for (const timeout of visibleOverlayBlurRefreshTimeouts) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
visibleOverlayBlurRefreshTimeouts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
|
||||||
|
for (const timeout of windowsVisibleOverlayZOrderRetryTimeouts) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishMacOSVisibleOverlayForegroundProbe(token: number): void {
|
||||||
|
if (token !== macOSVisibleOverlayForegroundProbeToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||||
|
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||||
|
macOSVisibleOverlayForegroundProbeTimeout = null;
|
||||||
|
}
|
||||||
|
if (!macOSVisibleOverlayForegroundProbeActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
macOSVisibleOverlayForegroundProbeActive = false;
|
||||||
|
deps.updateVisibleOverlayVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMacOSVisibleOverlayForegroundProbe(): void {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tracker = deps.getWindowTracker();
|
||||||
|
if (!tracker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
macOSVisibleOverlayForegroundProbeActive = true;
|
||||||
|
const token = ++macOSVisibleOverlayForegroundProbeToken;
|
||||||
|
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||||
|
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||||
|
}
|
||||||
|
macOSVisibleOverlayForegroundProbeTimeout = setTimeout(() => {
|
||||||
|
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||||
|
}, MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
void tracker
|
||||||
|
.refreshNow()
|
||||||
|
.catch((error) => {
|
||||||
|
logger.warn('Failed to refresh macOS frontmost app after overlay blur', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNativeWindowHandleDecimal(window: BrowserWindow): string {
|
||||||
|
const handle = window.getNativeWindowHandle();
|
||||||
|
return handle.length >= 8
|
||||||
|
? handle.readBigUInt64LE(0).toString()
|
||||||
|
: BigInt(handle.readUInt32LE(0)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
||||||
|
return getNativeWindowHandleDecimal(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number {
|
||||||
|
const handle = window.getNativeWindowHandle();
|
||||||
|
return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueVisibleOverlayX11OwnerBindingOperation(
|
||||||
|
window: BrowserWindow,
|
||||||
|
args: string[],
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
): void {
|
||||||
|
const previous = linuxVisibleOverlayOwnerBindingQueues.get(window) ?? Promise.resolve();
|
||||||
|
const operation = previous
|
||||||
|
.catch(() => {})
|
||||||
|
.then(
|
||||||
|
() =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
if (window.isDestroyed()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
execFile('xprop', args, { timeout: 1500 }, (error) => {
|
||||||
|
if (error) {
|
||||||
|
onError?.(error);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const queued = operation.finally(() => {
|
||||||
|
if (linuxVisibleOverlayOwnerBindingQueues.get(window) === queued) {
|
||||||
|
linuxVisibleOverlayOwnerBindingQueues.delete(window);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linuxVisibleOverlayOwnerBindingQueues.set(window, queued);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVisibleOverlayX11OwnerBinding(window: BrowserWindow): void {
|
||||||
|
if (window.isDestroyed()) return;
|
||||||
|
enqueueVisibleOverlayX11OwnerBindingOperation(window, [
|
||||||
|
'-id',
|
||||||
|
getNativeWindowHandleDecimal(window),
|
||||||
|
'-remove',
|
||||||
|
'WM_TRANSIENT_FOR',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWindowsOverlayBindTargetHandle(
|
||||||
|
targetMpvSocketPath?: string | null,
|
||||||
|
): number | null {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (targetMpvSocketPath) {
|
||||||
|
const windowTracker = deps.getWindowTracker() as {
|
||||||
|
getTargetWindowHandle?: () => number | null;
|
||||||
|
} | null;
|
||||||
|
const trackedHandle = windowTracker?.getTargetWindowHandle?.();
|
||||||
|
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
||||||
|
return trackedHandle;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return findWindowsMpvTargetWindowHandle();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOverlayWindowTracker(
|
||||||
|
override?: string | null,
|
||||||
|
targetMpvSocketPath?: string | null,
|
||||||
|
) {
|
||||||
|
const initialArgs = deps.getInitialArgs();
|
||||||
|
if (initialArgs && isHeadlessInitialCommand(initialArgs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createWindowTrackerCore(override, targetMpvSocketPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindVisibleOverlayOwner(): void {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
deps.bindVisibleOverlayToTrackedX11Window(mainWindow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.platform !== 'win32') return;
|
||||||
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
|
const targetSocketPath = deps.getMpvSocketPath();
|
||||||
|
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(targetSocketPath);
|
||||||
|
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetSocketPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tracker = deps.getWindowTracker();
|
||||||
|
const mpvResult = tracker
|
||||||
|
? (() => {
|
||||||
|
try {
|
||||||
|
const win32 =
|
||||||
|
require('../../window-trackers/win32') as typeof import('../../window-trackers/win32');
|
||||||
|
const poll = win32.findMpvWindows();
|
||||||
|
const focused = poll.matches.find((m) => m.isForeground);
|
||||||
|
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
if (!mpvResult) return;
|
||||||
|
if (!setWindowsOverlayOwner(overlayHwnd, mpvResult.hwnd)) {
|
||||||
|
logger.warn('Failed to set overlay owner via koffi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseVisibleOverlayOwner(): void {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
deps.setLinuxVisibleOverlayOwnerBindingKey(null);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
clearVisibleOverlayX11OwnerBinding(mainWindow);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
|
if (!clearWindowsOverlayOwner(overlayHwnd)) {
|
||||||
|
logger.warn('Failed to clear overlay owner via koffi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOverlayWindowTrackerForCurrentSocket(): void {
|
||||||
|
startOverlayWindowTrackerCore({
|
||||||
|
backendOverride: deps.getBackendOverride(),
|
||||||
|
getMpvSocketPath: () => deps.getMpvSocketPath(),
|
||||||
|
createWindowTracker: createOverlayWindowTracker,
|
||||||
|
setWindowTracker: (tracker) => {
|
||||||
|
deps.setWindowTracker(tracker);
|
||||||
|
},
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||||
|
deps.updateVisibleOverlayBounds(geometry),
|
||||||
|
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||||
|
refreshCurrentSubtitle: () => {
|
||||||
|
deps.refreshCurrentSubtitle();
|
||||||
|
},
|
||||||
|
getOverlayWindows: () => deps.getOverlayWindows(),
|
||||||
|
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
|
||||||
|
bindOverlayOwner: () => bindVisibleOverlayOwner(),
|
||||||
|
releaseOverlayOwner: () => releaseVisibleOverlayOwner(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function retargetOverlayWindowTrackerForMpvSocket(
|
||||||
|
nextSocketPath: string,
|
||||||
|
previousSocketPath: string,
|
||||||
|
): void {
|
||||||
|
if (nextSocketPath === previousSocketPath || !deps.getOverlayRuntimeInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTracker = deps.getWindowTracker();
|
||||||
|
if (previousTracker) {
|
||||||
|
try {
|
||||||
|
previousTracker.stop();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to stop previous overlay window tracker before retargeting', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseVisibleOverlayOwner();
|
||||||
|
deps.setWindowTracker(null);
|
||||||
|
deps.setTrackerNotReadyWarningShown(false);
|
||||||
|
deps.resetLastOverlayWindowGeometry();
|
||||||
|
startOverlayWindowTrackerForCurrentSocket();
|
||||||
|
deps.updateVisibleOverlayVisibility();
|
||||||
|
deps.syncOverlayShortcuts();
|
||||||
|
logger.info(
|
||||||
|
`Retargeted overlay window tracker for MPV socket: ${previousSocketPath} -> ${nextSocketPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncWindowsVisibleOverlayToMpvZOrder(): Promise<boolean> {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (
|
||||||
|
!mainWindow ||
|
||||||
|
mainWindow.isDestroyed() ||
|
||||||
|
!mainWindow.isVisible() ||
|
||||||
|
!overlayManager.getVisibleOverlayVisible()
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowTracker = deps.getWindowTracker();
|
||||||
|
if (!windowTracker) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof windowTracker.isTargetWindowMinimized === 'function' &&
|
||||||
|
windowTracker.isTargetWindowMinimized()
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!windowTracker.isTracking() && windowTracker.getGeometry() === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
|
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(deps.getMpvSocketPath());
|
||||||
|
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
||||||
|
(mainWindow as BrowserWindow & { setOpacity?: (opacity: number) => void }).setOpacity?.(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestWindowsVisibleOverlayZOrderSync(): void {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowsVisibleOverlayZOrderSyncInFlight) {
|
||||||
|
windowsVisibleOverlayZOrderSyncQueued = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowsVisibleOverlayZOrderSyncInFlight = true;
|
||||||
|
void syncWindowsVisibleOverlayToMpvZOrder()
|
||||||
|
.catch((error) => {
|
||||||
|
logger.warn('Failed to bind Windows overlay z-order to mpv', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||||
|
if (!windowsVisibleOverlayZOrderSyncQueued) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowsVisibleOverlayZOrderSyncQueued = false;
|
||||||
|
requestWindowsVisibleOverlayZOrderSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleWindowsVisibleOverlayZOrderSyncBurst(): void {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearWindowsVisibleOverlayZOrderRetryTimeouts();
|
||||||
|
for (const delayMs of WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS) {
|
||||||
|
const retryTimeout = setTimeout(() => {
|
||||||
|
windowsVisibleOverlayZOrderRetryTimeouts = windowsVisibleOverlayZOrderRetryTimeouts.filter(
|
||||||
|
(timeout) => timeout !== retryTimeout,
|
||||||
|
);
|
||||||
|
requestWindowsVisibleOverlayZOrderSync();
|
||||||
|
}, delayMs);
|
||||||
|
windowsVisibleOverlayZOrderRetryTimeouts.push(retryTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasWindowsVisibleOverlayFocusHandoffGrace(): boolean {
|
||||||
|
return (
|
||||||
|
process.platform === 'win32' &&
|
||||||
|
lastWindowsVisibleOverlayBlurredAtMs > 0 &&
|
||||||
|
Date.now() - lastWindowsVisibleOverlayBlurredAtMs <=
|
||||||
|
WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPollWindowsVisibleOverlayForegroundProcess(): boolean {
|
||||||
|
if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowTracker = deps.getWindowTracker();
|
||||||
|
if (!windowTracker) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof windowTracker.isTargetWindowMinimized === 'function' &&
|
||||||
|
windowTracker.isTargetWindowMinimized()
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayFocused = mainWindow.isFocused();
|
||||||
|
const trackerFocused = windowTracker.isTargetWindowFocused?.() ?? false;
|
||||||
|
return !overlayFocused && !trackerFocused;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybePollWindowsVisibleOverlayForegroundProcess(): void {
|
||||||
|
if (!shouldPollWindowsVisibleOverlayForegroundProcess()) {
|
||||||
|
lastWindowsVisibleOverlayForegroundProcessName = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processName = getWindowsForegroundProcessName();
|
||||||
|
const normalizedProcessName = processName?.trim().toLowerCase() ?? null;
|
||||||
|
const previousProcessName = lastWindowsVisibleOverlayForegroundProcessName;
|
||||||
|
lastWindowsVisibleOverlayForegroundProcessName = normalizedProcessName;
|
||||||
|
|
||||||
|
if (normalizedProcessName !== previousProcessName) {
|
||||||
|
deps.updateVisibleOverlayVisibility();
|
||||||
|
}
|
||||||
|
if (normalizedProcessName === 'mpv' && previousProcessName !== 'mpv') {
|
||||||
|
requestWindowsVisibleOverlayZOrderSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureWindowsVisibleOverlayForegroundPollLoop(): void {
|
||||||
|
if (process.platform !== 'win32' || windowsVisibleOverlayForegroundPollInterval !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowsVisibleOverlayForegroundPollInterval = setInterval(() => {
|
||||||
|
maybePollWindowsVisibleOverlayForegroundProcess();
|
||||||
|
}, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWindowsVisibleOverlayForegroundPollLoop(): void {
|
||||||
|
if (windowsVisibleOverlayForegroundPollInterval === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(windowsVisibleOverlayForegroundPollInterval);
|
||||||
|
windowsVisibleOverlayForegroundPollInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleVisibleOverlayBlurRefresh(): void {
|
||||||
|
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
|
||||||
|
}
|
||||||
|
startMacOSVisibleOverlayForegroundProbe();
|
||||||
|
clearVisibleOverlayBlurRefreshTimeouts();
|
||||||
|
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
|
||||||
|
const refreshTimeout = setTimeout(() => {
|
||||||
|
visibleOverlayBlurRefreshTimeouts = visibleOverlayBlurRefreshTimeouts.filter(
|
||||||
|
(timeout) => timeout !== refreshTimeout,
|
||||||
|
);
|
||||||
|
deps.updateVisibleOverlayVisibility();
|
||||||
|
}, delayMs);
|
||||||
|
visibleOverlayBlurRefreshTimeouts.push(refreshTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureWindowsVisibleOverlayForegroundPollLoop();
|
||||||
|
|
||||||
|
const linuxX11CursorPointReader = createLinuxX11CursorPointReader();
|
||||||
|
|
||||||
|
function getLinuxOverlayPointerMeasurement() {
|
||||||
|
const measurement = overlayContentMeasurementStore.getLatestByLayer('visible');
|
||||||
|
return mapOverlayMeasurementForPointerInteraction(measurement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSuspendLinuxOverlayPointerInteraction(): boolean {
|
||||||
|
return deps.getModalInputExclusive() || deps.getStatsOverlayVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSuppressLinuxOverlayPointerInteraction(): boolean {
|
||||||
|
return resolveForegroundSuppressionWithGrace({
|
||||||
|
hasForegroundSeparateWindow: hasLiveSeparateWindow(
|
||||||
|
deps.getOverlayForegroundSeparateWindows(),
|
||||||
|
),
|
||||||
|
isTrackingMpvWindow: Boolean(deps.getWindowTracker()?.isTracking()),
|
||||||
|
isMpvWindowFocused: deps.getWindowTracker()?.isTargetWindowFocused?.() !== false,
|
||||||
|
isOverlayWindowFocused: overlayManager.getMainWindow()?.isFocused() === true,
|
||||||
|
nowMs: Date.now(),
|
||||||
|
graceMs: LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS,
|
||||||
|
state: linuxPointerForegroundSuppressionGrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseLinuxOverlayInputShape(): boolean {
|
||||||
|
// Electron's setShape is a *bounding* shape: outside the given rects no pixels are drawn, so
|
||||||
|
// it clips the visible subtitle (and makes a dragged subtitle vanish behind the shaped
|
||||||
|
// region). There is no input-only region API on Linux, so selective hit-testing is handled by
|
||||||
|
// the main-process cursor poll instead. Keep this off to avoid clipping the overlay.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLinuxVisibleOverlayStartupInputGrace(): boolean {
|
||||||
|
return (
|
||||||
|
process.platform === 'linux' &&
|
||||||
|
linuxVisibleOverlayStartupInputGraceUntilMs > 0 &&
|
||||||
|
Date.now() < linuxVisibleOverlayStartupInputGraceUntilMs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLinuxVisibleOverlayStartupInputGrace(): void {
|
||||||
|
linuxVisibleOverlayStartupInputGraceUntilMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLinuxVisibleOverlayStartupInputPrimer(): void {
|
||||||
|
linuxVisibleOverlayStartupInputPrimed = false;
|
||||||
|
clearLinuxVisibleOverlayStartupInputGrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLinuxOverlayInputShapeFromLatestMeasurement(): boolean {
|
||||||
|
if (!shouldUseLinuxOverlayInputShape()) {
|
||||||
|
linuxOverlayInputShapeActive = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = applyLinuxOverlayInputShape({
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
||||||
|
getRendererInteractiveHint: () => linuxOverlayInteractiveHint,
|
||||||
|
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
||||||
|
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
||||||
|
});
|
||||||
|
linuxOverlayInputShapeActive = result.active;
|
||||||
|
return result.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLinuxOverlayPointerInteractionActive(active: boolean): void {
|
||||||
|
visibleOverlayInteractionActive = active;
|
||||||
|
if (
|
||||||
|
process.platform === 'linux' &&
|
||||||
|
applyLinuxOverlayPointerInteractionMousePassthrough({
|
||||||
|
active,
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
||||||
|
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
||||||
|
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.updateVisibleOverlayVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function primeLinuxOverlayPointerInteractionAfterFirstMeasurement(): void {
|
||||||
|
if (process.platform !== 'linux') return;
|
||||||
|
if (linuxVisibleOverlayStartupInputPrimed) return;
|
||||||
|
if (shouldUseLinuxOverlayInputShape()) return;
|
||||||
|
if (
|
||||||
|
!shouldPrimeLinuxOverlayInteractionFromMeasurement({
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
||||||
|
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
||||||
|
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxVisibleOverlayStartupInputPrimed = true;
|
||||||
|
linuxVisibleOverlayStartupInputGraceUntilMs =
|
||||||
|
Date.now() + LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS;
|
||||||
|
updateLinuxOverlayPointerInteractionActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linuxOverlayZOrderKeepAliveDeps = {
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
isTrackingMpvWindow: () => Boolean(deps.getWindowTracker()?.isTracking()),
|
||||||
|
isMpvWindowFocused: () => deps.getWindowTracker()?.isTargetWindowFocused?.() !== false,
|
||||||
|
isOverlayWindowFocused: () => overlayManager.getMainWindow()?.isFocused() === true,
|
||||||
|
shouldSuppressReassert: () =>
|
||||||
|
deps.getModalInputExclusive() ||
|
||||||
|
deps.getStatsOverlayVisible() ||
|
||||||
|
hasLiveSeparateWindow(deps.getOverlayForegroundSeparateWindows()) ||
|
||||||
|
(visibleOverlayInteractionActive && overlayManager.getMainWindow()?.isFocused() !== true),
|
||||||
|
raiseMpvWindow: () => {
|
||||||
|
if (
|
||||||
|
lastLinuxVisibleOverlayFollowedMpvAtMs > 0 &&
|
||||||
|
Date.now() - lastLinuxVisibleOverlayFollowedMpvAtMs <=
|
||||||
|
LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS
|
||||||
|
) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
lastLinuxVisibleOverlayFollowedMpvAtMs = Date.now();
|
||||||
|
return deps.getWindowTracker()?.raiseTargetWindow?.() ?? Promise.resolve(false);
|
||||||
|
},
|
||||||
|
releaseOverlayLayerOrder: () => {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
mainWindow.setAlwaysOnTop(false);
|
||||||
|
mainWindow.setFullScreen?.(false);
|
||||||
|
mainWindow.setVisibleOnAllWorkspaces?.(false, { visibleOnFullScreen: false });
|
||||||
|
if (
|
||||||
|
deps.getLinuxVisibleOverlayWindowMode() === 'fullscreen-override' &&
|
||||||
|
mainWindow.isVisible()
|
||||||
|
) {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enforceOverlayLayerOrder: () => {
|
||||||
|
deps.enforceOverlayLayerOrder();
|
||||||
|
},
|
||||||
|
focusOverlayWindow: () => {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed() || mainWindow.isFocused()) return;
|
||||||
|
mainWindow.focus();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function requestLinuxOverlayZOrderFollow(): void {
|
||||||
|
if (!shouldRunLinuxOverlayZOrderKeepAlive()) return;
|
||||||
|
void tickLinuxOverlayZOrderKeepAlive(linuxOverlayZOrderKeepAliveDeps).catch((error) => {
|
||||||
|
logger.debug(
|
||||||
|
'Failed to follow tracked mpv behind focused overlay:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLinuxOverlayZOrderKeepAliveLoop(linuxOverlayZOrderKeepAliveDeps);
|
||||||
|
|
||||||
|
const linuxOverlayPointerInteractionDeps = {
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getCursorScreenPoint: () =>
|
||||||
|
linuxX11CursorPointReader.getCursorScreenPoint(screen.getCursorScreenPoint()),
|
||||||
|
getSubtitleMeasurement: getLinuxOverlayPointerMeasurement,
|
||||||
|
getRendererInteractiveHint: () =>
|
||||||
|
linuxOverlayInteractiveHint || hasLinuxVisibleOverlayStartupInputGrace(),
|
||||||
|
shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction,
|
||||||
|
shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction,
|
||||||
|
shouldUseInputShape: shouldUseLinuxOverlayInputShape,
|
||||||
|
getInteractionActive: () => visibleOverlayInteractionActive,
|
||||||
|
setInteractionActive: updateLinuxOverlayPointerInteractionActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
function tickLinuxOverlayPointerInteractionNow(): void {
|
||||||
|
if (applyLinuxOverlayInputShapeFromLatestMeasurement()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tickLinuxOverlayPointerInteraction(linuxOverlayPointerInteractionDeps);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLinuxOverlayPointerInteractionLoop(linuxOverlayPointerInteractionDeps);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleStatsOverlayVisibilityChanged,
|
||||||
|
resetVisibleOverlayInputState,
|
||||||
|
restoreVisibleOverlayWindowShapeForShow,
|
||||||
|
startMacOSVisibleOverlayForegroundProbe,
|
||||||
|
getNativeWindowHandleDecimal,
|
||||||
|
getWindowsNativeWindowHandle,
|
||||||
|
getWindowsNativeWindowHandleNumber,
|
||||||
|
enqueueVisibleOverlayX11OwnerBindingOperation,
|
||||||
|
clearVisibleOverlayX11OwnerBinding,
|
||||||
|
createOverlayWindowTracker,
|
||||||
|
bindVisibleOverlayOwner,
|
||||||
|
releaseVisibleOverlayOwner,
|
||||||
|
startOverlayWindowTrackerForCurrentSocket,
|
||||||
|
retargetOverlayWindowTrackerForMpvSocket,
|
||||||
|
requestWindowsVisibleOverlayZOrderSync,
|
||||||
|
scheduleWindowsVisibleOverlayZOrderSyncBurst,
|
||||||
|
hasWindowsVisibleOverlayFocusHandoffGrace,
|
||||||
|
ensureWindowsVisibleOverlayForegroundPollLoop,
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop,
|
||||||
|
scheduleVisibleOverlayBlurRefresh,
|
||||||
|
getLinuxOverlayPointerMeasurement,
|
||||||
|
hasLinuxVisibleOverlayStartupInputGrace,
|
||||||
|
clearLinuxVisibleOverlayStartupInputGrace,
|
||||||
|
resetLinuxVisibleOverlayStartupInputPrimer,
|
||||||
|
applyLinuxOverlayInputShapeFromLatestMeasurement,
|
||||||
|
updateLinuxOverlayPointerInteractionActive,
|
||||||
|
primeLinuxOverlayPointerInteractionAfterFirstMeasurement,
|
||||||
|
requestLinuxOverlayZOrderFollow,
|
||||||
|
tickLinuxOverlayPointerInteractionNow,
|
||||||
|
getVisibleOverlayInteractionActive: () => visibleOverlayInteractionActive,
|
||||||
|
setVisibleOverlayInteractionActive: (active: boolean) => {
|
||||||
|
visibleOverlayInteractionActive = active;
|
||||||
|
},
|
||||||
|
getLinuxOverlayInputShapeActive: () => linuxOverlayInputShapeActive,
|
||||||
|
getLastWindowsVisibleOverlayForegroundProcessName: () =>
|
||||||
|
lastWindowsVisibleOverlayForegroundProcessName,
|
||||||
|
getMacOSVisibleOverlayForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
|
||||||
|
setLinuxOverlayInteractiveHint: (interactive: boolean) => {
|
||||||
|
linuxOverlayInteractiveHint = interactive;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VisibleOverlayInteractionRuntime = ReturnType<
|
||||||
|
typeof createVisibleOverlayInteractionRuntime
|
||||||
|
>;
|
||||||
Reference in New Issue
Block a user