Files
heynote/electron/main/index.ts
Jonatan Heyman 3107cb5368 Add right click context menu and improve application menu
- Add right-click context menu with undo/redo/cut/copy/paste/select all options, as well as Delete block and Move block to another buffer
- Add Delete Block and Move Block to another buffer in the application menu
- Change so that we use our custom "undo" event instead of relying on the default undo event, since there were cases when the default undo didn't trigger the CodeMirror undo reliably
2025-01-09 17:19:59 +01:00

444 lines
15 KiB
TypeScript

import { app, BrowserWindow, Tray, shell, ipcMain, Menu, nativeTheme, globalShortcut, nativeImage, screen } from 'electron'
import { release } from 'node:os'
import { join } from 'node:path'
import fs from "fs"
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '@/src/common/constants'
import { menu, getTrayMenu, getEditorContextMenu } from './menu'
import CONFIG from "../config"
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
import { fixElectronCors } from './cors';
import {
FileLibrary,
setupFileLibraryEventHandlers,
setCurrentFileLibrary,
migrateBufferFileToLibrary,
NOTES_DIR_NAME
} from './file-library';
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.DIST_ELECTRON = join(__dirname, '..')
process.env.DIST = join(process.env.DIST_ELECTRON, '../dist')
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, '../public')
: process.env.DIST
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// Set application name for Windows 10+ notifications
if (isWindows) app.setAppUserModelId(app.getName())
if (!process.env.VITE_DEV_SERVER_URL && !app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
// Set custom application menu
Menu.setApplicationMenu(menu)
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
export let win: BrowserWindow | null = null
let fileLibrary: FileLibrary | null = null
let tray: Tray | null = null;
let initErrors: string[] = []
// Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html')
let currentKeymap = CONFIG.get("settings.keymap")
// if this version is a beta version, set the release channel to beta
const isBetaVersion = app.getVersion().includes("beta")
if (isBetaVersion) {
CONFIG.set("settings.allowBetaVersions", true)
}
let forceQuit = false
export function setForceQuit() {
forceQuit = true
}
export function quit() {
setForceQuit()
app.quit()
}
async function createWindow() {
// read any stored window settings from config, or use defaults
let windowConfig = {
width: CONFIG.get("windowConfig.width", 900) as number,
height: CONFIG.get("windowConfig.height", 680) as number,
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
x: CONFIG.get("windowConfig.x"),
y: CONFIG.get("windowConfig.y"),
}
// windowConfig.x and windowConfig.y will be undefined when config file is missing, e.g. first time run
if (windowConfig.x !== undefined && windowConfig.y !== undefined) {
// check if window is outside of screen, or too large
const area = screen.getDisplayMatching({
x: windowConfig.x,
y: windowConfig.y,
width: windowConfig.width,
height: windowConfig.height,
}).workArea
if (windowConfig.width > area.width) {
windowConfig.width = area.width
}
if (windowConfig.height > area.height) {
windowConfig.height = area.height
}
if (windowConfig.x + windowConfig.width > area.width || windowConfig.y + windowConfig.height > area.height) {
// window is outside of screen, reset position
windowConfig.x = undefined
windowConfig.y = undefined
}
}
const pngSystems: NodeJS.Platform[] = ["linux", "freebsd", "openbsd", "netbsd"]
const icon = join(
process.env.PUBLIC,
pngSystems.includes(process.platform)
? "favicon-linux.png"
: "favicon.ico",
)
win = new BrowserWindow(Object.assign({
title: 'heynote',
icon,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#262B37' : '#FFFFFF',
//titleBarStyle: 'customButtonsOnHover',
autoHideMenuBar: true,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: true,
},
}, windowConfig))
// maximize window if it was maximized last time
if (windowConfig.isMaximized) {
win.maximize()
}
if (windowConfig.isFullScreen) {
win.setFullScreen(true)
}
win.on("close", (event) => {
if (!forceQuit && CONFIG.get("settings.showInMenu")) {
event.preventDefault()
win.hide()
return
}
// Prevent the window from closing, and send a message to the renderer which will in turn
// send a message to the main process to save the current buffer and close the window.
if (!!fileLibrary && !fileLibrary.contentSaved) {
event.preventDefault()
win?.webContents.send(WINDOW_CLOSE_EVENT)
} else {
// save window config
Object.assign(windowConfig, {
isMaximized: win.isMaximized(),
isFullScreen: win.isFullScreen(),
}, win.getNormalBounds())
CONFIG.set("windowConfig", windowConfig)
}
})
win.on("hide", () => {
if (isWindows && CONFIG.get("settings.showInMenu")) {
win.setSkipTaskbar(true)
}
})
win.on("show", () => {
if (isWindows && CONFIG.get("settings.showInMenu")) {
win.setSkipTaskbar(false)
}
})
nativeTheme.themeSource = CONFIG.get("theme")
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
win.loadURL(url + '?dev=1')
// Open devTool if the app is not packaged
//win.webContents.openDevTools()
} else {
win.loadFile(indexHtml)
//win.webContents.openDevTools()
}
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString())
})
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith('https:') || url.startsWith('http:')) {
shell.openExternal(url)
}
return {action: 'deny'}
})
fixElectronCors(win)
}
function createTray() {
let img
if (isMac) {
img = nativeImage.createFromPath(join(process.env.PUBLIC, "iconTemplate.png"))
} else if (isLinux) {
img = nativeImage.createFromPath(join(process.env.PUBLIC, 'favicon-linux.png'));
} else{
img = nativeImage.createFromPath(join(process.env.PUBLIC, 'favicon.ico'));
}
tray = new Tray(img);
tray.setToolTip("Heynote");
const menu = getTrayMenu(win)
if (isMac) {
// using tray.setContextMenu() on macOS will open the menu on left-click, so instead we'll
// manually bind the right-click event to open the menu
tray.addListener("right-click", () => {
tray?.popUpContextMenu(menu)
})
} else {
tray.setContextMenu(menu);
}
tray.addListener("click", () => {
win?.show()
})
}
function registerGlobalHotkey() {
globalShortcut.unregisterAll()
if (CONFIG.get("settings.enableGlobalHotkey")) {
try {
const ret = globalShortcut.register(CONFIG.get("settings.globalHotkey"), () => {
if (!win) {
return
}
if (win.isFocused()) {
if (isMac) {
// app.hide() only available on macOS
// We want to use app.hide() so that the menu bar also gets changed
app?.hide()
if (CONFIG.get("settings.alwaysOnTop")) {
// if alwaysOnTop is on, calling app.hide() won't hide the window
win.hide()
}
} else if (isLinux) {
win.blur()
// If we don't hide the window, it will stay on top of the stack even though it's not visible
// and pressing the hotkey again won't do anything
win.hide()
} else {
win.blur()
if (CONFIG.get("settings.showInMenu") || CONFIG.get("settings.alwaysOnTop")) {
// if we're using a tray icon, or alwaysOnTop is on, we want to completely hide the window
win.hide()
}
}
} else {
app.focus({steal: true})
if (win.isMinimized()) {
win.restore()
}
if (!win.isVisible()) {
win.show()
}
win.focus()
}
})
} catch (error) {
console.log("Could not register global hotkey:", error)
}
}
}
function registerShowInDock() {
// dock is only available on macOS
if (isMac) {
if (CONFIG.get("settings.showInDock")) {
app.dock.show().catch((error) => {
console.log("Could not show app in dock: ", error);
});
} else {
app.dock.hide();
}
}
}
function registerShowInMenu() {
if (CONFIG.get("settings.showInMenu")) {
createTray()
} else {
tray?.destroy()
}
}
function registerAlwaysOnTop() {
if (CONFIG.get("settings.alwaysOnTop")) {
const disableAlwaysOnTop = () => {
win.setAlwaysOnTop(true, "floating");
win.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true});
win.setFullScreenable(false);
}
// if we're in fullscreen mode, we need to exit fullscreen before we can set alwaysOnTop
if (win.isFullScreen()) {
// on Mac setFullScreen happens asynchronously, so we need to wait for the event before we can disable alwaysOnTop
win.once("leave-full-screen", disableAlwaysOnTop)
win.setFullScreen(false)
} else {
disableAlwaysOnTop()
}
} else {
win.setAlwaysOnTop(false);
win.setVisibleOnAllWorkspaces(false);
win.setFullScreenable(true);
}
}
app.whenReady().then(createWindow).then(async () => {
initFileLibrary(win).then(() => {
setupFileLibraryEventHandlers()
})
initializeAutoUpdate(win)
registerGlobalHotkey()
registerShowInDock()
registerShowInMenu()
registerAlwaysOnTop()
})
app.on("before-quit", () => {
// if CMD+Q is pressed, we want to quit the app even if we're using a Menu/Tray icon
setForceQuit()
})
app.on('window-all-closed', () => {
win = null
if (!isMac) app.quit()
})
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
})
app.on('activate', (event, hasVisibleWindows) => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
// show the window if it's hidden (e.g. the window was closed with "show in menu bar" setting turned on)
if (!allWindows[0].isVisible()) {
allWindows[0].show()
}
} else {
createWindow()
}
})
ipcMain.handle('dark-mode:set', (event, mode) => {
CONFIG.set("theme", mode)
nativeTheme.themeSource = mode
})
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
ipcMain.handle("setWindowTitle", (event, title) => {
win?.setTitle(title)
})
ipcMain.handle("showEditorContextMenu", () => {
getEditorContextMenu(win).popup({window:win});
})
// Initialize note/file library
async function initFileLibrary(win) {
await migrateBufferFileToLibrary(app)
const customLibraryPath = CONFIG.get("settings.bufferPath")
const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
const libraryPath = customLibraryPath ? customLibraryPath : defaultLibraryPath
//console.log("libraryPath", libraryPath)
// if we're using the default library path, and it doesn't exist (e.g. first time run), create it
if (!customLibraryPath && !fs.existsSync(defaultLibraryPath)) {
fs.mkdirSync(defaultLibraryPath)
}
try {
fileLibrary = new FileLibrary(libraryPath, win)
fileLibrary.setupWatcher()
} catch (error) {
initErrors.push(`Error: ${error.message}`)
}
setCurrentFileLibrary(fileLibrary)
}
ipcMain.handle("getInitErrors", () => {
return initErrors
})
ipcMain.handle('settings:set', async (event, settings) => {
if (settings.keymap !== CONFIG.get("settings.keymap")) {
currentKeymap = settings.keymap
}
let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey")
let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu");
let bufferPathChanged = settings.bufferPath !== CONFIG.get("settings.bufferPath");
let alwaysOnTopChanged = settings.alwaysOnTop !== CONFIG.get("settings.alwaysOnTop");
CONFIG.set("settings", settings)
win?.webContents.send(SETTINGS_CHANGE_EVENT, settings)
if (globalHotkeyChanged) {
registerGlobalHotkey()
}
if (showInDockChanged) {
registerShowInDock()
}
if (showInMenuChanged) {
registerShowInMenu()
}
if (alwaysOnTopChanged) {
registerAlwaysOnTop()
}
if (bufferPathChanged) {
console.log("bufferPath changed, closing existing file library")
fileLibrary.close()
console.log("initializing new file library")
initFileLibrary(win)
await win.webContents.send("library:pathChanged")
}
})