From 95db053bd07dabb40080d42a54ebb8d60fcb25ba Mon Sep 17 00:00:00 2001 From: Oleksii Skorobogatko Date: Sun, 15 Dec 2024 18:37:44 +0200 Subject: [PATCH] #4 append and update first N clipboard items in tray menu --- src/main/main.js | 35 +++++++++++++++++--------- src/main/system.js | 63 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/main/main.js b/src/main/main.js index 6de733d..7979d35 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -7,6 +7,7 @@ import { clipboardEventEmitter } from './clipboard'; import { keyboard } from '../renderer/keyshortcuts'; import { setStartAppAtLogin, isPlatformLinux, isPlatformDarwin, saveImage, saveText, quitApp, + updateTrayContextMenu, } from './system'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. @@ -15,9 +16,20 @@ if (require('electron-squirrel-startup')) { app.quit(); } +class AppBrowserWindow extends BrowserWindow { + /** @type {?Electron.Menu} */ + trayContextMenu = null; + /** @type {?Electron.Tray} */ + tray = null; + + toggleVisibility() { + this.isVisible() ? this.hide() : this.show(); + }; +} + const createMainWindow = () => { // Create the browser window. - const mainWindow = new BrowserWindow({ + const mainWindow = new AppBrowserWindow({ width: 380, height: 640, minWidth: 260, @@ -70,28 +82,23 @@ const createMainWindow = () => { ipcMain.on('open:url', (_, url) => shell.openExternal(url)); ipcMain.on('save:image', (_, image) => saveImage(mainWindow, image)); ipcMain.on('save:text', (_, text) => saveText(mainWindow, text)); + // updates clipboard items in tray context menu + ipcMain.on('clipboard:top', (_, clips) => updateTrayContextMenu(mainWindow.tray, mainWindow.trayContextMenu, clips)); return mainWindow; }; // Create and setup the application tray icon. /** - * @param {BrowserWindow} mainWindow + * @param {AppBrowserWindow} mainWindow */ const createTrayIcon = (mainWindow) => { const icon = nativeImage.createFromPath(path.join(__dirname, '..', '..', 'resources', 'icon.png')); const tray = new Tray(icon); - const showHideCallback = () => { - if (mainWindow.isVisible()) { - mainWindow.hide(); - } else { - mainWindow.show(); - } - }; - const contextMenu = Menu.buildFromTemplate([ { + id: 'quit', label: 'Quit', click: quitApp, }, @@ -101,8 +108,9 @@ const createTrayIcon = (mainWindow) => { // There is a menu item does the same as clicking on the icon to show/hide the main window. if (isPlatformLinux()) { contextMenu.insert(0, new MenuItem({ + id: 'lnx-show-toggle', label: 'Show/Hide', - click: () => showHideCallback(), + click: () => mainWindow.toggleVisibility(), })); contextMenu.insert(1, new MenuItem({ type: 'separator' })); } @@ -110,7 +118,10 @@ const createTrayIcon = (mainWindow) => { tray.setContextMenu(contextMenu); tray.setToolTip('Pasted'); - tray.on('click', () => showHideCallback()); + tray.on('click', () => mainWindow.toggleVisibility()); + + mainWindow.trayContextMenu = contextMenu; + mainWindow.tray = tray; }; /** diff --git a/src/main/system.js b/src/main/system.js index f7cb8e6..7382236 100644 --- a/src/main/system.js +++ b/src/main/system.js @@ -1,7 +1,10 @@ -import { app, dialog, nativeImage } from 'electron'; +import { + app, dialog, nativeImage, MenuItem, Menu, +} from 'electron'; import path from 'node:path'; import { writeFile } from 'node:fs/promises'; import * as linux from './linux'; +import { clipboardEventEmitter } from './clipboard'; /** * @returns {boolean} @@ -150,6 +153,63 @@ async function saveText(parentWindow, text, filename) { } } +/** + * Updates the application tray icon menu. + * + * @param {Electron.Tray} tray + * @param {Electron.Menu} contextMenu The tray context menu. + * @param {import('../models/clip').Model[]} clipItems=[] Appends the clipboard items to the context menu. + * @returns {Electron.Menu} The modified context menu. + */ +function updateTrayContextMenu(tray, contextMenu, clipItems) { + // Remove previously added clipboard items. + const menuItems = contextMenu.items.filter((item) => ! item.id?.startsWith('clipboard--')); + + contextMenu = Menu.buildFromTemplate(menuItems); + + if (clipItems?.length > 0) { + contextMenu.insert(0, new MenuItem({ + id: 'clipboard--sep', + type: 'separator', + })); + + for (const clipItem of clipItems) { + const label = stringCut(clipItem.data, 50); + contextMenu.insert(0, new MenuItem({ + id: `clipboard--item-${clipItem.id}`, + label, + click: () => clipboardEventEmitter.copy(clipItem), + })); + } + } + + // Linux: In order for changes made to individual MenuItems to take effect, you have to call setContextMenu again. + // https://www.electronjs.org/docs/latest/api/tray + if (isPlatformLinux()) { + tray.setContextMenu(contextMenu); + } + + return contextMenu; +} + +/** + * Cuts a string to the specified limit of characters. + * + * @param {string} str + * @param {number} limit A desired string limit. + * @param {string} trail='...' A trail appended to the end of the string longer than limit. + * @return {string} + */ +function stringCut(str, limit, trail = '...') { + const cutStr = str.trim(); + + if (cutStr.length <= limit) { + return cutStr; + } + + return cutStr.slice(0, limit) + trail; +} + export { isPlatformLinux, isPlatformWindows, @@ -158,4 +218,5 @@ export { saveImage, saveText, quitApp, + updateTrayContextMenu, };