Fix tray exception when multiple windows

This commit is contained in:
SiriusXT 2025-03-21 11:08:33 +08:00
parent 606e6bcca2
commit be2064fbf0
11 changed files with 156 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

View File

@ -1,4 +1,4 @@
import { Menu, Tray } from "electron"; import { Menu, Tray, BrowserWindow } from "electron";
import path from "path"; import path from "path";
import windowService from "./window.js"; import windowService from "./window.js";
import optionService from "./options.js"; import optionService from "./options.js";
@ -17,7 +17,7 @@ import cls from "./cls.js";
let tray: Tray; let tray: Tray;
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
// is minimized // is minimized
let isVisible = true; let windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status
function getTrayIconPath() { function getTrayIconPath() {
let name: string; 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`); return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "tray", `${name}Template${suffix}.png`);
} }
function registerVisibilityListener() { function registerVisibilityListener(window: BrowserWindow) {
const mainWindow = windowService.getMainWindow(); if (!window) {
if (!mainWindow) {
return; return;
} }
// They need to be registered before the tray updater is registered // They need to be registered before the tray updater is registered
mainWindow.on("show", () => { window.on("show", () => {
isVisible = true; windowVisibilityMap[window.id] = true;
updateTrayMenu(); updateTrayMenu();
}); });
mainWindow.on("hide", () => { window.on("hide", () => {
isVisible = false; windowVisibilityMap[window.id] = false;
updateTrayMenu(); updateTrayMenu();
}); });
mainWindow.on("minimize", updateTrayMenu); window.on("minimize", updateTrayMenu);
mainWindow.on("maximize", updateTrayMenu); window.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);
} }
function updateTrayMenu() { function getWindowTitle(window: BrowserWindow | null) {
const mainWindow = windowService.getMainWindow(); if (!window) {
if (!mainWindow) {
return; return;
} }
const title = window.getTitle();
const titleWithoutAppName = title.replace(/\s-\s[^-]+$/, ''); // Remove the name of the app
function ensureVisible() { // Limit title maximum length to 17
if (mainWindow) { if (titleWithoutAppName.length > 20) {
mainWindow.show(); return titleWithoutAppName.substring(0, 17) + '...';
mainWindow.focus(); }
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) { function triggerKeyboardAction(actionName: KeyboardActionNames) {
mainWindow?.webContents.send("globalShortcut", actionName); if (lastFocusedWindow){
ensureVisible(); lastFocusedWindow.webContents.send("globalShortcut", actionName);
ensureVisible(lastFocusedWindow);
}
} }
function openInSameTab(note: BNote | BRecentNote) { function openInSameTab(note: BNote | BRecentNote) {
mainWindow?.webContents.send("openInSameTab", note.noteId); if (lastFocusedWindow){
ensureVisible(); lastFocusedWindow.webContents.send("openInSameTab", note.noteId);
ensureVisible(lastFocusedWindow);
}
} }
function buildBookmarksMenu() { function buildBookmarksMenu() {
@ -144,20 +178,44 @@ function updateTrayMenu() {
return menuItems; return menuItems;
} }
const contextMenu = Menu.buildFromTemplate([ const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
{
label: t("tray.show-windows"), // 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", type: "checkbox",
checked: isVisible, checked: isVisible,
click: () => { click: () => {
if (isVisible) { if (isVisible) {
mainWindow.hide(); win.hide();
windowVisibilityMap[id] = false;
} else { } else {
ensureVisible(); ensureVisible(win);
windowVisibilityMap[id] = true;
} }
} }
}, });
}
const contextMenu = Menu.buildFromTemplate([
...windowVisibilityMenuItems,
{ type: "separator" }, { type: "separator" },
{
label: t("tray.open_new_window"),
type: "normal",
icon: getIconPath("new-window"),
click: () => triggerKeyboardAction("openNewWindow")
},
{ {
label: t("tray.new-note"), label: t("tray.new-note"),
type: "normal", type: "normal",
@ -188,7 +246,10 @@ function updateTrayMenu() {
type: "normal", type: "normal",
icon: getIconPath("close"), icon: getIconPath("close"),
click: () => { click: () => {
mainWindow.close(); const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
window.close();
});
} }
} }
]); ]);
@ -197,16 +258,18 @@ function updateTrayMenu() {
} }
function changeVisibility() { function changeVisibility() {
const window = windowService.getMainWindow(); const lastFocusedWindow = windowService.getLastFocusedWindow();
if (!window) {
if (!lastFocusedWindow) {
return; return;
} }
if (isVisible) { // If the window is visible, hide it
window.hide(); if (windowVisibilityMap[lastFocusedWindow.id]) {
lastFocusedWindow.hide();
} else { } else {
window.show(); lastFocusedWindow.show();
window.focus(); lastFocusedWindow.focus();
} }
} }
@ -221,9 +284,15 @@ function createTray() {
tray.on("click", changeVisibility); tray.on("click", changeVisibility);
updateTrayMenu(); 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 { export default {
createTray createTray,
updateTrayMenu
}; };

View File

@ -11,6 +11,7 @@ import remoteMain from "@electron/remote/main/index.js";
import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron"; import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron";
import { dialog, ipcMain } from "electron"; import { dialog, ipcMain } from "electron";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import tray from "./tray.js";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname } from "path"; import { dirname } from "path";
@ -19,6 +20,26 @@ import { t } from "i18next";
// Prevent the window being garbage collected // Prevent the window being garbage collected
let mainWindow: BrowserWindow | null; let mainWindow: BrowserWindow | null;
let setupWindow: 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) { async function createExtraWindow(extraWindowHash: string) {
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); 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}`); win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
configureWebContents(win.webContents, spellcheckEnabled); configureWebContents(win.webContents, spellcheckEnabled);
trackWindowFocus(win);
} }
ipcMain.on("create-extra-window", (event, arg) => { ipcMain.on("create-extra-window", (event, arg) => {
@ -154,18 +177,21 @@ async function createMainWindow(app: App) {
configureWebContents(mainWindow.webContents, spellcheckEnabled); configureWebContents(mainWindow.webContents, spellcheckEnabled);
app.on("second-instance", (event, commandLine) => { app.on("second-instance", (event, commandLine) => {
const lastFocusedWindow = getLastFocusedWindow();
if (commandLine.includes("--new-window")) { if (commandLine.includes("--new-window")) {
createExtraWindow(""); createExtraWindow("");
} else if (mainWindow) { } else if (lastFocusedWindow) {
// Someone tried to run a second instance, we should focus our window. // Someone tried to run a second instance, we should focus our window.
// see www.ts "requestSingleInstanceLock" for the rest of this logic with explanation // see www.ts "requestSingleInstanceLock" for the rest of this logic with explanation
if (mainWindow.isMinimized()) { if (lastFocusedWindow.isMinimized()) {
mainWindow.restore(); lastFocusedWindow.restore();
} }
mainWindow.show(); lastFocusedWindow.show();
mainWindow.focus(); lastFocusedWindow.focus();
} }
}); });
trackWindowFocus(mainWindow);
} }
function getWindowExtraOpts() { function getWindowExtraOpts() {
@ -296,10 +322,20 @@ function getMainWindow() {
return mainWindow; return mainWindow;
} }
function getLastFocusedWindow() {
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
}
function getAllWindows(){
return allWindows;
}
export default { export default {
createMainWindow, createMainWindow,
createSetupWindow, createSetupWindow,
closeSetupWindow, closeSetupWindow,
registerGlobalShortcuts, registerGlobalShortcuts,
getMainWindow getMainWindow,
getLastFocusedWindow,
getAllWindows
}; };

View File

@ -272,7 +272,8 @@
"bookmarks": "Bookmarks", "bookmarks": "Bookmarks",
"today": "Open today's journal note", "today": "Open today's journal note",
"new-note": "New note", "new-note": "New note",
"show-windows": "Show windows" "show-windows": "Show windows",
"open_new_window": "Open new window"
}, },
"migration": { "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.", "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.",