feat: implement tray and minimize behaviors (#13851)

This PR introduces new window behaviors, which can be enabled when the
menubar setting is active:

New Features:
- Quick open from tray icon
- Minimize to tray
- Exit to tray
- Start minimized

These changes have not yet been tested on macOS.

<img width="645" height="479" alt="image"
src="https://github.com/user-attachments/assets/7bdd13d0-5322-45a4-8e71-85c081aa0c86"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Configurable menubar/tray behaviors: open on left-click, minimize to
tray, close to tray (exit to tray), and start minimized.

* **UI**
* Appearance settings add a Menubar → Window Behavior group with four
toggles; group shows only when menubar/tray is enabled (hidden on
macOS).

* **Settings**
* Tray settings persisted and exposed via the settings API with getters
and setters for each option.

* **Localization**
* Added translation keys and English strings for the new controls and
descriptions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
Martin Pauli
2025-11-06 21:10:15 +01:00
committed by GitHub
parent 9f6ea83ac1
commit 2bd9f1a353
8 changed files with 258 additions and 29 deletions

View File

@@ -62,22 +62,92 @@ export const ThemeSettings = () => {
const MenubarSetting = () => {
const t = useI18n();
const traySettingService = useService(TraySettingService);
const { enabled } = useLiveData(traySettingService.setting$);
const traySetting = useLiveData(traySettingService.settings$);
return (
<SettingWrapper
id="menubar"
title={t['com.affine.appearanceSettings.menubar.title']()}
>
<SettingRow
name={t['com.affine.appearanceSettings.menubar.toggle']()}
desc={t['com.affine.appearanceSettings.menubar.description']()}
<>
<SettingWrapper
id="menubar"
title={t['com.affine.appearanceSettings.menubar.title']()}
>
<Switch
checked={enabled}
onChange={checked => traySettingService.setEnabled(checked)}
/>
</SettingRow>
</SettingWrapper>
<SettingRow
name={t['com.affine.appearanceSettings.menubar.toggle']()}
desc={t['com.affine.appearanceSettings.menubar.description']()}
>
<Switch
checked={traySetting.enabled}
onChange={checked => traySettingService.setEnabled(checked)}
/>
</SettingRow>
</SettingWrapper>
{traySetting.enabled && !environment.isMacOs ? (
<SettingWrapper
id="windowBehavior"
title={t[
'com.affine.appearanceSettings.menubar.windowBehavior.title'
]()}
>
<SettingRow
name={t[
'com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.toggle'
]()}
desc={t[
'com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.description'
]()}
>
<Switch
checked={traySetting.openOnLeftClick}
onChange={checked =>
traySettingService.setOpenOnLeftClick(checked)
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.toggle'
]()}
desc={t[
'com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.description'
]()}
>
<Switch
checked={traySetting.minimizeToTray}
onChange={checked =>
traySettingService.setMinimizeToTray(checked)
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.toggle'
]()}
desc={t[
'com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.description'
]()}
>
<Switch
checked={traySetting.closeToTray}
onChange={checked => traySettingService.setCloseToTray(checked)}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.toggle'
]()}
desc={t[
'com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.description'
]()}
>
<Switch
checked={traySetting.startMinimized}
onChange={checked =>
traySettingService.setStartMinimized(checked)
}
/>
</SettingRow>
</SettingWrapper>
) : null}
</>
);
};

View File

@@ -3,26 +3,73 @@ import type {
MenubarStateSchema,
} from '@affine/electron/main/shared-state-schema';
import { LiveData, Service } from '@toeverything/infra';
import { defaults } from 'lodash-es';
import type { GlobalStateService } from '../../storage';
const MENUBAR_SETTING_KEY: typeof MenubarStateKey = 'menubarState';
const defaultTraySetting: MenubarStateSchema = {
enabled: true,
minimizeToTray: false,
closeToTray: false,
startMinimized: false,
openOnLeftClick: false,
};
export class TraySettingService extends Service {
constructor(private readonly globalStateService: GlobalStateService) {
super();
}
setting$ = LiveData.from(
this.globalStateService.globalState.watch<MenubarStateSchema>(
MENUBAR_SETTING_KEY
),
null
).map(v => v ?? { enabled: true });
readonly settings$ = LiveData.computed(get => {
const value = get(
LiveData.from(
this.globalStateService.globalState.watch<MenubarStateSchema>(
MENUBAR_SETTING_KEY
),
undefined
)
);
return defaults(value, defaultTraySetting);
});
get settings() {
return this.settings$.value;
}
setEnabled(enabled: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
enabled,
...this.settings$.value,
enabled: enabled,
});
}
setMinimizeToTray(minimizeToTray: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
...this.settings$.value,
minimizeToTray: minimizeToTray,
});
}
setCloseToTray(closeToTray: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
...this.settings$.value,
closeToTray: closeToTray,
});
}
setStartMinimized(startMinimized: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
...this.settings$.value,
startMinimized: startMinimized,
});
}
setOpenOnLeftClick(openOnLeftClick: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
...this.settings$.value,
openOnLeftClick: openOnLeftClick,
});
}
}