Skip to content

Desktop: Fixes #14861: Restore window position and size after relaunch on Windows#15003

Closed
gagansokhal-coder wants to merge 3 commits intolaurent22:devfrom
gagansokhal-coder:fix-window-state
Closed

Desktop: Fixes #14861: Restore window position and size after relaunch on Windows#15003
gagansokhal-coder wants to merge 3 commits intolaurent22:devfrom
gagansokhal-coder:fix-window-state

Conversation

@gagansokhal-coder
Copy link
Copy Markdown

Fixes #14861

Problem

On Windows, when the Joplin window is snapped (docked) to the left or right, closing and reopening the app resets the window position and size.

Solution

Ensured that the correct window bounds are saved after snap by handling resize and move events and persisting final bounds.

Testing

Tested the following scenarios:

  • Left snap (Win + Left Arrow)
  • Right snap (Win + Right Arrow)
  • Manual resize
  • Maximized window

All cases correctly restore position and size after relaunch.

Notes

This change focuses on Windows behavior and does not affect macOS/Linux.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Inserted inline comments in createWindow() of packages/app-desktop/ElectronAppWrapper.ts explaining a Windows Snap persistence edge case involving electron-window-state debounce and Joplin's close handling. No executable code or behavioural changes were made.

Changes

Cohort / File(s) Summary
Comments only — Window state rationale
packages/app-desktop/ElectronAppWrapper.ts
Added explanatory inline comments in createWindow() describing a Windows Snap persistence issue, how electron-window-state's debounce can miss final bounds, and how Joplin's close handling (via event.preventDefault()) may prevent the library's closed handler from running. No logic or behaviour was changed.

Suggested reviewers

  • personalizedrefrigerator
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Whitespace/indentation changes in the quitWithSyncCheck() method appear unrelated to the window snap fix and may be out of scope. Remove unrelated whitespace changes from the quitWithSyncCheck() method; keep only changes directly addressing issue #14861.
Linked Issues check ❓ Inconclusive The changeset addresses the core requirement from issue #14861 by adding handling for window bounds persistence after snap, though the code shows only comments were added without visible functional logic changes. Clarify whether the comment block represents a complete fix or if additional functional code changes are pending or hidden in the summary.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing window position/size restoration on Windows after relaunch, which directly addresses the changeset.
Description check ✅ Passed The description is related to the changeset, explaining the Windows snap persistence problem and solution, though it claims functional changes beyond what the code summary shows (only comments added).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Pr Description Must Follow Guidelines ✅ Passed The PR description includes all three required sections: problem description (window position/size reset on Windows snap), high-level solution (bounds saved via event handling), and test plan with specific scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added bug It's a bug desktop All desktop platforms windows labels Apr 3, 2026
@gagansokhal-coder
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Apr 3, 2026
@gagansokhal-coder
Copy link
Copy Markdown
Author

recheck

Comment on lines +580 to +593
this.win_.on('close', () => {
// Cancel any pending debounced save and do a final immediate save
if (snapSaveTimer) {
clearTimeout(snapSaveTimer);
snapSaveTimer = null;
}
try {
if (this.win_ && !this.win_.isDestroyed()) {
windowState.saveState(this.win_);
}
} catch (_e) {
// Window may have been destroyed
}
});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that the final window position and size are saved when the app is closed.

Without this, when the window is snapped (e.g using Win + Arrow keys) the last updated bounds may not be persisted correctly because the debounced save might not execute before the app closes.

By handling the 'close' event, we cancel any pending debounced save and immediately persist the latest window state, ensuring the correct snapped position is restored on next launch.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, when the window is snapped (e.g using Win + Arrow keys) the last updated bounds may not be persisted correctly because the debounced save might not execute before the app closes.

"may not be" - under what condition that can happen?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can happen when the window state is updated shortly before the app is closed, while the debounced save is still pending.

For example, when the user snaps the window (Win + Arrow keys), a resize/move event triggers the debounced save. If the user immediately closes the app before the debounce delay completes, the pending save may not run, so the latest bounds are not persisted.

In that case, the previously saved window state (before snapping) would be restored on the next launch instead of the snapped position.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean if the user resize then close the window within 1 second? is it worth the extra code?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that this scenario happens only when the window is resized or snapped and then closed very quickly.

I added this to ensure consistency, especially for snap behavior, since users might expect the last visible state to always be restored.

However, I agree that if this is considered too edge-case or adds unnecessary complexity, we can simplify the approach or rely on the existing debounced save.

Happy to adjust or remove this part based on your preference.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, remove

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-desktop/ElectronAppWrapper.ts`:
- Around line 607-612: The indentation in the conditional block inside the
ElectronAppWrapper class uses spaces instead of tabs; update the whitespace in
the block containing the lines "if (syncPending)", "dispatch({ type:
'QUIT_SYNC_DIALOG_OPEN' });" and "this.quit();" to use tabs for indentation to
match project TypeScript coding guidelines and ensure consistent formatting
across the method.
- Around line 552-559: The comment inside the ElectronAppWrapper class
referencing a "longer debounced save" for Windows Snap is stale because the
supplemental implementation was removed; either remove the comment entirely or
update it to state the current limitation (that electron-window-state's 100ms
debounce can miss snapped positions and no extra debounced save is implemented).
Locate the Windows Snap comment block in ElectronAppWrapper (near where
electron-window-state and the window close/closed handling are discussed), and
change the wording to accurately reflect the absence of a supplemental save, or
restore the previously-removed longer-debounced save logic and wire it into the
window close/closed handlers so snapped bounds are persisted. Ensure the updated
comment references electron-window-state and the window close/closed behavior so
future readers know the constraint or the fix applied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6619ae1-f763-4098-ba79-280e7dcfb30c

📥 Commits

Reviewing files that changed from the base of the PR and between f6c8b4e and 405e958.

📒 Files selected for processing (1)
  • packages/app-desktop/ElectronAppWrapper.ts

Comment on lines +552 to +559
// Windows Snap fix: electron-window-state uses a 100ms debounce for
// resize/move events, which can miss the final bounds after a Windows
// Snap gesture. Additionally, Joplin's close handler calls
// event.preventDefault() (for the appClose round-trip or tray hide),
// which prevents the library's 'closed' handler from firing — so the
// state may never be written to disk. We supplement with a longer
// debounced save that ensures the snapped position is always persisted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Comment references a non-existent implementation.

The comment states "We supplement with a longer debounced save that ensures the snapped position is always persisted," but no code implementing this supplement is present. Based on the past review discussion, the code that handled this was removed. The comment should either be updated to describe the actual current state (i.e., the limitation exists without a fix) or removed entirely if it no longer applies.

Suggested fix

If the issue remains unaddressed, consider revising the comment to describe the limitation without claiming a fix:

-// Windows Snap fix: electron-window-state uses a 100ms debounce for
-// resize/move events, which can miss the final bounds after a Windows
-// Snap gesture. Additionally, Joplin's close handler calls
-// event.preventDefault() (for the appClose round-trip or tray hide),
-// which prevents the library's 'closed' handler from firing — so the
-// state may never be written to disk. We supplement with a longer
-// debounced save that ensures the snapped position is always persisted.
+// Windows Snap limitation: electron-window-state uses a 100ms debounce for
+// resize/move events, which can miss the final bounds after a Windows
+// Snap gesture if the user closes the app within the debounce window.
+// Additionally, Joplin's close handler calls event.preventDefault() (for
+// the appClose round-trip or tray hide), which prevents the library's
+// 'closed' handler from firing in some cases.

Alternatively, if this comment serves no purpose without the fix, consider removing it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Windows Snap fix: electron-window-state uses a 100ms debounce for
// resize/move events, which can miss the final bounds after a Windows
// Snap gesture. Additionally, Joplin's close handler calls
// event.preventDefault() (for the appClose round-trip or tray hide),
// which prevents the library's 'closed' handler from firing — so the
// state may never be written to disk. We supplement with a longer
// debounced save that ensures the snapped position is always persisted.
// Windows Snap limitation: electron-window-state uses a 100ms debounce for
// resize/move events, which can miss the final bounds after a Windows
// Snap gesture if the user closes the app within the debounce window.
// Additionally, Joplin's close handler calls event.preventDefault() (for
// the appClose round-trip or tray hide), which prevents the library's
// 'closed' handler from firing in some cases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/ElectronAppWrapper.ts` around lines 552 - 559, The
comment inside the ElectronAppWrapper class referencing a "longer debounced
save" for Windows Snap is stale because the supplemental implementation was
removed; either remove the comment entirely or update it to state the current
limitation (that electron-window-state's 100ms debounce can miss snapped
positions and no extra debounced save is implemented). Locate the Windows Snap
comment block in ElectronAppWrapper (near where electron-window-state and the
window close/closed handling are discussed), and change the wording to
accurately reflect the absence of a supplemental save, or restore the
previously-removed longer-debounced save logic and wire it into the window
close/closed handlers so snapped bounds are persisted. Ensure the updated
comment references electron-window-state and the window close/closed behavior so
future readers know the constraint or the fix applied.

Comment on lines 607 to 612
if (syncPending) {
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
this.quit();
}
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
this.quit();
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use tabs for indentation.

The indentation in this method uses spaces instead of tabs, violating the coding guidelines. As per coding guidelines, tabs should be used for indentation in TypeScript files.

🔧 Proposed fix to use tabs
 	public quitWithSyncCheck(
 		dispatch: (action: { type: string; [key: string]: unknown })=> void,
 		syncPending: boolean,
 	) {
 		if (syncPending) {
-	        dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
-        } else {
-	        this.quit();
-        }
+			dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
+		} else {
+			this.quit();
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (syncPending) {
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
this.quit();
}
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
this.quit();
}
}
public quitWithSyncCheck(
dispatch: (action: { type: string; [key: string]: unknown })=> void,
syncPending: boolean,
) {
if (syncPending) {
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
this.quit();
}
}
🧰 Tools
🪛 ESLint

[error] 608-608: Mixed spaces and tabs.

(no-mixed-spaces-and-tabs)


[error] 610-610: Mixed spaces and tabs.

(no-mixed-spaces-and-tabs)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/ElectronAppWrapper.ts` around lines 607 - 612, The
indentation in the conditional block inside the ElectronAppWrapper class uses
spaces instead of tabs; update the whitespace in the block containing the lines
"if (syncPending)", "dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });" and
"this.quit();" to use tabs for indentation to match project TypeScript coding
guidelines and ensure consistent formatting across the method.

@laurent22
Copy link
Copy Markdown
Owner

Thanks for the submission.

While this PR targets an existing issue, the proposed solution is not suitable for the project in its current form. Bringing it up to an acceptable standard would require significant redesign and guidance.

For future contributions, please ensure changes are well-aligned with the existing architecture and sufficiently tested.

Please make sure you read our guidelines: https://github.com/joplin/gsoc

And our AI policy: https://github.com/joplin/gsoc/blob/master/ai_policy.md

@laurent22 laurent22 closed this Apr 14, 2026
@gagansokhal-coder
Copy link
Copy Markdown
Author

Thank you for the feedback. I understand that the approach doesn't align with the current architecture.

I’ll review the guidelines and codebase more carefully before attempting another fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug It's a bug desktop All desktop platforms windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Doesn't remember last docked location + size after relaunch

2 participants