Skip to content

DYN-10845: Fix Dynamo window restoring off-screen when saved monitor is unavailable - #17321

Open
johnpierson wants to merge 5 commits into
DynamoDS:masterfrom
johnpierson:fix/dynamo-window-offscreen-restore
Open

johnpierson wants to merge 5 commits into
DynamoDS:masterfrom
johnpierson:fix/dynamo-window-offscreen-restore

Conversation

@johnpierson

@johnpierson johnpierson commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Purpose

Fixes DYN-10845.

Dynamo restores its previous window position on launch. When the monitor it was last shown on is no longer available — undocking/docking a laptop, disconnecting a display, switching between office and home setups, remote sessions, or otherwise rearranging a multi-monitor layout — the saved position can place the window entirely off-screen: running, but inaccessible, with no reliable way to recover it (Win+Arrow does not consistently work on Dynamo's modeless window).

Reported via the community here: https://forum.dynamobim.com/t/dynamo-button-idea-for-ui/115174
Related issue: DynamoDS/DynamoRevit#3429

Root cause

The startup restore logic in DynamoView validated the saved bounds by summing every monitor's width and height (leftLimit/topLimit) and only reset Left/Top to 0 if the saved value exceeded that sum. The virtual desktop is not a simple tiling — monitors can sit at negative coordinates or with gaps between them — so that guard is geometrically meaningless. CheckVirtualScreenSize() only rejected positions past the top/left origin, so a window saved on a monitor that was later unplugged stayed off-screen. Nothing tested whether the saved rectangle actually intersects a currently connected display.

Fix

Replace the summing guard and CheckVirtualScreenSize() with a real visibility test (IsWindowVisibleOnAnyScreen) that intersects the saved bounds against each connected display's working area. The saved bounds are restored only when the window overlaps a live monitor by a usable amount (>= 100 units in both dimensions); otherwise the window is recentered on the primary monitor's work area. This handles negative coordinates and disconnected or rearranged monitors.

All of the geometry is done in device-independent units, which is the unit the WindowX/WindowY/WindowW/WindowH preferences are written in (they are populated from WPF Left/Top in DynamoView_LocationChanged). SystemParameters.WorkArea is already in DIPs and is used directly for the recentering fallback; the System.Windows.Forms.Screen rectangles are in device pixels, so they are scaled by the primary monitor's DIP-to-pixel ratio before being intersected. The scale is derived from the primary monitor, so it is an approximation on mixed-DPI layouts — acceptable here because the check only needs to know roughly where the connected monitors are, and it is bounded by the 100-unit overlap tolerance.

Behavior change worth noting

Previously the window was also reset when the saved size was larger than the virtual desktop. With this change an oversized saved window is restored as-is; because its top-left corner is guaranteed to be on a visible monitor, it remains recoverable and resizable. Called out here so it is not a surprise in review.

Declarations

Check these if you believe they are true

No public API surface changes — both new members are private to DynamoView.

Release Notes

Dynamo no longer opens off-screen when the monitor it was last used on is disconnected or rearranged; the window is recentered on the primary display when the saved position is no longer visible.

Reviewers

Open to the Dynamo team — happy to add specific reviewers on request.

Manual verification: launch Dynamo on a secondary monitor, close it, disconnect that monitor, and relaunch — the window comes back centered on the primary display instead of off-screen. Repeat at 100% and 150% display scaling.

FYIs

@JacobSmall (filed the related DynamoRevit issue)

Validate the saved window bounds against the working area of each
connected display on startup instead of summing monitor widths/heights.
Restore the saved position only when it overlaps a live monitor by a
usable amount; otherwise recenter on the primary monitor. Handles
negative coordinates and disconnected/rearranged multi-monitor layouts.

Related: DynamoDS/DynamoRevit#3429

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Copilot AI lite review requested due to automatic review settings September 2, 2026 12:51

Copilot AI left a comment

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.

🟡 Changes recommended

The new visibility/recenter logic mixes WPF logical units with WinForms screen pixel coordinates under DPI scaling, which can misplace or misclassify window visibility on non-100% DPI setups.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR updates Dynamo’s main window restore logic in DynamoView to prevent Dynamo from launching fully off-screen when the previously-saved monitor layout is no longer available, by validating/restoring the saved bounds only when they meaningfully overlap a currently-connected display and otherwise recentering on the primary display.

Changes:

  • Replaces the prior “sum of monitor widths/heights” guard and CheckVirtualScreenSize() with IsWindowVisibleOnAnyScreen(...) based on rectangle intersection with each display’s working area.
  • Recenters the window on the primary monitor’s working area when the saved bounds don’t overlap any connected display by at least a minimum visible extent (100px).
File summaries
File Description
src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs Reworks startup window-bounds restoration to avoid off-screen launches by validating bounds against connected display working areas and recenters when not recoverable.
Review details

Suppressed comments (1)

src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs:1210

  • IsWindowVisibleOnAnyScreen intersects the saved bounds (coming from WPF WindowX/Y/W/H doubles, i.e., WPF logical units) with Screen.WorkingArea (device pixels). On DPI-scaled displays this mixes coordinate spaces and can misclassify a window as visible/not visible. Consider converting the WPF bounds to device pixels via PresentationSource.CompositionTarget.TransformToDevice before intersecting (see src/DynamoCoreWpf/Windows/ModelessChildWindow.cs for an existing pattern).
            foreach (var screen in System.Windows.Forms.Screen.AllScreens)
            {
                var overlap = System.Drawing.Rectangle.Intersect(screen.WorkingArea, bounds);
                if (overlap.Width >= MinVisibleWindowExtent &&
                    overlap.Height >= MinVisibleWindowExtent)
                {
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +206 to +210
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
Width = Math.Min(1024, workingArea.Width);
Height = Math.Min(768, workingArea.Height);
Left = workingArea.Left + (workingArea.Width - Width) / 2;
Top = workingArea.Top + (workingArea.Height - Height) / 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a great fix!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in 8ab87aa. All of the geometry now happens in device-independent units:

  • The fallback centering uses SystemParameters.WorkArea instead of Screen.PrimaryScreen.WorkingArea, so it is already in DIPs and matches what Left/Top/Width/Height expect.
  • The visibility check keeps Screen.AllScreens (there is no WPF API that enumerates monitors), but each WorkingArea is scaled by SystemParameters.PrimaryScreenWidth / Screen.PrimaryScreen.Bounds.Width before being intersected with the saved bounds, and the intersection now uses System.Windows.Rect rather than System.Drawing.Rectangle so nothing is truncated to integers.

The scale factor is derived from the primary monitor, so it is an approximation on mixed-DPI layouts. That is documented on the helper and is acceptable here because the check only needs to know roughly where the connected monitors are — it is bounded by the 100-unit overlap tolerance. It also degrades correctly for a DPI-unaware host, where the ratio is 1.

Address review feedback: System.Windows.Forms.Screen reports device
pixels, while WPF Left/Top/Width/Height and the saved WindowX/Y/W/H
preferences are device-independent units. On non-100% DPI the previous
comparison mixed the two coordinate spaces, so the visibility check and
the recentering fallback could land in the wrong place.

Use SystemParameters.WorkArea (already DIPs) for the fallback bounds and
scale each Screen working area by the primary monitor's DIP-to-pixel
ratio before intersecting it with the saved bounds.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@johnpierson johnpierson changed the title Fix Dynamo window restoring off-screen when saved monitor is unavailable DYN-10845: Fix Dynamo window restoring off-screen when saved monitor is unavailable Sep 2, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-10845

@johnpierson

Copy link
Copy Markdown
Member Author

Findings summary

Symptom. After a monitor layout change (undocking, unplugging a display, remote sessions, home/office swaps), Dynamo launches but its window is off-screen and unreachable on any connected display. The process is running, but standard Windows recovery (Win+arrow) is unreliable because Dynamo's window is modeless.

Where. src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs — the window-position restore block in the DynamoView constructor, plus the CheckVirtualScreenSize() helper. Since DynamoView is shared, this affects both Sandbox and Revit.

Root cause. On startup Dynamo restores the last-saved WindowX/Y/W/H, but the validation didn't confirm the saved rectangle lands on a currently-connected display:

  • It summed the width and height of every monitor into leftLimit/topLimit and only reset the position to 0,0 if the saved value exceeded that total. The virtual desktop isn't a simple tiling (monitors can sit at negative coordinates or with gaps), so that sum is meaningless as a bounds check.
  • CheckVirtualScreenSize() only rejected positions past the top/left origin (< origin - 10); nothing verified the window intersected a live monitor.

As a result, a window last shown on a monitor that's since been removed keeps its stale coordinates and renders off-screen.

Fix. Replace the summing logic and CheckVirtualScreenSize() with a real visibility test, IsWindowVisibleOnAnyScreen(...), which intersects the saved bounds against each connected display's WorkingArea. Saved bounds are restored only if the window overlaps a live monitor by a usable amount (>= 100px in both dimensions); otherwise the window recenters on the primary monitor's working area. This handles negative coordinates and disconnected/rearranged layouts.

Behavior change to note. Previously an oversized saved window (larger than the virtual desktop) was reset. Now it's restored as-is; because its top-left still lands on a live monitor it remains reachable and resizable.

Scope / limits. Core-side fix, covers Sandbox + Revit. A manual "reset window position" command was intentionally left to the Revit side, tracked in DynamoDS/DynamoRevit#3429.

@jasonstratton
jasonstratton self-requested a review September 3, 2026 21:01

@edwin-vasquez-ucaldas edwin-vasquez-ucaldas left a comment

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.

LGTM.
Just one question: Each Screen working area is converted with the primary monitor scale, this is good to despite monitors potentially using different scales.

Reword the doc comment to explain that, because Dynamo is System-DPI-aware,
Windows virtualizes every monitor's coordinates to the single system scale,
so the primary-derived factor is correct for all displays rather than an
approximation. Note the Per-Monitor V2 condition under which it would need a
per-monitor GetDpiForMonitor lookup. Addresses review feedback from Edwin.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@johnpierson

Copy link
Copy Markdown
Member Author

@edwin-vasquez-ucaldas thanks for the review! Good question. It's safe because Dynamo runs System-DPI-aware (no Per-Monitor V2 manifest), so Windows virtualizes every monitor's coordinates to the single system/primary DPI before we ever read them — Screen.WorkingArea already comes back in that one uniform scale for all displays, so the primary scale factor is the correct one for the whole desktop, not just an approximation on mixed-DPI layouts. The 100-unit overlap tolerance covers any rounding on top of that.

The only case where per-monitor scales would matter is if we ever move Dynamo to Per-Monitor V2 awareness, where each display can carry its own scale — at that point this helper would need a per-monitor GetDpiForMonitor lookup instead. I tightened the doc comment on GetDeviceToDipScale() in cf28018 to spell that out.

@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

if (IsWindowVisibleOnAnyScreen(savedBounds))
{
Left = savedBounds.Left;
Top = savedBounds.Top;

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.

Saved Top is restored unclamped, so the title bar can still end up off-screen.

The visibility test only requires a 100-unit overlap somewhere on a connected display, and the old WindowY < oy - 10 guard was removed, so nothing now keeps the caption row on-screen.

Scenario: two monitors stacked vertically (secondary at 0,-1080, primary at 0,0). Dynamo is left straddling the boundary at Top = -300, Height = 768, then the secondary is disconnected. IsWindowVisibleOnAnyScreen intersects (0,-300,1024,768) with the primary work area (0,0,1920,1032) and gets a 1024x468 overlap, which passes both >= 100 checks, so Top = -300 is restored. The top 300 DIPs of the window (title bar, menu bar, toolbar) are above the screen edge -- the user cannot drag it back, and per the PR description Win+Arrow is unreliable on this modeless window. The old code reset this case to 0,0 / 1024x768.

Suggest that, once the bounds are deemed visible, Top (and ideally Left) is clamped so the caption row lies inside the working area of the screen the window intersects.

{
Left = savedBounds.Left;
Top = savedBounds.Top;
Width = savedBounds.Width;

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.

An oversized saved size is now restored verbatim, which can push all of the window chrome off-screen.

The PR body calls out that the size guard was dropped, but the concrete failure mode is worth weighing: the old WindowW > VirtualScreenWidth || WindowH > VirtualScreenHeight check was the only clamp, and nothing replaced it.

Scenario: the user runs Dynamo maximized on a 3840x2160 display, so DynamoView_SizeChanged persists WindowW ~= 3856 / WindowH ~= 2176 and DynamoView_LocationChanged persists WindowX/Y = -8. They later launch on a laptop with only a 1920x1080 display. The saved rect overlaps the primary work area heavily, so this branch restores Left/Top = -8 and Width/Height = 3856x2176 -- roughly double the screen in each dimension. The minimize/maximize/close buttons (top-right) and the ResizeMode="CanResizeWithGrip" grip (bottom-right) are both off-screen, leaving only Alt+Space / Alt+F4. Previously this reset to 1024x768.

Clamping the restored Width/Height to the working area of the intersecting screen would keep the off-screen fix without this regression.

/// on is no longer available.
/// </summary>
/// <param name="bounds">Window bounds in device-independent units.</param>
private static bool IsWindowVisibleOnAnyScreen(Rect bounds)

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.

No test coverage, and the shape makes it untestable.

This predicate is the entire correctness surface of the fix -- negative coordinates, disconnected monitors, the DPI scale derivation, and the 100-unit tolerance -- but it is private static on a WPF Window, so no test can reach any of its branches. A later tweak to the tolerance or the scale factor could silently reintroduce the off-screen launch with a green CI.

Consider making it internal static bool IsWindowVisibleOnAnyScreen(Rect bounds, IEnumerable<Rect> screenWorkAreas) (with the Screen.AllScreens + scaling lookup left at the call site). That makes the interesting cases assertable from DynamoCoreWpfTests: fully on-screen, negative-coordinate maximized (-8,-8), a sliver overlap below the tolerance, and no overlap after a monitor is removed.

@jasonstratton jasonstratton left a comment

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.

Roberto had 3 comments that need to be addressed before merging:

  1. DynamoView.xaml.cs:200 — restored Top isn't clamped, so a window straddling two stacked monitors can still land with its title bar/menu above the visible screen edge once the second monitor is gone. Concrete repro given (secondary at 0,-1080, saved Top=-300). This is literally a variant of the bug the PR is fixing.
  2. DynamoView.xaml.cs:201 — the old oversized-window guard was dropped with nothing replacing it; a window maximized on a 4K display restored on a 1080p laptop keeps ~3856×2176 dimensions, pushing the resize grip and min/max/close buttons off-screen.
  3. DynamoView.xaml.cs:1201 — IsWindowVisibleOnAnyScreen is private static on a WPF Window, so this is the entire correctness surface of the fix and it's untestable from DynamoCoreWpfTests. Suggests internal static with Rect/IEnumerable params.

Looks like the failing PR checks are due to infrastructure failures. I will record them, but when changes are made, they should rerun and hopefully succeed on that run.

@edwin-vasquez-ucaldas

Copy link
Copy Markdown
Contributor

Just agree with the Roberto's comments, when you have a chance to address before merge. Also same suggestion from the Jason's comment.

@jasonstratton
jasonstratton requested a review from a team as a code owner October 8, 2026 19:25

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the ticket for this pull request: https://autodesk.atlassian.net/browse/DYN-10845

@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants