diff --git a/package-lock.json b/package-lock.json index eb19a81..4f89204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "license": "MIT", "bin": { "builder": "dist/index.js" diff --git a/package.json b/package.json index 4d28072..f032082 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)", "author": "Toshiyuki Ienaga ", "license": "MIT", diff --git a/templates/electron/index.js b/templates/electron/index.js index 73608f3..1b2f4cc 100644 --- a/templates/electron/index.js +++ b/templates/electron/index.js @@ -51,6 +51,21 @@ const overlayBounds = () => { const placeOverlay = () => { if (mainWindow && !mainWindow.isDestroyed()) mainWindow.setBounds(overlayBounds()); }; +// The overlay never takes focus, so Windows does not raise it when another topmost window (the taskbar, another +// always-on-top app) is activated, nor after the display is turned off and on. Put it back on top. +const keepOnTop = () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.setAlwaysOnTop(true, "floating"); + mainWindow.moveTop(); +}; +// Windows forwards mouse moves to a click-through window with a system mouse hook, and Electron installs it only +// when forwarding switches on. After sleep (closing the lid) the hook can be gone while Electron still thinks it is +// installed, so the page never sees the pointer. Switching forwarding off and on again installs a fresh hook. +const refreshForwarding = () => { + if (!overlayIgnore || !mainWindow || mainWindow.isDestroyed()) return; + mainWindow.setIgnoreMouseEvents(false); + mainWindow.setIgnoreMouseEvents(true, { forward: true }); +}; const createWindow = async () => { const { width, height } = screen.getPrimaryDisplay().workAreaSize; // The preload reads these flags; each API is exposed only when its feature is configured. @@ -89,10 +104,16 @@ const createWindow = async () => { mainWindow.setIgnoreMouseEvents(true, { forward: true }); } const events = ["display-metrics-changed", "display-added", "display-removed"]; - for (const name of events) screen.on(name, placeOverlay); + const onDisplayChange = () => { placeOverlay(); keepOnTop(); }; + for (const name of events) screen.on(name, onDisplayChange); mainWindow.once("closed", () => { - for (const name of events) screen.removeListener(name, placeOverlay); + for (const name of events) screen.removeListener(name, onDisplayChange); }); + if (process.platform === "win32") { + const timer = setInterval(() => { keepOnTop(); refreshForwarding(); }, 3000); + timer.unref?.(); + mainWindow.once("closed", () => clearInterval(timer)); + } } mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" })); mainWindow.webContents.on("will-navigate", (event, url) => { @@ -180,7 +201,7 @@ app.whenReady().then(async () => { // z-order/bounds. Apply the overlay state again and tell the page so it can recover too. const recoverOverlay = () => { if (!mainWindow || mainWindow.isDestroyed()) return; - mainWindow.setAlwaysOnTop(true, "floating"); + keepOnTop(); placeOverlay(); mainWindow.setIgnoreMouseEvents(false); mainWindow.setIgnoreMouseEvents(overlayIgnore, overlayIgnore ? { forward: true } : undefined); diff --git a/tests/electron-overlay.test.ts b/tests/electron-overlay.test.ts index 2f91281..48afaca 100644 --- a/tests/electron-overlay.test.ts +++ b/tests/electron-overlay.test.ts @@ -53,8 +53,9 @@ const fakeElectron = (options = {}) => { isFullScreen() { return false; } isDestroyed() { return false; } isVisible() { return true; } + moveTop() { calls.push(["moveTop"]); } showInactive() { calls.push(["showInactive"]); } - once() {} + once(name, cb) { this.onceListeners = { ...this.onceListeners, [name]: cb }; } async loadURL() {} } const electron = { @@ -244,3 +245,33 @@ test("a game without the overlay is not limited to one instance", async (t) => { assert.equal(calls.some(([name]) => name === "requestSingleInstanceLock"), false); assert.equal(appListeners.has("second-instance"), false); }); + +test("on Windows the overlay is put back on top every few seconds and when the displays change", async (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + const platform = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: "win32" }); + t.after(() => Object.defineProperty(process, "platform", platform)); + const { calls, listeners, handlers, windows } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); + calls.length = 0; + t.mock.timers.tick(3000); + assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); + // Click-through is on: the mouse-forwarding hook is installed again (off, then on). + assert.deepEqual(calls.filter(([name]) => name === "setIgnoreMouseEvents"), [["setIgnoreMouseEvents", false], ["setIgnoreMouseEvents", true, { forward: true }]]); + // While the page captures the mouse (pointer over the pet) nothing is toggled under it. + handlers.get("next2d:window:set-ignore-mouse-events")({ sender: windows[0].webContents, senderFrame: Object.assign(windows[0].webContents.mainFrame, { url: "next2d://game/index.html" }) }, false); + calls.length = 0; + t.mock.timers.tick(3000); + assert.equal(calls.some(([name]) => name === "setIgnoreMouseEvents"), false); + calls.length = 0; + listeners.get("display-added")(); + assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); +}); + +test("on other platforms there is no periodic raise", async (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + if (process.platform === "win32") return; + const { calls } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); + calls.length = 0; + t.mock.timers.tick(10000); + assert.equal(calls.some(([name]) => name === "moveTop"), false); +});