mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 07:03:00 +08:00
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: kangfenmao <kangfenmao@qq.com>
246 lines
11 KiB
Markdown
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.
|