Files
CherryHQ-cherry-studio/docs/references/window-manager/window-manager-usage.md
2026-07-06 16:47:10 +08:00

246 lines
11 KiB
Markdown

# WindowManager Usage Guide
Practical guide for using WindowManager from consumer code. For architectural context, see [Overview](./window-manager-overview.md). For full method reference, see [API Reference](./window-manager-api-reference.md).
## Quick Start
### 1. Add the WindowType enum value
In `types.ts`:
```typescript
export enum WindowType {
Main = 'main',
// ... existing types
MyWindow = 'myWindow', // <-- add your new type
}
```
### 2. Register in the window registry
In `windowRegistry.ts`:
```typescript
WINDOW_TYPE_REGISTRY[WindowType.MyWindow] = {
type: WindowType.MyWindow,
lifecycle: 'singleton',
htmlPath: 'windows/myWindow/index.html',
// preload omitted → defaults to 'index.js'
// showMode omitted → defaults to 'auto'
windowOptions: {
...DEFAULT_WINDOW_CONFIG,
width: 800,
height: 600,
minWidth: 600,
minHeight: 400,
},
}
```
### 3. Open the window
```typescript
import { application } from '@application'
import { WindowType } from '@main/core/window/types'
const wm = application.get('WindowManager')
// open() is lifecycle-aware — handles singleton reuse, pool recycle, etc.
const windowId = wm.open(WindowType.MyWindow)
```
### 4. Inject domain behavior via `onWindowCreatedByType`
```typescript
// In your domain service's onInit():
const wm = application.get('WindowManager')
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
// Store the windowId for later use
this.myWindowId = id
// Attach event listeners BEFORE content loads
window.on('closed', () => {
this.myWindowId = undefined
})
})
```
The example above uses **destructuring**. An equivalent using the `mw` shorthand (useful when the callback body is long or accesses many fields):
```typescript
wm.onWindowCreatedByType(WindowType.MyWindow, (mw) => {
this.myWindowId = mw.id
mw.window.on('closed', () => { this.myWindowId = undefined })
})
```
Both are valid — see [Callback styles](#callback-styles-destructuring-vs-mw-shorthand) for when to prefer which.
## Domain Service Integration
The `onWindowCreated` event is the canonical hook for domain services to inject window-specific behavior, and pairs with `wm.open()` / `wm.close()` as the universal consumer API. For single-type subscriptions (the typical case) prefer the `onWindowCreatedByType` / `onWindowDestroyedByType` convenience variants — they filter by type for you so the callback body focuses on behavior, not guards.
### The Pattern
```typescript
@Injectable('MyWindowService')
@ServicePhase(Phase.WhenReady)
export class MyWindowService extends BaseService {
private myWindowId: string | undefined
protected override onInit(): void {
const wm = application.get('WindowManager')
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
// 1. Store the windowId
this.myWindowId = id
// 2. Attach listeners BEFORE content loads
window.once('ready-to-show', () => {
this.sendInitialData(window)
})
window.on('closed', () => {
this.myWindowId = undefined
})
})
wm.onWindowDestroyedByType(WindowType.MyWindow, () => {
this.myWindowId = undefined
})
}
}
```
### Injecting behavior: `onWindowCreated` is the canonical hook
Domain services attach window-specific behavior inside an `onWindowCreated` subscription. This pairs with `wm.open()` as the universal consumer API: `open()` produces or reuses a window according to its registry `lifecycle`, and `onWindowCreated` fires exactly once per fresh `BrowserWindow` instance. You never need to branch on "new vs reused" at the call site.
For subscriptions that only care about a single window type (the typical consumer case), use the `onWindowCreatedByType(type, listener)` / `onWindowDestroyedByType(type, listener)` convenience variants — they apply the type filter for you, so the callback body never starts with `if (managed.type !== X) return`. The generic `onWindowCreated` / `onWindowDestroyed` remain available for the rare "observe all windows" use case.
**What `onWindowCreated` gives you for free:**
- **Fires exactly once per fresh BrowserWindow.** Singleton reopens and pool recycles do NOT re-fire — so listeners attached here never accumulate duplicates, and `open()` is always safe regardless of reuse path.
- **Covers every `open()` call site with one subscription.** Primary path, crash recovery, test fixtures, and any future-added entry point all flow through the same event. You cannot forget to wire up a new path.
- **Fires before `loadURL`.** Pre-load configuration such as `setFocusable` (Linux Wayland), `setContentProtection`, or `webContents` session setup can be applied in time to affect first paint.
- **Works for pooled windows too.** Per-instance listeners like `resized` or `closed` must be attached here — the recycle path does not re-fire the event, so attaching them at an `open()` call site would either miss the recycled instance or accumulate on re-open.
**Anti-pattern: direct-ID attachment at the `open()` call site.**
It's tempting to attach listeners inline after `wm.open()` returns, since the ID is right there:
```typescript
const id = wm.open(WindowType.MyWindow)
const window = wm.getWindow(id)!
window.on('blur', this.hideIfUnpinned)
window.once('closed', () => { this.windowId = null })
```
This looks cleaner than subscribing to an event, but it carries three hidden costs:
1. **Forces you off `open()`.** If the window is reused (singleton reopen or pool recycle), these listeners attach a second time on a window that already has them. To make the pattern safe you'd have to switch to `create()` — which is an internal primitive, not a consumer API (see "Window API layers" below).
2. **Multiple entry paths silently decouple.** Crash recovery, test fixtures, or any future `open()` call site each need to remember to run setup. An `onWindowCreated` subscription covers all of them in one place.
3. **Implicit coupling to registry config.** If listener safety depends on a specific `showMode` / `paintWhenInitiallyHidden` / etc. value (e.g. pre-show `setFocusable` timing that only works when `showMode: 'manual'`), a later registry change breaks correctness with no compile-time signal.
If you feel drawn to this pattern, subscribe to `onWindowCreatedByType(type, listener)` — one extra line, and all three costs disappear.
### Callback styles: destructuring vs `mw` shorthand
The `onWindowCreatedByType` / `onWindowDestroyedByType` listeners receive a `ManagedWindow` — the same record shape as the generic variants. Two idiomatic ways to access its fields:
**Destructuring (recommended default, short callback):**
```typescript
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
this.myWindowId = id
window.on('closed', () => { this.myWindowId = undefined })
})
```
Pull exactly the fields you need out of the parameter — `{ window }`, `{ window, id }`, `{ window, id, metadata }`. Self-documenting and avoids the `mw.window.on(...)` visual noise.
**`mw` shorthand (callback with inner closures or many accesses):**
```typescript
wm.onWindowCreatedByType(WindowType.SelectionAction, (mw) => {
// Inner closure reads mw.window's methods repeatedly — keeping the whole
// record under one short name reads better than re-destructuring.
mw.window.on('resized', () => {
if (mw.window.isDestroyed()) return
this.saveBounds(mw.id, mw.window.getBounds())
})
})
```
`mw` is the initials of `ManagedWindow` — short, specific, and doesn't collide with the `.window` field the way a parameter named `window` would.
**Pick whichever reads better in context.** Mixing them across files — or even within the same service — is fine; the parameter name is the only difference.
### Window API layers: consumer vs internal
WindowManager exposes four lifecycle methods, arranged in two layers:
| Layer | Method | Semantics | When to call |
|---|---|---|---|
| **Consumer** | `open(type, args?)` | Lifecycle-aware: fresh create, singleton reuse, or pool recycle per registry | Always, to obtain a window |
| **Consumer** | `close(windowId)` | Lifecycle-aware: destroy non-pooled, release-to-pool for pooled | Always, to release a window |
| Internal | `create(type, args?)` | Force fresh creation; throws if singleton already exists | Defensive assertion — consumer code should not need it |
| Internal | `destroy(windowId)` | Force destroy; bypasses pool recycling | Not needed in consumer code (see below) |
**Consumer code should only ever call `open()` and `close()`.** The registry's `lifecycle` declaration is the single source of truth for how those methods behave, so call sites do not need to branch on window type.
**Why `create()` is not a consumer API.** Every common motivation for reaching for `create()` has a cleaner `open()`-based resolution:
| Urge | Resolution |
|---|---|
| "I need my setup to run only on fresh windows" | Subscribe to `onWindowCreatedByType` — it fires only on fresh, never on reuse |
| "I need to be sure no duplicate singleton exists" | Registry `lifecycle: 'singleton'` already guarantees it; `open()` returns the existing instance |
| "My service's local `windowId` must match WindowManager's" | Subscribe to `onWindowDestroyedByType` to clear local state in sync with WM's `'closed'` tracking |
**Why `destroy()` is not a consumer API.** On non-pooled windows (default and singleton) `close()` falls through to the same `destroyWindow()` call — there is no behavioral difference. On pooled windows, `destroy()` bypasses the pool, which is almost never what a consumer actually wants; the correct API for "stop the whole pool" is `suspendPool(type)`, which destroys idle windows and prevents further recycling without touching in-use windows.
### Domain-Key-to-WindowId Mapping
For window types that are keyed by domain data (e.g., a topic-specific window), the domain service maintains its own mapping:
```typescript
// Domain service tracks which topic is shown in which window
private topicWindows = new Map<string, string>() // topicId -> windowId
wm.onWindowCreatedByType(WindowType.TopicView, ({ id }) => {
const topicId = wm.getInitData(id) as string
this.topicWindows.set(topicId, id)
})
// Open a topic — reuse existing or create new
openTopic(topicId: string): void {
const existingId = this.topicWindows.get(topicId)
if (existingId) {
wm.show(existingId)
wm.focus(existingId)
return
}
const windowId = wm.open(WindowType.TopicView, { initData: topicId })
}
```
## Renderer: `useWindowInitData` hook
`src/renderer/hooks/useWindowInitData.ts` provides the canonical way for any managed window to consume its init data across both creation paths:
```typescript
import { useWindowInitData } from '@renderer/hooks/useWindowInitData'
const MyWindowApp: FC = () => {
const data = useWindowInitData<MyInitData>()
if (!data) return null
return <ControlledContent data={data} />
}
```
- On mount: pulls via `WindowManager_GetInitData` invoke (cold-start path).
- On re-use: receives the `WindowManager_Reused` payload (PUSH path, zero round-trip).
- Per-session state resets should live inside the child component in `useEffect([data.someStableId], …)`, so the DOM stays continuous across recycles — never use `key={resetKey}` to forcibly remount; that reintroduces the flash this contract was designed to eliminate.
For the full cold-start vs reuse timing contract, see [Init Data](./window-manager-api-reference.md#init-data) in the API Reference.