diff --git a/package-lock.json b/package-lock.json index 6586dec..a967c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nw-wrld", - "version": "0.4.0-beta", + "version": "0.3.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nw-wrld", - "version": "0.4.0-beta", + "version": "0.3.0-beta", "license": "GPL-3.0", "dependencies": { "@dnd-kit/core": "^6.1.0", diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 708e2cf..5ffdbaf 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -341,6 +341,7 @@ const Dashboard = () => { setWorkspaceModalMode, setWorkspaceModalPath, setIsWorkspaceModalOpen, + setIsSettingsModalOpen, }); const handleSelectWorkspace = useCallback(async () => { diff --git a/src/dashboard/core/hooks/useDashboardBootstrap.ts b/src/dashboard/core/hooks/useDashboardBootstrap.ts index 8185262..a9cb857 100644 --- a/src/dashboard/core/hooks/useDashboardBootstrap.ts +++ b/src/dashboard/core/hooks/useDashboardBootstrap.ts @@ -20,6 +20,7 @@ type UseDashboardBootstrapArgs = { setWorkspaceModalMode: (mode: "initial" | "lostSync") => void; setWorkspaceModalPath: (path: string | null) => void; setIsWorkspaceModalOpen: (open: boolean) => void; + setIsSettingsModalOpen: (open: boolean) => void; }; export const useDashboardBootstrap = ({ @@ -36,6 +37,7 @@ export const useDashboardBootstrap = ({ setWorkspaceModalMode, setWorkspaceModalPath, setIsWorkspaceModalOpen, + setIsSettingsModalOpen, }: UseDashboardBootstrapArgs) => { useEffect(() => { const initializeUserData = async () => { @@ -138,4 +140,8 @@ export const useDashboardBootstrap = ({ setWorkspaceModalPath(lostPath); setIsWorkspaceModalOpen(true); }); + + useIPCListener("menu:openSettings", () => { + setIsSettingsModalOpen(true); + }); }; diff --git a/src/dashboard/core/hooks/useIPC.ts b/src/dashboard/core/hooks/useIPC.ts index bee1e26..7e2c3de 100644 --- a/src/dashboard/core/hooks/useIPC.ts +++ b/src/dashboard/core/hooks/useIPC.ts @@ -77,6 +77,8 @@ export const useIPCListener = ( cleanup = messaging.onWorkspaceModulesChanged?.(handler); } else if (channel === "workspace:lostSync") { cleanup = messaging.onWorkspaceLostSync?.(handler); + } else if (channel === "menu:openSettings") { + cleanup = messaging.onMenuOpenSettings?.(handler); } else { return; } diff --git a/src/main/mainProcess/entry.ts b/src/main/mainProcess/entry.ts index 9388320..722ba95 100644 --- a/src/main/mainProcess/entry.ts +++ b/src/main/mainProcess/entry.ts @@ -13,6 +13,7 @@ import { registerWorkspaceSelectionIpc, } from "./workspace"; import { isExistingDirectory } from "./pathSafety"; +import { registerMenuHandlers, createApplicationMenu } from "./menu"; const getTestProjectDir = (): string | null => { const raw = process.env.NW_WRLD_TEST_PROJECT_DIR; @@ -26,6 +27,9 @@ const getTestProjectDir = (): string | null => { export function start() { setupApp(); + // Register menu handlers before creating windows + registerMenuHandlers(); + registerIpcBridge(); registerSandboxIpc(); registerMessagingIpc({ ipcMain }); @@ -48,5 +52,8 @@ export function start() { } registerActivate({ createWindow }); createWindow(testProjectDir); + + // Create application menu after window is ready + createApplicationMenu(); }); } diff --git a/src/main/mainProcess/menu.ts b/src/main/mainProcess/menu.ts new file mode 100644 index 0000000..8c759ea --- /dev/null +++ b/src/main/mainProcess/menu.ts @@ -0,0 +1,127 @@ +import { Menu, ipcMain, BrowserWindow } from "electron"; + +import { state } from "./state"; + +const { app } = require("electron") as { app: { name: string } }; + +// Module-level handler functions for menu actions +function openSettingsHandler() { + try { + const dashboard = state.dashboardWindow as BrowserWindow | null; + if (dashboard && !dashboard.isDestroyed()) { + dashboard.show(); + dashboard.focus(); + dashboard.webContents.send("menu:openSettings"); + } + } catch {} +} + +function setAspectRatioHandler(aspectRatioId: string) { + try { + ipcMain.emit("dashboard-to-projector", null, { + type: "toggleAspectRatioStyle", + props: { name: aspectRatioId }, + }); + } catch {} +} + +export function registerMenuHandlers(): void { + // Handler to open settings modal + ipcMain.on("menu:openSettings", () => { + openSettingsHandler(); + }); + + // Handler to set aspect ratio + ipcMain.on("menu:setAspectRatio", (_event, aspectRatioId: string) => { + setAspectRatioHandler(aspectRatioId); + }); +} + +export function createApplicationMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + // App menu (macOS only) + ...(isMac + ? [ + { + label: app.name, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }, + ] + : []), + // Window menu + { + label: "Window", + submenu: [ + { + label: "Settings...", + accelerator: "CmdOrCtrl+,", + click: () => { + openSettingsHandler(); + }, + }, + { type: "separator" as const }, + { + label: "Set Pane Size", + submenu: [ + { + label: "Default", + click: () => { + setAspectRatioHandler("default"); + }, + }, + { + label: "16:9 (Landscape)", + click: () => { + setAspectRatioHandler("16-9"); + }, + }, + { + label: "9:16 (Vertical)", + click: () => { + setAspectRatioHandler("9-16"); + }, + }, + { + label: "4:5 (Portrait)", + click: () => { + setAspectRatioHandler("4-5"); + }, + }, + { type: "separator" as const }, + { + label: "Fullscreen", + click: () => { + setAspectRatioHandler("fullscreen"); + }, + }, + ], + }, + { type: "separator" as const }, + { role: "reload" as const }, + { role: "forceReload" as const }, + { role: "toggleDevTools" as const }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : []), + ], + }, + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} diff --git a/src/preload.ts b/src/preload.ts index 9af2f59..eb14bdf 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -123,6 +123,12 @@ const nwWrldBridge = { ipcRenderer.on("workspace:lostSync", wrapped); return () => ipcRenderer.removeListener("workspace:lostSync", wrapped); }, + onMenuOpenSettings: (handler: IpcHandler) => { + if (typeof handler !== "function") return undefined; + const wrapped = (event: IpcRendererEvent, data: unknown) => handler(event, data); + ipcRenderer.on("menu:openSettings", wrapped); + return () => ipcRenderer.removeListener("menu:openSettings", wrapped); + }, configureInput: (payload: unknown) => ipcRenderer.invoke("input:configure", payload), getMidiDevices: () => ipcRenderer.invoke("input:get-midi-devices"), emitAudioBand: (payload: unknown) => ipcRenderer.invoke("input:audio:emitBand", payload), diff --git a/tsconfig.runtime.json b/tsconfig.runtime.json index 8190453..3eba19c 100644 --- a/tsconfig.runtime.json +++ b/tsconfig.runtime.json @@ -48,6 +48,7 @@ "src/sandboxPreload.ts", "src/main/mainProcess/state.ts", "src/main/mainProcess/pathSafety.ts", + "src/main/mainProcess/menu.ts", "src/main/mainProcess/ipcBridge.ts", "src/main/mainProcess/ipcBridge/projectContext.ts", "src/main/mainProcess/ipcBridge/registerProjectBridge.ts",