mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(desktop): leave full screen before hiding on macOS close (#3284)
This commit is contained in:
1 parent
46a356cc40
commit
e7859fefb1
3 files changed
+158
-2
No files matched your search
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import {describe, test} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
const sourcePath = fileURLToPath(new URL('./MacFullScreenHide.ts', import.meta.url));
|
||||
const transformedSource = esbuild.transformSync(readFileSync(sourcePath, 'utf8'), {
|
||||
loader: 'ts',
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
}).code;
|
||||
|
||||
function loadMacFullScreenHide() {
|
||||
const module = {exports: {}};
|
||||
const context = vm.createContext({module, exports: module.exports, process: {platform: 'darwin'}});
|
||||
vm.runInContext(transformedSource, context, {filename: sourcePath});
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
class FakeWindow extends EventEmitter {
|
||||
fullScreen;
|
||||
visible = true;
|
||||
destroyed = false;
|
||||
calls = [];
|
||||
|
||||
constructor(fullScreen) {
|
||||
super();
|
||||
this.fullScreen = fullScreen;
|
||||
}
|
||||
|
||||
isDestroyed() {
|
||||
return this.destroyed;
|
||||
}
|
||||
|
||||
isFullScreen() {
|
||||
return this.fullScreen;
|
||||
}
|
||||
|
||||
setFullScreen(flag) {
|
||||
this.calls.push(`setFullScreen(${flag})`);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.calls.push('hide');
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
finishLeavingFullScreen() {
|
||||
this.fullScreen = false;
|
||||
this.emit('leave-full-screen');
|
||||
}
|
||||
}
|
||||
|
||||
describe('MacFullScreenHide', () => {
|
||||
test('hides a windowed window at once', () => {
|
||||
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(false);
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
assert.deepEqual(window.calls, ['hide']);
|
||||
});
|
||||
|
||||
test('leaves full screen on macOS and hides only after the transition ends', () => {
|
||||
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(true);
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
assert.deepEqual(window.calls, ['setFullScreen(false)']);
|
||||
assert.equal(window.visible, true);
|
||||
window.finishLeavingFullScreen();
|
||||
assert.deepEqual(window.calls, ['setFullScreen(false)', 'hide']);
|
||||
assert.equal(window.visible, false);
|
||||
});
|
||||
|
||||
test('a second close during the transition does not queue another exit', () => {
|
||||
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(true);
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
window.finishLeavingFullScreen();
|
||||
assert.deepEqual(window.calls, ['setFullScreen(false)', 'hide']);
|
||||
assert.equal(window.listenerCount('leave-full-screen'), 0);
|
||||
});
|
||||
|
||||
test('showing the window during the transition cancels the pending hide', () => {
|
||||
const {hideWindowLeavingFullScreen, cancelPendingFullScreenHide} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(true);
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
cancelPendingFullScreenHide(window);
|
||||
window.finishLeavingFullScreen();
|
||||
assert.deepEqual(window.calls, ['setFullScreen(false)']);
|
||||
assert.equal(window.visible, true);
|
||||
});
|
||||
|
||||
test('does not touch a window destroyed before the transition ends', () => {
|
||||
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(true);
|
||||
hideWindowLeavingFullScreen(window, 'darwin');
|
||||
window.destroyed = true;
|
||||
window.finishLeavingFullScreen();
|
||||
assert.deepEqual(window.calls, ['setFullScreen(false)']);
|
||||
});
|
||||
|
||||
test('hides directly on other platforms', () => {
|
||||
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
|
||||
const window = new FakeWindow(true);
|
||||
hideWindowLeavingFullScreen(window, 'win32');
|
||||
assert.deepEqual(window.calls, ['hide']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface FullScreenHideableWindow {
|
||||
isDestroyed(): boolean;
|
||||
isFullScreen(): boolean;
|
||||
setFullScreen(flag: boolean): void;
|
||||
hide(): void;
|
||||
once(event: 'leave-full-screen', listener: () => void): unknown;
|
||||
removeListener(event: 'leave-full-screen', listener: () => void): unknown;
|
||||
}
|
||||
|
||||
const pendingHides = new WeakMap<FullScreenHideableWindow, () => void>();
|
||||
|
||||
export function hideWindowLeavingFullScreen(
|
||||
window: FullScreenHideableWindow,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): void {
|
||||
if (platform !== 'darwin' || !window.isFullScreen()) {
|
||||
window.hide();
|
||||
return;
|
||||
}
|
||||
if (pendingHides.has(window)) return;
|
||||
const hide = () => {
|
||||
pendingHides.delete(window);
|
||||
if (!window.isDestroyed()) window.hide();
|
||||
};
|
||||
pendingHides.set(window, hide);
|
||||
window.once('leave-full-screen', hide);
|
||||
window.setFullScreen(false);
|
||||
}
|
||||
|
||||
export function cancelPendingFullScreenHide(window: FullScreenHideableWindow): void {
|
||||
const hide = pendingHides.get(window);
|
||||
if (!hide) return;
|
||||
pendingHides.delete(window);
|
||||
window.removeListener('leave-full-screen', hide);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {isNativeGatewayAvailable} from '@electron/main/GatewaySocketNativeBounda
|
||||
import {shouldDisableV8CodeCache} from '@electron/main/LaunchOptions';
|
||||
import {getDesktopLocalAppAuthorization} from '@electron/main/LocalAppProtocolAuthorization';
|
||||
import {isLocalAppRendererDocumentURL, isLocalAppURL} from '@electron/main/LocalAppURL';
|
||||
import {cancelPendingFullScreenHide, hideWindowLeavingFullScreen} from '@electron/main/MacFullScreenHide';
|
||||
import {t} from '@electron/main/MainI18n';
|
||||
import {MainWindowRevealGate, MainWindowRevealReason} from '@electron/main/MainWindowReveal';
|
||||
import {signalMainWindowCreated, signalMainWindowReady} from '@electron/main/ModuleBootHandoff';
|
||||
@@ -874,7 +875,7 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||
? 'Window close hid the app. Use Quit to terminate the process'
|
||||
: 'Window close hid the app to the tray. Use Quit to terminate the process',
|
||||
);
|
||||
mainWindow?.hide();
|
||||
if (mainWindow) hideWindowLeavingFullScreen(mainWindow);
|
||||
refreshDesktopTrayMenu();
|
||||
}
|
||||
});
|
||||
@@ -1140,6 +1141,7 @@ export function showWindow(): void {
|
||||
return;
|
||||
}
|
||||
if (mainWindow) {
|
||||
cancelPendingFullScreenHide(mainWindow);
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
@@ -1180,7 +1182,7 @@ export function showWindow(): void {
|
||||
|
||||
export function hideWindow(): void {
|
||||
if (mainWindow) {
|
||||
mainWindow.hide();
|
||||
hideWindowLeavingFullScreen(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user