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 000000000..b587fe0bb Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate-inverted.png differ diff --git a/images/app-icons/tray/new-windowTemplate-inverted@1.25x.png b/images/app-icons/tray/new-windowTemplate-inverted@1.25x.png new file mode 100644 index 000000000..0ca0d82ce Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate-inverted@1.25x.png differ diff --git a/images/app-icons/tray/new-windowTemplate-inverted@1.5x.png b/images/app-icons/tray/new-windowTemplate-inverted@1.5x.png new file mode 100644 index 000000000..6a25dc41a Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate-inverted@1.5x.png differ diff --git a/images/app-icons/tray/new-windowTemplate-inverted@2x.png b/images/app-icons/tray/new-windowTemplate-inverted@2x.png new file mode 100644 index 000000000..ba61b5177 Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate-inverted@2x.png differ diff --git a/images/app-icons/tray/new-windowTemplate.png b/images/app-icons/tray/new-windowTemplate.png new file mode 100644 index 000000000..c4ef4e41e Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate.png differ 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 000000000..b52f2158d Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate@1.25x.png differ 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 000000000..61172b732 Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate@1.5x.png differ 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 000000000..a1ced0569 Binary files /dev/null and b/images/app-icons/tray/new-windowTemplate@2x.png differ 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.",