feat(electron): more desktop app related shortcuts (#9724)

fix AF-2126, AF-2124

- Add CMD+M for minimize the app.
- Enhance how CMD+W works. Close the following in order, stop if any one is closed:
  - peek view
  - split view
  - tab
  - otherwise, hide the app
This commit is contained in:
pengx17
2025-01-16 06:50:08 +00:00
parent 676f855564
commit 9f3a304885
6 changed files with 56 additions and 8 deletions
@@ -2,10 +2,10 @@ import { app, Menu } from 'electron';
import { isMacOS } from '../../shared/utils'; import { isMacOS } from '../../shared/utils';
import { logger, revealLogFile } from '../logger'; import { logger, revealLogFile } from '../logger';
import { uiSubjects } from '../ui/subject';
import { checkForUpdates } from '../updater'; import { checkForUpdates } from '../updater';
import { import {
addTab, addTab,
closeTab,
initAndShowMainWindow, initAndShowMainWindow,
reloadView, reloadView,
showDevTools, showDevTools,
@@ -103,6 +103,9 @@ export function createApplicationMenu() {
reloadView().catch(console.error); reloadView().catch(console.error);
}, },
}, },
{
role: 'windowMenu',
},
{ {
label: 'Open devtools', label: 'Open devtools',
accelerator: isMac ? 'Cmd+Option+I' : 'Ctrl+Shift+I', accelerator: isMac ? 'Cmd+Option+I' : 'Ctrl+Shift+I',
@@ -129,11 +132,12 @@ export function createApplicationMenu() {
}, },
}, },
{ {
label: 'Close tab', label: 'Close view',
accelerator: 'CommandOrControl+W', accelerator: 'CommandOrControl+W',
click() { click() {
logger.info('Close tab with shortcut'); logger.info('Close view with shortcut');
closeTab().catch(console.error); // tell the active workbench to close the current view
uiSubjects.onCloseView$.next();
}, },
}, },
{ {
@@ -195,7 +199,7 @@ export function createApplicationMenu() {
{ {
label: 'Learn More', label: 'Learn More',
click: async () => { click: async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires // oxlint-disable-next-line
const { shell } = require('electron'); const { shell } = require('electron');
await shell.openExternal('https://affine.pro/'); await shell.openExternal('https://affine.pro/');
}, },
@@ -216,7 +220,7 @@ export function createApplicationMenu() {
{ {
label: 'Documentation', label: 'Documentation',
click: async () => { click: async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires // oxlint-disable-next-line
const { shell } = require('electron'); const { shell } = require('electron');
await shell.openExternal( await shell.openExternal(
'https://docs.affine.pro/docs/hello-bonjour-aloha-你好' 'https://docs.affine.pro/docs/hello-bonjour-aloha-你好'
@@ -42,4 +42,10 @@ export const uiEvents = {
sub.unsubscribe(); sub.unsubscribe();
}; };
}, },
onCloseView: (fn: () => void) => {
const sub = uiSubjects.onCloseView$.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>; } satisfies Record<string, MainEventRegister>;
@@ -71,6 +71,10 @@ export const uiHandlers = {
handleCloseApp: async () => { handleCloseApp: async () => {
app.quit(); app.quit();
}, },
handleHideApp: async () => {
const window = await getMainWindow();
window?.hide();
},
handleNetworkChange: async (_, _isOnline: boolean) => { handleNetworkChange: async (_, _isOnline: boolean) => {
isOnline = _isOnline; isOnline = _isOnline;
}, },
@@ -191,6 +195,7 @@ export const uiHandlers = {
closeTab: async (_, ...args: Parameters<typeof closeTab>) => { closeTab: async (_, ...args: Parameters<typeof closeTab>) => {
await closeTab(...args); await closeTab(...args);
}, },
activateView: async (_, ...args: Parameters<typeof activateView>) => { activateView: async (_, ...args: Parameters<typeof activateView>) => {
await activateView(...args); await activateView(...args);
}, },
@@ -7,4 +7,6 @@ export const uiSubjects = {
onFullScreen$: new Subject<boolean>(), onFullScreen$: new Subject<boolean>(),
onToggleRightSidebar$: new Subject<string>(), onToggleRightSidebar$: new Subject<string>(),
authenticationRequest$: new Subject<AuthenticationRequest>(), authenticationRequest$: new Subject<AuthenticationRequest>(),
// via menu -> close view (CMD+W)
onCloseView$: new Subject<void>(),
}; };
@@ -14,6 +14,7 @@ export { WorkbenchRoot } from './view/workbench-root';
import { type Framework } from '@toeverything/infra'; import { type Framework } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api'; import { DesktopApiService } from '../desktop-api';
import { PeekViewService } from '../peek-view';
import { GlobalStateService } from '../storage'; import { GlobalStateService } from '../storage';
import { WorkspaceScope } from '../workspace'; import { WorkspaceScope } from '../workspace';
import { SidebarTab } from './entities/sidebar-tab'; import { SidebarTab } from './entities/sidebar-tab';
@@ -64,5 +65,9 @@ export function configureDesktopWorkbenchModule(services: Framework) {
.impl(WorkbenchNewTabHandler, DesktopWorkbenchNewTabHandler, [ .impl(WorkbenchNewTabHandler, DesktopWorkbenchNewTabHandler, [
DesktopApiService, DesktopApiService,
]) ])
.service(DesktopStateSynchronizer, [WorkbenchService, DesktopApiService]); .service(DesktopStateSynchronizer, [
WorkbenchService,
DesktopApiService,
PeekViewService,
]);
} }
@@ -1,6 +1,7 @@
import { LiveData, Service } from '@toeverything/infra'; import { LiveData, Service } from '@toeverything/infra';
import type { DesktopApiService } from '../../desktop-api'; import type { DesktopApiService } from '../../desktop-api';
import type { PeekViewService } from '../../peek-view';
import type { WorkbenchService } from '../../workbench'; import type { WorkbenchService } from '../../workbench';
/** /**
@@ -9,7 +10,8 @@ import type { WorkbenchService } from '../../workbench';
export class DesktopStateSynchronizer extends Service { export class DesktopStateSynchronizer extends Service {
constructor( constructor(
private readonly workbenchService: WorkbenchService, private readonly workbenchService: WorkbenchService,
private readonly electronApi: DesktopApiService private readonly electronApi: DesktopApiService,
private readonly peekViewService: PeekViewService
) { ) {
super(); super();
this.startSync(); this.startSync();
@@ -52,6 +54,30 @@ export class DesktopStateSynchronizer extends Service {
} }
}); });
this.electronApi.events.ui.onCloseView(() => {
(async () => {
if (await this.electronApi.handler.ui.isActiveTab()) {
// close current view. stop if any one is successful
// 1. peek view
// 2. split view
// 3. tab
// 4. otherwise, hide the window
if (this.peekViewService.peekView.show$.value?.value) {
this.peekViewService.peekView.close();
} else if (workbench.views$.value.length > 1) {
workbench.close(workbench.activeView$.value);
} else {
const tabs = await this.electronApi.handler.ui.getTabsStatus();
if (tabs.length > 1) {
await this.electronApi.handler.ui.closeTab();
} else {
await this.electronApi.handler.ui.handleHideApp();
}
}
}
})().catch(console.error);
});
this.electronApi.events.ui.onToggleRightSidebar(tabId => { this.electronApi.events.ui.onToggleRightSidebar(tabId => {
if (tabId === appInfo?.viewId) { if (tabId === appInfo?.viewId) {
workbench.sidebarOpen$.next(!workbench.sidebarOpen$.value); workbench.sidebarOpen$.next(!workbench.sidebarOpen$.value);