TidGi-Desktop/src/services/windows/handleAttachToTidgiMiniWindow.ts
lin onetwo 19ef74a4a6
Feat/mini window (#642)
* feat: new config for tidgi mini window

* chore: upgrade electron-forge

* fix: use 汉语 和 漢語

* feat: shortcut to open mini window

* test: TidGiMenubarWindow

* feat: allow updateWindowProperties on the fly

* fix: wrong icon path

* fix: log not showing error message and stack

* refactor: directly log error when using logger.error

* feat: shortcut to open window

* fix: menubar not closed

* test: e2e for menubar

* test: keyboard shortcut

* test: wiki web content, and refactor to files

* test: update command

* Update Testing.md

* test: menubar settings about menubarSyncWorkspaceWithMainWindow, menubarFixedWorkspaceId

* test: simplify menubar test and cleanup test config

* fix: view missing when execute several test all together

* refactor: use hook to cleanup menubar setting

* refactor: I clear test ai settings to before hook

* Add option to show title bar on menubar window

Introduces a new preference 'showMenubarWindowTitleBar' allowing users to toggle the title bar visibility on the menubar window. Updates related services, interfaces, and UI components to support this feature, and adds corresponding localization strings for English and Chinese.

* refactor: tidgiminiwindow

* refactor: preference keys to right order

* Refactor window dimension checks to use constants

Replaces hardcoded window dimensions with values from windowDimension and WindowNames constants for improved maintainability and consistency in window identification and checks.

* I cleanup test wiki

* Update defaultPreferences.ts

* test: mini window workspace switch

* fix: image broken by ai, and lint

* fix: can't switch to mini window

* refactor: useless todo

* Update index.ts

* refactor: reuse serialize-error

* Update index.ts

* Update testKeyboardShortcuts.ts

* refactor: dup logic

* Update ui.ts

* fix: electron-ipc-cat
2025-10-21 20:07:04 +08:00

190 lines
7.6 KiB
TypeScript

import { TIDGI_MINI_WINDOW_ICON_PATH } from '@/constants/paths';
import { isMac } from '@/helpers/system';
import { container } from '@services/container';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import type { IMenuService } from '@services/menu/interface';
import type { IPreferenceService } from '@services/preferences/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IViewService } from '@services/view/interface';
import { BrowserWindowConstructorOptions, Menu, nativeImage, Tray } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { debounce } from 'lodash';
import { Menubar, menubar } from 'menubar';
import type { IWindowService } from './interface';
import { getMainWindowEntry } from './viteEntry';
import { WindowNames } from './WindowProperties';
export async function handleAttachToTidgiMiniWindow(
windowConfig: BrowserWindowConstructorOptions,
windowWithBrowserViewState: windowStateKeeper.State | undefined,
): Promise<Menubar> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
// Get tidgi mini window-specific titleBar preference
const tidgiMiniWindowShowTitleBar = await preferenceService.get('tidgiMiniWindowShowTitleBar');
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
// https://github.com/electron/electron/issues/22137#issuecomment-586105622
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
tray.setImage(nativeImage.createFromPath(TIDGI_MINI_WINDOW_ICON_PATH));
// Create tidgi mini window-specific window configuration
// Override titleBar settings from windowConfig with tidgi mini window-specific preference
const tidgiMiniWindowConfig: BrowserWindowConstructorOptions = {
...windowConfig,
show: false,
minHeight: 100,
minWidth: 250,
// Use tidgi mini window-specific titleBar setting instead of inheriting from main window
titleBarStyle: tidgiMiniWindowShowTitleBar ? 'default' : 'hidden',
frame: tidgiMiniWindowShowTitleBar,
// Always hide the menu bar (File, Edit, View menu), even when showing title bar
autoHideMenuBar: true,
};
logger.info('Creating tidgi mini window with titleBar configuration', {
function: 'handleAttachToTidgiMiniWindow',
tidgiMiniWindowShowTitleBar,
titleBarStyle: tidgiMiniWindowConfig.titleBarStyle,
frame: tidgiMiniWindowConfig.frame,
});
const tidgiMiniWindow = menubar({
index: getMainWindowEntry(),
tray,
activateWithApp: false,
showDockIcon: true,
preloadWindow: true,
tooltip: i18n.t('Menu.TidGiMiniWindow'),
browserWindow: tidgiMiniWindowConfig,
});
tidgiMiniWindow.on('after-create-window', () => {
if (tidgiMiniWindow.window !== undefined) {
tidgiMiniWindow.window.on('focus', async () => {
logger.debug('restore window position', { function: 'handleAttachToTidgiMiniWindow' });
if (windowWithBrowserViewState === undefined) {
logger.debug('windowWithBrowserViewState is undefined for tidgiMiniWindow', { function: 'handleAttachToTidgiMiniWindow' });
} else {
if (tidgiMiniWindow.window === undefined) {
logger.debug('tidgiMiniWindow.window is undefined', { function: 'handleAttachToTidgiMiniWindow' });
} else {
const haveXYValue = [windowWithBrowserViewState.x, windowWithBrowserViewState.y].every((value) => Number.isFinite(value));
const haveWHValue = [windowWithBrowserViewState.width, windowWithBrowserViewState.height].every((value) => Number.isFinite(value));
if (haveXYValue) {
tidgiMiniWindow.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
}
if (haveWHValue) {
tidgiMiniWindow.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
}
}
}
const view = await viewService.getActiveBrowserView();
view?.webContents.focus();
});
tidgiMiniWindow.window.removeAllListeners('close');
tidgiMiniWindow.window.on('close', (event) => {
event.preventDefault();
tidgiMiniWindow.hideWindow();
});
}
});
tidgiMiniWindow.on('hide', async () => {
// on mac, calling `tidgiMiniWindow.app.hide()` with main window open will bring background main window up, which we don't want. We want to bring previous other app up. So close main window first.
if (isMac) {
const mainWindow = windowService.get(WindowNames.main);
if (mainWindow?.isVisible() === true) {
await windowService.hide(WindowNames.main);
}
}
});
// https://github.com/maxogden/menubar/issues/120
tidgiMiniWindow.on('after-hide', () => {
if (isMac) {
tidgiMiniWindow.app.hide();
}
});
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
const debouncedSaveWindowState = debounce(
() => {
if (tidgiMiniWindow.window !== undefined) {
windowWithBrowserViewState?.saveState(tidgiMiniWindow.window);
}
},
500,
);
// tidgi mini window is hide, not close, so not managed by windowStateKeeper, need to save manually
tidgiMiniWindow.window?.on('resize', debouncedSaveWindowState);
tidgiMiniWindow.window?.on('move', debouncedSaveWindowState);
return await new Promise<Menubar>((resolve) => {
tidgiMiniWindow.on('ready', async () => {
// right on tray icon
tidgiMiniWindow.tray.on('right-click', () => {
const contextMenu = Menu.buildFromTemplate([
{
label: i18n.t('ContextMenu.OpenTidGi'),
click: async () => {
await windowService.open(WindowNames.main);
},
},
{
label: i18n.t('ContextMenu.OpenTidGiMiniWindow'),
click: async () => {
await tidgiMiniWindow.showWindow();
},
},
{
type: 'separator',
},
{
label: i18n.t('ContextMenu.About'),
click: async () => {
await windowService.open(WindowNames.about);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Preferences'),
click: async () => {
await windowService.open(WindowNames.preferences);
},
},
{
label: i18n.t('ContextMenu.Notifications'),
click: async () => {
await windowService.open(WindowNames.notifications);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Quit'),
click: () => {
tidgiMiniWindow.app.quit();
},
},
]);
tidgiMiniWindow.tray.popUpContextMenu(contextMenu);
});
// right click on window content
if (tidgiMiniWindow.window?.webContents !== undefined) {
const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(tidgiMiniWindow.window.webContents);
tidgiMiniWindow.on('after-close', () => {
unregisterContextMenu();
});
}
resolve(tidgiMiniWindow);
});
});
}