Desktop: Multiple window support#11181
Conversation
Related to laurent22#10459, but needs more testing -- this change could cause regressions.
| const parentNode = loaded ? doc?.body : null; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM | ||
| const contentPortal = parentNode && createPortal(props.children, parentNode) as any; |
There was a problem hiding this comment.
Related documentation/explanation:
- React Portals documentation.
- StackOverflow thread with discussion on how to render React components to a new window.
- A blog post about using
createPortalwith new windows.
…me' into test/editor-in-iframe
…en and into separate directory This pull request previously moved the main window command registration logic from MainScreen.tsx to a different component that could manage showing dialogs, etc for different windows. This, however, could cause errors when closing settings (attempts to use commands with unregistered runtimes). This pull request ensures that command runtimes are registered by loading them in Root.tsx, rather than in MainScreen.tsx.
styled-components only works in the main window. See the related styled-components/styled-components#2620.
| return <> | ||
| <div ref={setDependencyStyleContainer}></div> | ||
| <StyleSheetManager target={dependencyStyleContainer}> | ||
| <CacheProvider value={cache}> |
There was a problem hiding this comment.
This code uses CacheProvider from @emotion/react to cause react-select to insert its styles into the new window (see comment). @emotion/react is a dependency of react-select and, at present, is not a direct dependency of Joplin.
Adding @emotion/react to app-desktop/package.json could be problematic if react-select is updated without also updating the @emotion/react version — having multiple versions of @emotion/react may cause styling issues (or perhaps break styling just in new windows).
See this unmerged upstream pull request for a possible future solution that doesn't involve importing transitive dependencies.
| return { | ||
| execute: async (context: CommandContext, noteId: string = null) => { | ||
| noteId = noteId || stateUtils.selectedNoteId(context.state); | ||
| const note = await Note.load(noteId, { fields: ['parent_id'] }); |
There was a problem hiding this comment.
I think noteId could be undefined? If there's no note in the notebook for example
There was a problem hiding this comment.
The enabledCondition is currently oneNoteSelected. As such, the command should only run if triggered by a script or oneNoteSelected is true. To make this requirement clearer, I've added a throw new Error if !noteId.
There was a problem hiding this comment.
No you're right, with the condition the note should indeed be defined so the exception is not necessary. Could you remove it please?
|
It's quite difficult to review this PR since it's very large, but what I've seen looks good. The description in the readme also makes sense. |
See laurent22#11181 (comment) Note: The name might still be improvable
link in new windows This commit ensures that the global event handlers added in main-html.js are also added in new windows.
The download button used bridge().window(...), which had been renamed to bridge().activeWindow(...).
…me' into test/editor-in-iframe
Summary
This pull request allows opening Joplin's editor in a new window using React's portal API.
See background_windows.md for how this is handled by Joplin's reducer logic.
Known issues
Show resolved
Notes need to be reloaded to show updates. (Fixed)
Editor commands (e.g. bold with ctrl-b) are always applied to the last-created editor. (Fixed)
All dialogs (e.g. go to anything, note info) open in the main window. (Fixed)
The editor sometimes incorrectly reloads content — it seems to detect changes from recent saves as edits made by a different editor.
If editing in a secondary window, dialogs (e.g. note statistics) will report information for the note in the main window.
Commands that rely on
selectedNoteIdalways act on the note displayed in the main window.Commands that rely on
selectedNoteIdfor their enabled condition can be incorrectly enabled/disabled in new windows.Toolbar buttons are enabled/disabled based on the current window, rather than the window that they're in.
The tags list always reflects the tags associated with the currently focused window (even if the list is displayed in a background window).
state.notesis refreshed unnecessarily when a window gains focus and it isn't a just-created window.Context menus only work in the main window.
All windows must have the same editor (Markdown/Rich Text).
All windows have the same layout.
all-windows-same-layout.webm
Opening settings closes all secondary windows.
Drag-and-drop doesn't work between windows.
Plugin dialogs are shown on all windows, and (depending on the plugin) may only work correctly in one of the windows.
Enabling the Rich Text Editor in one of the windows disables certain commands (e.g. header, checklist) for all windows.
Adding a new tag in a new window causes the new window to navigate to a different note.
When notes are deleted and in the navigation history, they are only deleted from the navigation history for the last focused window.
The legacy Markdown editor is styled incorrectly (fixed size, Markdown preview hidden) in secondary windows.
Plugin IPC issues when multiple views exist with the same ID.
To allow editors to update when a note is edited in a different window,
useFormNotewas updated. In particular,Set. (Possible performance issue!)await Note.loadresolves. In this case,Note.loadmay return an outdated copy of the note (and not match the last saved content).Plugin API global CSS applies only to the main window.
When "show tray icon" is disabled, closing the main window also closes all secondary windows.
For IPC with plugin dialogs, some plugins assume that there is only one window.
Edit link dialog is only shown in the main window.
BrowserWindow.setMenu(which is Linux & Windows only), however:Menu.getApplicationMenuto update the enabled/checked/disabled state of the app menu. There doesn't seem to be a per-window equivalent ofMenu.getApplicationMenu.Menu.getApplicationMenuandBrowserWindow.setMenuoverwrites secondary window menus with the main window menu wheneverMenu.getApplicationMenuis called.Menu.getApplicationMenuwhen changing the enabled/disabled state of menu items.Screen recording
MacOS:
multi-window.webm
Linux (Fedora 40):
Screencast.from.2024-11-01.14-16-11.webm
Notes
documentandwindowvariables may no longer point to the current component's DOM.useDomhook has been added to get theDocumentfor a component.react-selectto be styled in new windows, it seems necessary to wrap the window's content in theCacheProvidercomponent from one of its dependencies.