From a0ea239aa6d48f27f5413c553de6f260959b81ed Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 9 Aug 2026 00:09:42 +0100 Subject: [PATCH] Enhance main window controls and introduce application menu - Updated `mainWindow.js` to include a 'quit' action in the IPC handlers, allowing the application to close gracefully. - Added a new `WindowAppMenu` component to manage application menu items, including options for 'About', 'Check For Updates', and 'Quit'. - Refactored `DashboardNavigation` to integrate the new `WindowAppMenu`, improving the user interface for non-macOS platforms. - Adjusted window control handling in `window.js` to support the new menu actions, ensuring consistent behavior across the application. --- public/mainWindow.js | 12 +- .../Dashboard/common/DashboardNavigation.jsx | 17 +- .../Dashboard/common/WindowAppMenu.jsx | 256 ++++++++++++++++++ src/desktop/window.js | 8 +- 4 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 src/components/Dashboard/common/WindowAppMenu.jsx diff --git a/public/mainWindow.js b/public/mainWindow.js index 82de743..cddec0d 100644 --- a/public/mainWindow.js +++ b/public/mainWindow.js @@ -1,4 +1,4 @@ -import { BrowserWindow, ipcMain, Menu } from 'electron' +import { app, BrowserWindow, ipcMain, Menu } from 'electron' import path, { dirname } from 'path' import { fileURLToPath } from 'url' @@ -256,7 +256,7 @@ export function setupMainWindowIPC() { // IPC handlers for window controls ipcMain.on('window-control', (event, action) => { - if (!win) return + if (!win && action !== 'quit') return switch (action) { case 'minimize': win.minimize() @@ -274,6 +274,14 @@ export function setupMainWindowIPC() { case 'fullscreen': win.setFullScreen(!win.isFullScreen()) break + case 'quit': + app.quit() + break + case 'toggle-devtools': + if (win && !win.isDestroyed()) { + win.webContents.toggleDevTools() + } + break default: break } diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 25280d4..3253d6b 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -38,6 +38,7 @@ import SettingsIcon from '../../Icons/SettingsIcon' import DeveloperIcon from '../../Icons/DeveloperIcon' import { ElectronContext } from '../context/ElectronContext' import DashboardWindowButtons from './DashboardWindowButtons' +import WindowAppMenu from './WindowAppMenu' import WebAppSwitcher from './WebAppSwitcher' import { getSidebarDefaultPath, @@ -141,7 +142,6 @@ const DashboardNavigation = () => { setSidebarViewMenu(sections) }, [isElectron, setSidebarViewMenu]) - const showAppLogo = isElectron && platform != 'darwin' const isMacOSApp = isElectron && platform == 'darwin' const isOtherApp = isElectron && platform != 'darwin' @@ -153,22 +153,15 @@ const DashboardNavigation = () => { const navigationContents = ( {isMacOSApp ? : null} - {showAppLogo == true && ( + {isOtherApp ? ( <> - + {' '} - )} + ) : null} {showDesktopLogo == true ? ( { + try { + document.execCommand(command) + } catch (error) { + console.warn( + `[WindowAppMenu] Failed to run edit command: ${command}`, + error + ) + } +} + +const mapSidebarItemsToMenuItems = (items = [], navigate) => + items + .map((item, index) => { + if (item?.type === 'divider') { + return { type: 'divider', key: `divider-${index}` } + } + + if (item?.children?.length) { + return { + key: item.key || `group-${item.label}-${index}`, + label: item.label, + children: mapSidebarItemsToMenuItems(item.children, navigate) + } + } + + if (item?.path) { + return { + key: item.path, + label: item.label, + onClick: () => navigate(item.path) + } + } + + return { + key: item.key || `disabled-${item.label}-${index}`, + label: item.label, + disabled: true + } + }) + .filter(Boolean) + +const MenuButton = ({ label, items }) => ( + + + +) + +MenuButton.propTypes = { + label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, + items: PropTypes.array.isRequired +} + +const WindowAppMenu = () => { + const navigate = useNavigate() + const { handleWindowControl } = useContext(ElectronContext) + const { checkForUpdates } = useAppUpdateContext() + const includeDev = import.meta.env.DEV + + const viewSections = useMemo( + () => getSidebarMenuSections({ includeDev }), + [includeDev] + ) + + const appMenuItems = useMemo( + () => [ + { + key: 'about', + label: 'About Farm Control', + onClick: () => navigate('/dashboard/management/about') + }, + { type: 'divider' }, + { + key: 'check-for-updates', + label: 'Check For Updates...', + onClick: () => checkForUpdates() + }, + { type: 'divider' }, + { + key: 'quit', + label: 'Quit Farm Control', + onClick: () => handleWindowControl('quit') + } + ], + [checkForUpdates, handleWindowControl, navigate] + ) + + const fileMenuItems = useMemo( + () => [ + { + key: 'close', + label: 'Close Window', + onClick: () => handleWindowControl('close') + } + ], + [handleWindowControl] + ) + + const editMenuItems = useMemo( + () => [ + { + key: 'undo', + label: 'Undo', + onClick: () => runEditCommand('undo') + }, + { + key: 'redo', + label: 'Redo', + onClick: () => runEditCommand('redo') + }, + { type: 'divider' }, + { + key: 'cut', + label: 'Cut', + onClick: () => runEditCommand('cut') + }, + { + key: 'copy', + label: 'Copy', + onClick: () => runEditCommand('copy') + }, + { + key: 'paste', + label: 'Paste', + onClick: () => runEditCommand('paste') + }, + { + key: 'pasteAndMatchStyle', + label: 'Paste and Match Style', + onClick: () => runEditCommand('paste') + }, + { + key: 'delete', + label: 'Delete', + onClick: () => runEditCommand('delete') + }, + { type: 'divider' }, + { + key: 'selectAll', + label: 'Select All', + onClick: () => runEditCommand('selectAll') + } + ], + [] + ) + + const viewMenuItems = useMemo(() => { + const sectionItems = + viewSections.length > 0 + ? viewSections.map((section) => ({ + key: `view-section-${section.key}`, + label: section.label, + children: mapSidebarItemsToMenuItems(section.items || [], navigate) + })) + : [ + { + key: 'no-sidebar-items', + label: 'No sidebar items available', + disabled: true + } + ] + + if (!includeDev) { + return sectionItems + } + + return [ + ...sectionItems, + { type: 'divider' }, + { + key: 'toggle-devtools', + label: 'Toggle Developer Tools', + onClick: () => handleWindowControl('toggle-devtools') + } + ] + }, [handleWindowControl, includeDev, navigate, viewSections]) + + const windowMenuItems = useMemo( + () => [ + { + key: 'minimize', + label: 'Minimize', + onClick: () => handleWindowControl('minimize') + }, + { + key: 'zoom', + label: 'Zoom', + onClick: () => handleWindowControl('maximize') + } + ], + [handleWindowControl] + ) + + const menus = useMemo( + () => [ + { key: 'app', label: 'Farm Control', items: appMenuItems }, + { key: 'file', label: 'File', items: fileMenuItems }, + { key: 'edit', label: 'Edit', items: editMenuItems }, + { key: 'view', label: 'View', items: viewMenuItems }, + { key: 'window', label: 'Window', items: windowMenuItems } + ], + [appMenuItems, editMenuItems, fileMenuItems, viewMenuItems, windowMenuItems] + ) + + return ( + + {menus.map((menu) => { + if (menu.key === 'app') { + return ( + + } + items={menu.items} + /> + ) + } + return ( + + ) + })} + + ) +} + +export default WindowAppMenu diff --git a/src/desktop/window.js b/src/desktop/window.js index 5e72150..8d07c83 100644 --- a/src/desktop/window.js +++ b/src/desktop/window.js @@ -341,7 +341,7 @@ export function getWindowState() { } export function handleWindowControl(action) { - if (!mainWindow) return + if (!mainWindow && action !== 'quit') return switch (action) { case 'minimize': @@ -375,6 +375,12 @@ export function handleWindowControl(action) { case 'close': mainWindow.close?.() break + case 'quit': + Utils.quit() + break + case 'toggle-devtools': + mainWindow?.webview?.toggleDevTools?.() + break default: break }