Skip to content

Keep the window title current, and ask before the window closes (#116) - #132

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/schema-issues-kwr4lk
Aug 28, 2026
Merged

Keep the window title current, and ask before the window closes (#116)#132
matt-edmondson merged 2 commits into
mainfrom
claude/schema-issues-kwr4lk

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Finishes #116. The last two acceptance criteria were blocked by ktsu.ImGui.AppImGuiAppConfig.Title is init-only and read once at window creation, and there was no cancellable close hook. I added both upstream in ktsu-dev/ImGuiApp#341, now released as 3.16.0, which this bumps to.

Live window title

Shows the open document plus a trailing * while it has unsaved changes, refreshed each frame. SetWindowTitle skips the write when the title is unchanged, so that costs a string comparison per frame rather than a window call.

The dirty state comes from the undo service via the existing HasUnsavedChanges — not tracked a second time.

The menu-bar indicator stays. It isn't redundant: a maximised window's title bar is easy to overlook, and a tiling window manager may not draw one at all.

Close prompt

Closing the window now raises the same Save / Discard / Cancel prompt New and Open already use, instead of discarding the work silently.

Three things about the hook are worth a reviewer's attention, because each is a way to get it wrong:

The prompt can't be drawn from the callback — it returns before the next frame is rendered. So a close with unsaved work only records the request and refuses; the next frame raises the prompt, and only the user's answer closes for real.

The callback also fires for ImGuiApp.Stop, which is how the confirmed exit closes. Without the explicit exitConfirmed latch the editor would veto its own agreed exit forever — discarding doesn't clear the unsaved flag, so the condition that refused the first close is still true once the user has said to close anyway. That would have been an unquittable window, and it's the one bug in this change I'd most want caught.

A second close while the prompt is up would otherwise stack another prompt, so the request is latched until the first is answered. WithUnsavedChangesGuard grew an optional onCancel for that — the close path is the only caller that needs to know the user backed out; New and Open simply do nothing.

Why the entry point moved

Adding OnClosing tipped SchemaEditor over the CA1506 class-coupling limit (96, limit 95). I checked what actually cost the point rather than guessing: it's the Func<bool> the assignment introduces.

The honest fix was to take out what doesn't belong. Main and the ImGuiAppConfig now live in SchemaEditor/Program.cs — that configuration names a windowing framework and a delegate type per callback, none of which is part of editing a schema. So the fix follows what the analyser was pointing at rather than suppressing it, and the host's whole contract with ImGuiApp is readable in one place. OnStart, OnTick, OnRender, OnMenu and ShouldClose become internal so the entry point can bind them.

Verification, and its limit

299 tests pass on net8.0, net9.0 and net10.0, and the solution builds against 3.16.0 — which is what proves the released package really carries the new API, rather than my assuming it from the merge.

The editor wiring itself is not covered by any test. SchemaEditor has no tests and no harness to write them against — that's #128. What's verified here is that it compiles and that nothing else regressed; the title updates and the close prompt have not been executed by a test. I'd rather say that plainly than let "299 tests pass" imply more than it does.

That's also the strongest argument yet for #128: this change adds exactly the kind of stateful, sequence-dependent logic (a latch, a deferred prompt, a re-entrancy guard) that a headless harness would catch regressions in. ktsu.ImGui.App ships an ImGui.App.Testing package built for this.

Closes #116

🤖 Generated with Claude Code

https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT


Generated by Claude Code

claude added 2 commits August 28, 2026 04:08
The last two acceptance criteria on #116, unblocked by ktsu-dev/ImGuiApp#341
(released as 3.16.0): the config's Title is init-only and read once at window
creation, and there was no cancellable close hook, so neither could be done
from here.

The title now shows the open document and a trailing "*" while it has unsaved
changes, refreshed each frame. SetWindowTitle skips the write when the title
is unchanged, so that costs a string comparison rather than a window call. The
dirty state is read from the undo service through the existing
HasUnsavedChanges, not tracked a second time. The menu-bar indicator stays:
it is not redundant, because a maximised window's title bar is easy to
overlook and a tiling window manager may not draw one at all.

Closing the window now raises the same Save / Discard / Cancel prompt that New
and Open already use, rather than discarding the work silently.

Three things about that hook are worth knowing, because each is a way to get
it wrong:

The prompt cannot be drawn from the callback - it returns before the next
frame is rendered - so a close with unsaved work only records the request and
refuses. The next frame raises the prompt, and only the user's answer closes
for real.

The callback also fires for ImGuiApp.Stop, which is how the confirmed exit
closes. Without an explicit exitConfirmed latch the editor would veto its own
agreed exit forever: discarding does not clear the unsaved flag, so the
condition that refused the first close is still true once the user has said to
close anyway. That would have been an unquittable window.

A second close while the prompt is up would otherwise stack another prompt, so
the request is latched until the first is answered. The guard grew an optional
onCancel for that - it is the only caller that needs to know the user backed
out; New and Open simply do nothing.

The entry point moves to its own Program.cs. Adding OnClosing tipped
SchemaEditor over the CA1506 class-coupling limit, and the honest fix was to
take out what does not belong: the configuration names a windowing framework
and a delegate type per callback, none of which is part of editing a schema.
That is what the analyser was pointing at, so the fix follows it rather than
suppressing it, and the host's whole contract with ImGuiApp is now readable in
one place. OnStart, OnTick, OnRender, OnMenu and ShouldClose become internal
so the entry point can bind them.

The 299 tests still pass on net8.0, net9.0 and net10.0, and the solution
builds against 3.16.0 - which is what proves the released package really
carries the new API. The editor wiring itself is NOT covered: SchemaEditor has
no tests and no harness to write them against, which is issue #128. What is
verified here is that it compiles and that nothing else regressed; the title
and prompt behaviour has not been executed by a test.

Closes #116

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT
The quality gate passed with one new issue: S3358, a nested ternary in the
title expression - the dirty marker's ternary sat inside the has-a-document
one. Splitting the null case into a guard clause and naming the marker reads
better anyway, and the interpolated string no longer has to be parsed inside
out.

Verified locally rather than taken on trust. Probing the editor with
SonarAnalyzer normally cannot compile at all: the referenced library's
pre-existing findings are promoted to errors by the SDK's editorconfig, so the
build stops before the editor is analysed. Suppressing exactly the library's
known baseline rules for the probe lets the editor compile, and it reported
this finding at SchemaEditor.Files.cs(307,24). After the change it is gone,
and the four that remain are pre-existing findings in files this PR does not
touch - two in ButtonTree.cs, two in SchemaEditor.cs.

299 tests still pass and the solution builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit a7bd4e1 into main Aug 28, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/schema-issues-kwr4lk branch August 28, 2026 04:57
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.

Editor: unsaved work can be lost — no Save As menu item, no dirty tracking, stale window title

2 participants