mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
fix: settings window z-order on Hyprland and Linux app detach (#85)
This commit is contained in:
@@ -28,6 +28,7 @@ export {
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
} from './startup';
|
||||
export { destroyYomitanSettingsWindow, openYomitanSettingsWindow } from './yomitan-settings';
|
||||
export { promoteSettingsWindowAboveOverlay } from './settings-window-z-order';
|
||||
export { createTokenizerDepsRuntime, tokenizeSubtitle } from './tokenizer';
|
||||
export {
|
||||
addYomitanNoteViaSearch,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
promoteSettingsWindowAboveOverlay,
|
||||
shouldPromoteSettingsWindowAboveOverlay,
|
||||
} from './settings-window-z-order';
|
||||
|
||||
test('settings window promotion only applies to Hyprland sessions', () => {
|
||||
assert.equal(
|
||||
shouldPromoteSettingsWindowAboveOverlay('linux', { HYPRLAND_INSTANCE_SIGNATURE: 'hypr' }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldPromoteSettingsWindowAboveOverlay('linux', { WAYLAND_DISPLAY: 'wayland-1' }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldPromoteSettingsWindowAboveOverlay('darwin', { HYPRLAND_INSTANCE_SIGNATURE: 'hypr' }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('promoteSettingsWindowAboveOverlay raises Hyprland settings windows above the overlay', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const promoted = promoteSettingsWindowAboveOverlay(
|
||||
{
|
||||
isDestroyed: () => false,
|
||||
getTitle: () => 'SubMiner Settings',
|
||||
setAlwaysOnTop: (flag: boolean) => calls.push(`always-on-top:${flag}`),
|
||||
moveTop: () => calls.push('move-top'),
|
||||
} as never,
|
||||
{
|
||||
platform: 'linux',
|
||||
env: { HYPRLAND_INSTANCE_SIGNATURE: 'hypr' },
|
||||
ensureHyprlandWindowFloatingByTitle: ({ title }) => {
|
||||
calls.push(`hyprland-top:${title}`);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(promoted, true);
|
||||
assert.deepEqual(calls, ['always-on-top:true', 'move-top', 'hyprland-top:SubMiner Settings']);
|
||||
});
|
||||
|
||||
test('promoteSettingsWindowAboveOverlay skips destroyed windows', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const promoted = promoteSettingsWindowAboveOverlay(
|
||||
{
|
||||
isDestroyed: () => true,
|
||||
getTitle: () => 'SubMiner Settings',
|
||||
setAlwaysOnTop: () => calls.push('always-on-top'),
|
||||
moveTop: () => calls.push('move-top'),
|
||||
} as never,
|
||||
{
|
||||
platform: 'linux',
|
||||
env: { HYPRLAND_INSTANCE_SIGNATURE: 'hypr' },
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(promoted, false);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import {
|
||||
ensureHyprlandWindowFloatingByTitle,
|
||||
shouldAttemptHyprlandWindowPlacement,
|
||||
} from './hyprland-window-placement';
|
||||
|
||||
type SettingsWindowLevelController = Pick<
|
||||
BrowserWindow,
|
||||
'getTitle' | 'isDestroyed' | 'moveTop' | 'setAlwaysOnTop'
|
||||
>;
|
||||
|
||||
type PromoteSettingsWindowOptions = {
|
||||
platform?: NodeJS.Platform;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
ensureHyprlandWindowFloatingByTitle?: typeof ensureHyprlandWindowFloatingByTitle;
|
||||
};
|
||||
|
||||
export function shouldPromoteSettingsWindowAboveOverlay(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return shouldAttemptHyprlandWindowPlacement(platform, env);
|
||||
}
|
||||
|
||||
export function promoteSettingsWindowAboveOverlay(
|
||||
window: SettingsWindowLevelController,
|
||||
options: PromoteSettingsWindowOptions = {},
|
||||
): boolean {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const env = options.env ?? process.env;
|
||||
if (window.isDestroyed() || !shouldPromoteSettingsWindowAboveOverlay(platform, env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.setAlwaysOnTop(true);
|
||||
window.moveTop();
|
||||
|
||||
const title = window.getTitle().trim();
|
||||
if (title) {
|
||||
(options.ensureHyprlandWindowFloatingByTitle ?? ensureHyprlandWindowFloatingByTitle)({
|
||||
title,
|
||||
platform,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -106,20 +106,32 @@ test('yomitan settings URL disables the embedded popup preview', () => {
|
||||
test('showYomitanSettingsWindow restores, repaints, shows, and focuses an existing window', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
showYomitanSettingsWindow({
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => true,
|
||||
restore: () => calls.push('restore'),
|
||||
getSize: () => [1200, 800],
|
||||
setSize: (width: number, height: number) => calls.push(`set-size:${width}x${height}`),
|
||||
webContents: {
|
||||
invalidate: () => calls.push('invalidate'),
|
||||
showYomitanSettingsWindow(
|
||||
{
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => true,
|
||||
restore: () => calls.push('restore'),
|
||||
getSize: () => [1200, 800],
|
||||
setSize: (width: number, height: number) => calls.push(`set-size:${width}x${height}`),
|
||||
webContents: {
|
||||
invalidate: () => calls.push('invalidate'),
|
||||
},
|
||||
show: () => calls.push('show'),
|
||||
focus: () => calls.push('focus'),
|
||||
} as never,
|
||||
{
|
||||
promoteSettingsWindowAboveOverlay: () => calls.push('promote'),
|
||||
},
|
||||
show: () => calls.push('show'),
|
||||
focus: () => calls.push('focus'),
|
||||
} as never);
|
||||
);
|
||||
|
||||
assert.deepEqual(calls, ['restore', 'set-size:1200x800', 'invalidate', 'show', 'focus']);
|
||||
assert.deepEqual(calls, [
|
||||
'restore',
|
||||
'set-size:1200x800',
|
||||
'invalidate',
|
||||
'show',
|
||||
'focus',
|
||||
'promote',
|
||||
]);
|
||||
});
|
||||
|
||||
test('destroyYomitanSettingsWindow destroys a live settings window before app quit', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import electron from 'electron';
|
||||
import type { BrowserWindow, Extension, Menu, MenuItemConstructorOptions, Session } from 'electron';
|
||||
import { createLogger } from '../../logger';
|
||||
import { promoteSettingsWindowAboveOverlay } from './settings-window-z-order';
|
||||
|
||||
const { BrowserWindow: ElectronBrowserWindow, Menu: ElectronMenu, session } = electron;
|
||||
const logger = createLogger('main:yomitan-settings');
|
||||
@@ -136,7 +137,12 @@ export function buildYomitanSettingsUrl(extensionId: string): string {
|
||||
return `chrome-extension://${extensionId}/settings.html?popup-preview=false`;
|
||||
}
|
||||
|
||||
export function showYomitanSettingsWindow(settingsWindow: BrowserWindow): void {
|
||||
export function showYomitanSettingsWindow(
|
||||
settingsWindow: BrowserWindow,
|
||||
options: {
|
||||
promoteSettingsWindowAboveOverlay?: (settingsWindow: BrowserWindow) => void;
|
||||
} = {},
|
||||
): void {
|
||||
if (settingsWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
@@ -148,6 +154,7 @@ export function showYomitanSettingsWindow(settingsWindow: BrowserWindow): void {
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
settingsWindow.focus();
|
||||
(options.promoteSettingsWindowAboveOverlay ?? promoteSettingsWindowAboveOverlay)(settingsWindow);
|
||||
}
|
||||
|
||||
export function destroyYomitanSettingsWindow(settingsWindow: BrowserWindow | null): boolean {
|
||||
@@ -177,6 +184,7 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
logger.info('Creating new settings window for extension:', options.yomitanExt.id);
|
||||
|
||||
const settingsWindow = new ElectronBrowserWindow({
|
||||
title: 'Yomitan Settings',
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
|
||||
Reference in New Issue
Block a user