mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-27 10:32:58 +02:00
4a88d6ff7a
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
164 lines
7.2 KiB
Markdown
164 lines
7.2 KiB
Markdown
# Background windows
|
|
|
|
This document explains Joplin's multiple window support from a technical perspective.
|
|
|
|
## Reducer state
|
|
|
|
On desktop, Joplin supports multiple windows with the `backgroundWindows` reducer state. Each entry in `backgroundWindows` contains the state of a window that is in the background (not currently focused). The currently focused window's state is stored in the top-level state object.
|
|
|
|
### State structure
|
|
|
|
Suppose that there are three windows, `window1`, `window2`, and `window3`.
|
|
|
|
Initially, `window1` is focused and the state might look like this:
|
|
```js
|
|
state = {
|
|
backgroundWindows: {
|
|
window2: {
|
|
windowId: 'window2',
|
|
selectedNoteIds: ['id-of-the-note-selected-in-window2'],
|
|
// ... other per-window properties ...
|
|
},
|
|
window3: {
|
|
windowId: 'window3',
|
|
selectedNoteIds: ['id-of-the-note-selected-in-window3'],
|
|
// ... other per-window properties ...
|
|
},
|
|
},
|
|
|
|
// <focused window state>
|
|
windowId: 'window1',
|
|
selectedNoteIds: ['id-of-the-note-selected-in-the-main-window'],
|
|
|
|
// ... other per-window properties ...
|
|
// </focused window state>
|
|
|
|
// <shared state>
|
|
settings: {...},
|
|
profileConfig: {...},
|
|
syncStatus: {...},
|
|
// ... all shared properties ...
|
|
// </shared state>
|
|
}
|
|
```
|
|
|
|
Notice that the state for the focused window is part of the main state object. This is for compatibility. Historically, Joplin only supported one window with at most one open editor. As a result, many commands assume that if `state.selectedNoteIds = ['some-id-here']`, then `state.selectedNoteIds[0]` is the ID of the note currently being edited. By storing the `selectedNoteIds` for the currently active window in the main `state` object, Joplin remains compatible with legacy commands and components.
|
|
|
|
### Switching windows
|
|
|
|
When the user switches from one window to another, a `WINDOW_FOCUS` action is sent to the reducer. The reducer responds by moving the old window's state to `backgroundWindows` and the new window's state to the main `state` object.
|
|
|
|
For example, if a user switches from `window1` to `window2`, the state change looks like this:
|
|
1. Initial state: `window2` is the only background window and `window1` is the foreground window.
|
|
```js
|
|
state = {
|
|
backgroundWindows: {
|
|
window2: {
|
|
windowId: 'window2',
|
|
selectedNoteIds: ['note-in-window2'],
|
|
// ... other per-window properties ...
|
|
},
|
|
},
|
|
|
|
// <main window state>
|
|
windowId: 'window1',
|
|
selectedNoteIds: ['note-in-window1'],
|
|
|
|
// ... other per-window properties ...
|
|
// </main window state>
|
|
|
|
// <shared state>
|
|
// ... all shared properties ...
|
|
// </shared state>
|
|
}
|
|
```
|
|
2. The reducer receives the `WINDOW_FOCUS` action and swaps which window state is in `backgroundWindows`. notice that only the window state is moved (the shared state is unmodified):
|
|
```js
|
|
state = {
|
|
backgroundWindows: {
|
|
window1: {
|
|
windowId: 'window1',
|
|
selectedNoteIds: ['note-in-window1'],
|
|
// ... other per-window properties ...
|
|
},
|
|
},
|
|
|
|
// <main window state>
|
|
windowId: 'window2',
|
|
selectedNoteIds: ['note-in-window2'],
|
|
|
|
// ... other per-window properties ...
|
|
// </main window state>
|
|
|
|
// <shared state>
|
|
// ... all shared properties ...
|
|
// </shared state>
|
|
}
|
|
```
|
|
Window 2 is now the main window.
|
|
|
|
### Extra per-window state
|
|
|
|
Some per-window state is application specific. For example, the `devToolsVisible` property is **specific to the desktop app** and is different for each window (because each window can have its own dev tools).
|
|
|
|
This is supported by giving `backgroundWindows` a type with an extended set of properties in the desktop application's state.
|
|
|
|
When new windows are opened with the `WINDOW_OPEN` action, the default value for the new window-specific properties must be provided. For example, `{ devToolsVisible: false }`.
|
|
|
|
## Rendering components in new windows
|
|
|
|
The [react-dom portals API](https://react.dev/reference/react-dom/createPortal) allows a component to render its children in a different place. In this case, Joplin uses the portals API to render certain components in a new window, while keeping them in the same React component tree.
|
|
|
|
### Same React tree, different DOMs
|
|
|
|
The React portals API allows all editors to be descendants of the `<Root/>` component, while also being in a different HTML `document`:
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
subgraph Main Window
|
|
Root
|
|
Root-->Navigator
|
|
Navigator-->MainContent("[[Main app content]]")
|
|
Root-->EditorWindow1
|
|
Root-->EditorWindow2
|
|
|
|
EditorWindow1-->NewWindowOrIframe1-->Portal1(("Portal"))
|
|
EditorWindow2-->NewWindowOrIframe2-->Portal2(("Portal"))
|
|
end
|
|
|
|
subgraph Window1
|
|
NoteEditor1["Note editor"]
|
|
end
|
|
|
|
subgraph Window2
|
|
NoteEditor2["Note editor"]
|
|
end
|
|
|
|
Portal1-->NoteEditor1
|
|
Portal2-->NoteEditor2
|
|
```
|
|
|
|
This also means that components running in secondary windows (e.g. `Window1`, `Window2` above) are controlled by JavaScript in the main window. As a result,
|
|
- Closing the main window means that secondary windows must also be closed.
|
|
- Hiding the window with `BrowserWindow.hide` is used instead.
|
|
- Component logic must be careful to reference the correct DOM. **Using the global `document` and `window` objects is often incorrect.**
|
|
- Instead of the `document` global variable, use [`HTMLElement.ownerDocument`](https://developer.mozilla.org/en-US/docs/Web/API/Node/ownerDocument) (or the `useDom` hook).
|
|
- Instead of `window`, use [`.defaultView`](https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView) on a [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document) object.
|
|
|
|
### Getting the ID for the current window
|
|
|
|
Sometimes, it's necessary to get the Joplin ID of the current window. This ID allows referencing the state specific to a window in Joplin's reducer.
|
|
|
|
Suppose `<MyComponent>` could be displayed in any window of the app and we want to find the selected note IDs for its window. To do this:
|
|
1. Get the window ID. This can be done by either:
|
|
- Passing it as a `prop`
|
|
- With `useContext(WindowIdContext)`, where `WindowIdContext` is the context that provides the window ID. ([Documentation for `useContext`](https://react.dev/reference/react/useContext)).
|
|
2. Use the window ID to determine the current window state. To do this, use the window ID to look up the state for the current window (e.g. with `stateUtils.windowStateById`).
|
|
3. Get the `selectedNoteIds` from the window state.
|
|
|
|
## Limitations
|
|
|
|
- **All windows have the same menubar:** Currently, Joplin sets the application menubar with `Menu.setApplicationMenu`. This allows the menu to be updated later with `Menu.getApplicationMenu`, but means that all windows have the same menu.
|
|
- An alternative might be to set menus on individual windows with [`BrowserWindow.setMenu`](https://www.electronjs.org/docs/latest/api/browser-window#winsetmenumenu-linux-windows), but this doesn't allow accessing the menu later (e.g. from Playwright) with `Menu.getApplicationMenu` and does not work on MacOS.
|
|
- **Certain UI only available on the main window:** At present, certain parts of the UI (e.g. sidebars, warning banners, etc.) are only visible on the main window.While this simplifies multi-window support by limiting which components need to work in secondary windows, it makes secondary windows more limited than primary windows.
|