Fix tray exception when multiple windows
BIN
images/app-icons/tray/new-windowTemplate-inverted.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
images/app-icons/tray/new-windowTemplate-inverted@1.25x.png
Normal file
After Width: | Height: | Size: 427 B |
BIN
images/app-icons/tray/new-windowTemplate-inverted@1.5x.png
Normal file
After Width: | Height: | Size: 514 B |
BIN
images/app-icons/tray/new-windowTemplate-inverted@2x.png
Normal file
After Width: | Height: | Size: 649 B |
BIN
images/app-icons/tray/new-windowTemplate.png
Normal file
After Width: | Height: | Size: 331 B |
BIN
images/app-icons/tray/new-windowTemplate@1.25x.png
Normal file
After Width: | Height: | Size: 409 B |
BIN
images/app-icons/tray/new-windowTemplate@1.5x.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
images/app-icons/tray/new-windowTemplate@2x.png
Normal file
After Width: | Height: | Size: 626 B |
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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.",
|
||||||
|