From be2064fbf04a329947128d6201b2a1030a2c1ff2 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Fri, 21 Mar 2025 11:08:33 +0800 Subject: [PATCH] Fix tray exception when multiple windows --- .../tray/new-windowTemplate-inverted.png | Bin 0 -> 348 bytes .../new-windowTemplate-inverted@1.25x.png | Bin 0 -> 427 bytes .../tray/new-windowTemplate-inverted@1.5x.png | Bin 0 -> 514 bytes .../tray/new-windowTemplate-inverted@2x.png | Bin 0 -> 649 bytes images/app-icons/tray/new-windowTemplate.png | Bin 0 -> 331 bytes .../tray/new-windowTemplate@1.25x.png | Bin 0 -> 409 bytes .../tray/new-windowTemplate@1.5x.png | Bin 0 -> 481 bytes .../app-icons/tray/new-windowTemplate@2x.png | Bin 0 -> 626 bytes src/services/tray.ts | 155 +++++++++++++----- src/services/window.ts | 48 +++++- translations/en/server.json | 3 +- 11 files changed, 156 insertions(+), 50 deletions(-) create mode 100644 images/app-icons/tray/new-windowTemplate-inverted.png create mode 100644 images/app-icons/tray/new-windowTemplate-inverted@1.25x.png create mode 100644 images/app-icons/tray/new-windowTemplate-inverted@1.5x.png create mode 100644 images/app-icons/tray/new-windowTemplate-inverted@2x.png create mode 100644 images/app-icons/tray/new-windowTemplate.png create mode 100644 images/app-icons/tray/new-windowTemplate@1.25x.png create mode 100644 images/app-icons/tray/new-windowTemplate@1.5x.png create mode 100644 images/app-icons/tray/new-windowTemplate@2x.png diff --git a/images/app-icons/tray/new-windowTemplate-inverted.png b/images/app-icons/tray/new-windowTemplate-inverted.png new file mode 100644 index 0000000000000000000000000000000000000000..b587fe0bbfbc27183bf610e90242885d6edcb539 GIT binary patch literal 348 zcmV-i0i*tjP)wJ0!)A#1C)P64hFae?vR9} zsWxA?lKKn;yaA8SN(0dgpaagLAp+FEGjR1!0_T3y*DKdw0b^jtdR!yh#`cM1_ZdWx zF40@y*l9mGSZ7gQLjJ$sPH|@a2>4v*Uk-dz4Lkq`z`oD*x@IdghAZzUzz6VZSpcV* z&#w}e2?uk_z4h}f@G=HZ0S~y4Kd<@A;XNq zvlTPTtvPZtTQ;*{%@$R|CdW4IT>@|lern+n_^4S^pEC{2i(^PwDq$bE_@%-fpf8Rg zp+`%^4#!G-=5feGunX*Io>gsd6>DDE0&uKS+yf6) zLedHcOJD-%0W)&52TTSg)+Pj)0?vVrpjPX0v|U$W8pPUC)CHb2y1;2bn8q9~a%`aE zl)wvcqV+>P&qjzEgl@a&{&n;Td3Rrd=iq8MU=ElKOzUXx_kC1zSR2zEjPrG5QV=Uk7I#7QCMUX*sx&164rKoLIVdN@8W_rScD0NW{V)pkOHy&{Ax~v=(ag57>yEptWf&6HqJ# zg)~Vt`F3|~-hsnr*S$C&8CZ7r?Cg2pnK@S%Lf|Mz-XeEX!V6slJWw@FOgJLOAp%Yx zGGJ~BBpmPuDD@mHfhf2JJOh>!A4OcuCa?;8ikL2h;5FeK@EJHYWxM+UTm`-Z&g)D9 za8cj~hIPsd={_T@0QkBM1l>z(C<%=(V)!;Mh-aSZNMrx2Zg&~D z-zN}x;3M!M@#=Sg2Yn;qqQ2!oks#U@(%Cx{mj<}#2rl#pT7`7>CNMg-r%FbB!YaUV zk=PkaC?5$P^TumE-!#4*hPudDw^|kO3h+oWKStS*k;#~@15X560*`?kdd{XrAc8Lg zFMwm_I5OZlaA7D3Kj7R;U>;ZpPAjoE={S(=4X|Kxei8z4l_!9AN~Io{F>_`ly9B%k zPMZIcctOku>%c3$UooL|O|^tKi>gu)h;FqFyfyM&L!d2pgs}uN8ChOiMP@?lVoUCz zR@mmoP&*04f@B+*=(1yCyrFE!9ibuSPs|P;+LgQkypUhtH5E+!wh!FV_BU?$x}*=7t=&-Er(cg#}RM##+Auw}7X>d9wn$J9~z3 zp?90ID^$r#G(`a0|dv={SIuH?T5BXe^clcf+IL8!~yi z0VbL3%)Hre{==R!&Zi6@IKw5-1!7|NWN$T4d4Lia3*cBbTLC-)&t{wo{T!YEbX;m* zz(Tgz5dKbO$C`b}z(5+O*TAjPep9eU z9Mclg`Ti=!ne-dr=Rg0fWlRO|3iN?1$%Wop0fHg~U;wOuPw^5MCO&^k*tj0d#V^wD zlfWAb?g6fWkviOHxpEiiLEl=wA0Dv;zExwiJy#QFk}JJ!RZ^KAQ!m+zN(Y9MgKPBq d*fdT$=N-zgNZA9LKn?%^002ovPDHLkV1mqFi_`!B literal 0 HcmV?d00001 diff --git a/images/app-icons/tray/new-windowTemplate@1.25x.png b/images/app-icons/tray/new-windowTemplate@1.25x.png new file mode 100644 index 0000000000000000000000000000000000000000..b52f2158d5ac4d95dfbd4cbf82f7cc5dafa810c5 GIT binary patch literal 409 zcmV;K0cQS*P)YU6vlt|=2o$~IQRe+2VMMw?mD=ZqV#!O-FyfYItkJT5Yz`ya1`7{L`0+@P2%!h zJ;o;5=m#eW=YPI)a^v_!0(j~n15&GS{2eY{9FPNBl6;H;(f0y4vvDiHP!4ru2rOeb zTNH}V zC@713Sc%C}^PG+F{Z_dQi4yDpyLwKl0z!o?cD4Ws1egL&fepjeI?xB2zwOW)T~i%(fqO|8I5r7=F-Is&Y@pZ4fJfj+_I=$?Ml?0B z(+-9GYv~h;(|rOS%xVN+4wyAVYbo~2KCYD<4hOQoNRY1RmjbJ?q64mgZD3Olao^6B zWX}}u1J6q06@Lj_+sXgy0bBJ8NY!oRdMKZy>=*D3C!%bA!HCNs00000NkvXXu0mjf DAmOSF literal 0 HcmV?d00001 diff --git a/images/app-icons/tray/new-windowTemplate@1.5x.png b/images/app-icons/tray/new-windowTemplate@1.5x.png new file mode 100644 index 0000000000000000000000000000000000000000..61172b73248887298524200e378fcb76181deecc GIT binary patch literal 481 zcmV<70UrK|P)Inm;DAry7I+LT zsR6A(Xj6KZ&ILjZIFKcqjyZ7X0c>F2TyFW=5TGetE(8Fm06)MfkP6HdRO)3Lz?*at z19(e%z<0^pPvQu)8A9HYEDKCdB6-cIP&wwc~1J0v86X+R8SF;F!x9}uN#I|_e3#9-O(H`(5#Z!?3o`HIFk+lH4 z`F9H2ZeT%Iy!+@a3jyp0w=ybz4d7TZL#-Q8VKh3s1Yk3o#3Ty256!cH=aVtbNVKK! zw$X#mRP=0+JO=11iCmP7%|OPN3McoPxY4w=naWH*cl6M^z*w{^UMG@GrK1H@!$=tc zm%vM2Mh11Nw_@J`ufUZqlEhuB>g#xPaG=>oMqe(dXLP=C*fRgU>6<@(Lrl(h?r+O4 XxGJCsS42yu00000NkvXXu0mjfzoF4= literal 0 HcmV?d00001 diff --git a/images/app-icons/tray/new-windowTemplate@2x.png b/images/app-icons/tray/new-windowTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ced05691f3434a8bfe412c9fed279be3363737 GIT binary patch literal 626 zcmV-&0*(ENP)=L;ko_j#}VOz-U)xX(EIvFk1(0 z3vd4ga--`QKwzu_FM!34k0KcJ5m*C0SP)J>SAlP0PIBx6SESQ90VshbU;#KXsJk3! z?@Q8E0?tQbAkjG$69~Sb<2Vp~Mf#;0T~jd}S3a^R1=fK*AuT$vyZ}~!hr^S;Lf}2{ zu4C&DfJb!z0Q5BHfox30VUJ$bWQ=m7IIB_0M-{D1d)5ZfS#7W5o^hJkd4V`+3&^)r zo+z?k>AP=oyKI^wZPRK^ithqX#B*&nYeK1l={E38N^{^Ta7W+iqyPlvi@*jjWsV~S zUII6of^ZGaJqPB1ZQ!CBOQJoF#Cr{#Gd;f(0Kv)&z+2#!_4 zA8Z3#`oC#P>pHM25U#56v;`2->L>8V@P8QqRlTEZ3m{dH`5`M(Q(Ai!y(3s(8XZYh zVEPE+MAi;8EDcOHzkt(CI4L)V8Wd+MC)hZ9`GEvZY;3g`EAf=`nN5+0KTdg z#NOM}(5jUc`vS)+-0hnmYHh_DA3IG5A M07*qoM6N<$f^Iq!cK`qY literal 0 HcmV?d00001 diff --git a/src/services/tray.ts b/src/services/tray.ts index c35e2c823..ab1dc0af0 100644 --- a/src/services/tray.ts +++ b/src/services/tray.ts @@ -1,4 +1,4 @@ -import { Menu, Tray } from "electron"; +import { Menu, Tray, BrowserWindow } from "electron"; import path from "path"; import windowService from "./window.js"; import optionService from "./options.js"; @@ -17,7 +17,7 @@ import cls from "./cls.js"; let tray: Tray; // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // is minimized -let isVisible = true; +let windowVisibilityMap: Record = {};; // Dictionary for storing window ID and its visibility status function getTrayIconPath() { let name: string; @@ -37,53 +37,87 @@ function getIconPath(name: string) { return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "tray", `${name}Template${suffix}.png`); } -function registerVisibilityListener() { - const mainWindow = windowService.getMainWindow(); - if (!mainWindow) { +function registerVisibilityListener(window: BrowserWindow) { + if (!window) { return; } // They need to be registered before the tray updater is registered - mainWindow.on("show", () => { - isVisible = true; + window.on("show", () => { + windowVisibilityMap[window.id] = true; updateTrayMenu(); }); - mainWindow.on("hide", () => { - isVisible = false; + window.on("hide", () => { + windowVisibilityMap[window.id] = false; updateTrayMenu(); }); - mainWindow.on("minimize", updateTrayMenu); - mainWindow.on("maximize", updateTrayMenu); - if (!isMac) { - // macOS uses template icons which work great on dark & light themes. - nativeTheme.on("updated", updateTrayMenu); - } - ipcMain.on("reload-tray", updateTrayMenu); - i18next.on("languageChanged", updateTrayMenu); + window.on("minimize", updateTrayMenu); + window.on("maximize", updateTrayMenu); } -function updateTrayMenu() { - const mainWindow = windowService.getMainWindow(); - if (!mainWindow) { +function getWindowTitle(window: BrowserWindow | null) { + if (!window) { return; } + const title = window.getTitle(); + const titleWithoutAppName = title.replace(/\s-\s[^-]+$/, ''); // Remove the name of the app - function ensureVisible() { - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); + // Limit title maximum length to 17 + if (titleWithoutAppName.length > 20) { + return titleWithoutAppName.substring(0, 17) + '...'; + } + + return titleWithoutAppName; +} + +function updateWindowVisibilityMap(allWindows: BrowserWindow[]) { + const currentWindowIds: number[] = allWindows.map(window => window.id); + + // Deleting closed windows from windowVisibilityMap + for (const [id, visibility] of Object.entries(windowVisibilityMap)) { + const windowId = Number(id); + if (!currentWindowIds.includes(windowId)) { + delete windowVisibilityMap[windowId]; + } + } + + // Iterate through allWindows to make sure the ID of each window exists in windowVisibilityMap + allWindows.forEach(window => { + const windowId = window.id; + if (!(windowId in windowVisibilityMap)) { + // If it does not exist, it is the newly created window + windowVisibilityMap[windowId] = true; + registerVisibilityListener(window); + } + }); +} + + +function updateTrayMenu() { + const lastFocusedWindow = windowService.getLastFocusedWindow(); + const allWindows = windowService.getAllWindows(); + updateWindowVisibilityMap(allWindows); + + function ensureVisible(win: BrowserWindow) { + if (win) { + win.show(); + win.focus(); } } function triggerKeyboardAction(actionName: KeyboardActionNames) { - mainWindow?.webContents.send("globalShortcut", actionName); - ensureVisible(); + if (lastFocusedWindow){ + lastFocusedWindow.webContents.send("globalShortcut", actionName); + ensureVisible(lastFocusedWindow); + } } function openInSameTab(note: BNote | BRecentNote) { - mainWindow?.webContents.send("openInSameTab", note.noteId); - ensureVisible(); + if (lastFocusedWindow){ + lastFocusedWindow.webContents.send("openInSameTab", note.noteId); + ensureVisible(lastFocusedWindow); + } } function buildBookmarksMenu() { @@ -144,20 +178,44 @@ function updateTrayMenu() { return menuItems; } - const contextMenu = Menu.buildFromTemplate([ - { - label: t("tray.show-windows"), + const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = []; + + // Only call getWindowTitle if windowVisibilityMap has more than one window + const showTitle = Object.keys(windowVisibilityMap).length > 1; + + for (const idStr in windowVisibilityMap) { + const id = parseInt(idStr, 10); // Get the ID of the window and make sure it is a number + const isVisible = windowVisibilityMap[id]; + const win = allWindows.find(w => w.id === id); + if (!win) { + continue; + } + windowVisibilityMenuItems.push({ + label: showTitle ? `${t("tray.show-windows")}: ${getWindowTitle(win)}` : t("tray.show-windows"), type: "checkbox", checked: isVisible, click: () => { if (isVisible) { - mainWindow.hide(); + win.hide(); + windowVisibilityMap[id] = false; } else { - ensureVisible(); + ensureVisible(win); + windowVisibilityMap[id] = true; } } - }, + }); + } + + + const contextMenu = Menu.buildFromTemplate([ + ...windowVisibilityMenuItems, { type: "separator" }, + { + label: t("tray.open_new_window"), + type: "normal", + icon: getIconPath("new-window"), + click: () => triggerKeyboardAction("openNewWindow") + }, { label: t("tray.new-note"), type: "normal", @@ -188,7 +246,10 @@ function updateTrayMenu() { type: "normal", icon: getIconPath("close"), click: () => { - mainWindow.close(); + const windows = BrowserWindow.getAllWindows(); + windows.forEach(window => { + window.close(); + }); } } ]); @@ -197,16 +258,18 @@ function updateTrayMenu() { } function changeVisibility() { - const window = windowService.getMainWindow(); - if (!window) { + const lastFocusedWindow = windowService.getLastFocusedWindow(); + + if (!lastFocusedWindow) { return; } - if (isVisible) { - window.hide(); + // If the window is visible, hide it + if (windowVisibilityMap[lastFocusedWindow.id]) { + lastFocusedWindow.hide(); } else { - window.show(); - window.focus(); + lastFocusedWindow.show(); + lastFocusedWindow.focus(); } } @@ -221,9 +284,15 @@ function createTray() { tray.on("click", changeVisibility); updateTrayMenu(); - registerVisibilityListener(); + if (!isMac) { + // macOS uses template icons which work great on dark & light themes. + nativeTheme.on("updated", updateTrayMenu); + } + ipcMain.on("reload-tray", updateTrayMenu); + i18next.on("languageChanged", updateTrayMenu); } export default { - createTray + createTray, + updateTrayMenu }; diff --git a/src/services/window.ts b/src/services/window.ts index 8b5697609..12c125b6d 100644 --- a/src/services/window.ts +++ b/src/services/window.ts @@ -11,6 +11,7 @@ import remoteMain from "@electron/remote/main/index.js"; import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron"; import { dialog, ipcMain } from "electron"; import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; +import tray from "./tray.js"; import { fileURLToPath } from "url"; import { dirname } from "path"; @@ -19,6 +20,26 @@ import { t } from "i18next"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; let setupWindow: BrowserWindow | null; +let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus. + +function trackWindowFocus(win: BrowserWindow) { + // We need to get the last focused window from allWindows. If the last window is closed, we return the previous window. + // Therefore, we need to push the window into the allWindows array every time it gets focused. + win.on("focus", () => { + allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win); + allWindows.push(win); + if (!optionService.getOptionBool("disableTray")) { + tray.updateTrayMenu(); + } + }); + + win.on("closed", () => { + allWindows = allWindows.filter(w => !w.isDestroyed()); + if (!optionService.getOptionBool("disableTray")) { + tray.updateTrayMenu(); + } + }); +} async function createExtraWindow(extraWindowHash: string) { const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); @@ -42,6 +63,8 @@ async function createExtraWindow(extraWindowHash: string) { win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`); configureWebContents(win.webContents, spellcheckEnabled); + + trackWindowFocus(win); } ipcMain.on("create-extra-window", (event, arg) => { @@ -154,18 +177,21 @@ async function createMainWindow(app: App) { configureWebContents(mainWindow.webContents, spellcheckEnabled); app.on("second-instance", (event, commandLine) => { + const lastFocusedWindow = getLastFocusedWindow(); if (commandLine.includes("--new-window")) { createExtraWindow(""); - } else if (mainWindow) { + } else if (lastFocusedWindow) { // Someone tried to run a second instance, we should focus our window. // see www.ts "requestSingleInstanceLock" for the rest of this logic with explanation - if (mainWindow.isMinimized()) { - mainWindow.restore(); + if (lastFocusedWindow.isMinimized()) { + lastFocusedWindow.restore(); } - mainWindow.show(); - mainWindow.focus(); + lastFocusedWindow.show(); + lastFocusedWindow.focus(); } }); + + trackWindowFocus(mainWindow); } function getWindowExtraOpts() { @@ -296,10 +322,20 @@ function getMainWindow() { return mainWindow; } +function getLastFocusedWindow() { + return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null; +} + +function getAllWindows(){ + return allWindows; +} + export default { createMainWindow, createSetupWindow, closeSetupWindow, registerGlobalShortcuts, - getMainWindow + getMainWindow, + getLastFocusedWindow, + getAllWindows }; diff --git a/translations/en/server.json b/translations/en/server.json index f25927fd6..6267a0060 100644 --- a/translations/en/server.json +++ b/translations/en/server.json @@ -272,7 +272,8 @@ "bookmarks": "Bookmarks", "today": "Open today's journal note", "new-note": "New note", - "show-windows": "Show windows" + "show-windows": "Show windows", + "open_new_window": "Open new window" }, "migration": { "old_version": "Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.",