diff --git a/app/src/main/helpers/shortcut.ts b/app/src/main/helpers/shortcut.ts new file mode 100644 index 0000000000..5df5b313ab --- /dev/null +++ b/app/src/main/helpers/shortcut.ts @@ -0,0 +1,36 @@ +import { BrowserWindow, Event, Input, ipcMain, Menu } from 'electron'; + +import { isMac } from './env'; + +const registerListeners = (mainWindow: BrowserWindow) => { + if (isMac) { + registerDarwinListeners(mainWindow); + } else { + // registerDefaultListeners(mainWindow); + } + + ipcMain.handle('shortcut-enabled', (_, id: string, enabled: boolean) => { + const menu = Menu.getApplicationMenu(); + if (!menu) return; + const menuItem = menu.getMenuItemById(id); + if (!menuItem) return; + menuItem.enabled = enabled; + }); +}; + +const registerDarwinListeners = (mainWindow: BrowserWindow) => { + mainWindow.webContents.on( + 'before-input-event', + (event: Event, input: Input) => { + // CMD on Mac + if (input.meta && input.type === 'keyDown') { + if (input.key === 'w') { + mainWindow.webContents.send('shortcut-event', 'close-current-window'); + event.preventDefault(); + } + } + } + ); +}; + +export const ShortcutHelper = { registerListeners }; diff --git a/app/src/main/main.ts b/app/src/main/main.ts index f60c76c830..3a55e938ea 100644 --- a/app/src/main/main.ts +++ b/app/src/main/main.ts @@ -26,6 +26,7 @@ import { KeyHelper } from './helpers/key'; import { MediaHelper } from './helpers/media'; import { MouseHelper } from './helpers/mouse'; import { PowerHelper } from './helpers/power'; +import { ShortcutHelper } from './helpers/shortcut'; import { WebViewHelper } from './helpers/webview'; import { MenuBuilder } from './menu'; import { resolveHtmlPath } from './util'; @@ -119,6 +120,7 @@ const createWindow = async () => { PowerHelper.registerListeners(mainWindow); KeyHelper.registerListeners(mainWindow); DeepLinkHelper.registerListeners(mainWindow); + ShortcutHelper.registerListeners(mainWindow); mainWindow.loadURL(resolveHtmlPath('index.html')); diff --git a/app/src/main/menu.ts b/app/src/main/menu.ts index eda3f135ec..fe599d04b0 100644 --- a/app/src/main/menu.ts +++ b/app/src/main/menu.ts @@ -98,6 +98,22 @@ export class MenuBuilder { }, ], }; + const subMenuFile: DarwinMenuItemConstructorOptions = { + label: 'File', + submenu: [ + { + label: 'Close Current Window', + accelerator: 'CommandOrControl+W', + enabled: false, + click: () => { + this.mainWindow.webContents.send( + 'shortcut-event', + 'close-current-window' + ); + }, + }, + ], + }; const subMenuEdit: DarwinMenuItemConstructorOptions = { label: 'Edit', submenu: [ @@ -165,37 +181,37 @@ export class MenuBuilder { { label: 'Bring All to Front', selector: 'arrangeInFront:' }, ], }; - const subMenuHelp: MenuItemConstructorOptions = { - label: 'Help', - submenu: [ - { - label: 'Learn More', - click() { - shell.openExternal('https://electronjs.org'); - }, - }, - { - label: 'Documentation', - click() { - shell.openExternal( - 'https://github.com/electron/electron/tree/main/docs#readme' - ); - }, - }, - { - label: 'Community Discussions', - click() { - shell.openExternal('https://www.electronjs.org/community'); - }, - }, - { - label: 'Search Issues', - click() { - shell.openExternal('https://github.com/electron/electron/issues'); - }, - }, - ], - }; + // const subMenuHelp: MenuItemConstructorOptions = { + // label: 'Help', + // submenu: [ + // { + // label: 'Learn More', + // click() { + // shell.openExternal('https://electronjs.org'); + // }, + // }, + // { + // label: 'Documentation', + // click() { + // shell.openExternal( + // 'https://github.com/electron/electron/tree/main/docs#readme' + // ); + // }, + // }, + // { + // label: 'Community Discussions', + // click() { + // shell.openExternal('https://www.electronjs.org/community'); + // }, + // }, + // { + // label: 'Search Issues', + // click() { + // shell.openExternal('https://github.com/electron/electron/issues'); + // }, + // }, + // ], + // }; // const subMenuView = // process.env.NODE_ENV === 'development' || @@ -205,7 +221,14 @@ export class MenuBuilder { const subMenuView = subMenuViewDev; - return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; + return [ + subMenuAbout, + subMenuFile, + subMenuEdit, + subMenuView, + subMenuWindow, + // subMenuHelp, + ]; } buildDefaultTemplate() { diff --git a/app/src/main/preload.ts b/app/src/main/preload.ts index beb031af43..fdd3647065 100644 --- a/app/src/main/preload.ts +++ b/app/src/main/preload.ts @@ -122,6 +122,11 @@ const appPreload = { callback(hex); }); }, + onShortcutEvent(callback: (shortcut: string) => void) { + ipcRenderer.on('shortcut-event', (_, shortcut: string) => { + callback(shortcut); + }); + }, onKeyDown(callback: (key: string, isFocused: boolean) => void) { ipcRenderer.on('key-down', (_, key: string, isFocused: boolean) => { callback(key, isFocused); @@ -160,6 +165,9 @@ const appPreload = { removeOnJoinSpace() { ipcRenderer.removeAllListeners('join-space'); }, + removeOnShortcutEvent() { + ipcRenderer.removeAllListeners('shortcut-event'); + }, }; export type AppPreloadType = typeof appPreload; diff --git a/app/src/renderer/stores/ipc.ts b/app/src/renderer/stores/ipc.ts index 7ffdfaf162..3d4303e137 100644 --- a/app/src/renderer/stores/ipc.ts +++ b/app/src/renderer/stores/ipc.ts @@ -13,3 +13,4 @@ export const SpacesIPC = window.spacesService; export const BazaarIPC = window.bazaarService; export const AppInstallIPC = window.appInstallService; export const AppRecentsIPC = window.appRecentsService; +// export const ShellIPC = window.shellService; diff --git a/app/src/renderer/stores/models/shell.model.ts b/app/src/renderer/stores/models/shell.model.ts index 0bad82fc72..c46903624f 100644 --- a/app/src/renderer/stores/models/shell.model.ts +++ b/app/src/renderer/stores/models/shell.model.ts @@ -191,6 +191,7 @@ export const ShellModel = types this.setActive(newWindow.appId); if (self.homePaneOpen) self.homePaneOpen = false; + // ipcRenderer.invoke('shortcut-enabled', 'close-current-window', true); return newWindow; }, openBookmark(bookmark: Omit) { @@ -279,6 +280,16 @@ export const ShellModel = types toggleDevTools() { return window.electron.app.toggleDevTools(); }, + closeCurrentWindow() { + const windows = Array.from(self.windows.values()); + const activeWindow = windows.find( + (app: AppWindowMobxType) => app.isActive + ); + // not optimal + if (activeWindow) { + this.closeWindow(activeWindow.appId); + } + }, closeWindow(appId: string) { const windows = Array.from(self.windows.values()); self.nativeConfig.delete(appId); @@ -291,6 +302,8 @@ export const ShellModel = types )[0]; if (nextWindow) { this.setActive(nextWindow.appId); + } else { + // ipcRenderer.invoke('shortcut-enabled', 'close-current-window', false); } } self.windows.delete(appId); @@ -301,6 +314,13 @@ export const ShellModel = types toggleMultiplayer() { self.multiplayerEnabled = !self.multiplayerEnabled; }, + handleShortcutEvent(shortcut: string) { + switch (shortcut) { + case 'close-current-window': + this.closeCurrentWindow(); + break; + } + }, })); export type ShellModelType = Instance; diff --git a/app/src/renderer/system/index.tsx b/app/src/renderer/system/index.tsx index 7bc7650aa2..3d2984206b 100644 --- a/app/src/renderer/system/index.tsx +++ b/app/src/renderer/system/index.tsx @@ -22,6 +22,16 @@ const ShellPresenter = () => { [shellStore.dialogId, shellStore.dialogProps] ); + useEffect(() => { + window.electron.app.onShortcutEvent((shortcut: string) => { + shellStore.handleShortcutEvent(shortcut); + }); + + return () => { + window.electron.app.removeOnShortcutEvent(); + }; + }, []); + useEffect(() => { // Sync Electron with MobX state. if (shellStore.isIsolationMode) {