-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Desktop: Multiple window support #11181
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
createPortal
with 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 import
ing 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think noteId could be undefined? If there's no note in the notebook for example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No you're right, with the condition the note should indeed be defined so the exception is not necessary. Could you remove it please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in 9919dca.
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
selectedNoteId
always act on the note displayed in the main window.Commands that rely on
selectedNoteId
for 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.notes
is 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,
useFormNote
was updated. In particular,Set
. (Possible performance issue!)await Note.load
resolves. In this case,Note.load
may 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.getApplicationMenu
to update the enabled/checked/disabled state of the app menu. There doesn't seem to be a per-window equivalent ofMenu.getApplicationMenu
.Menu.getApplicationMenu
andBrowserWindow.setMenu
overwrites secondary window menus with the main window menu wheneverMenu.getApplicationMenu
is called.Menu.getApplicationMenu
when 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
document
andwindow
variables may no longer point to the current component's DOM.useDom
hook has been added to get theDocument
for a component.react-select
to be styled in new windows, it seems necessary to wrap the window's content in theCacheProvider
component from one of its dependencies.