ADR 0023: Editor chrome — status bar, dirty tracking, and a custom Open/Save-As panel¶
Status¶
Accepted
Context¶
Phase 14 of the "complete normal editor" plan: status bar, dirty
tracking (explicitly deferred since ADR 0006), and Open/Save-As. The
original sketch for this phase used native
QFileDialog::getOpenFileName/getSaveFileName —
superseded mid-flight by ADR 0022,
written right after Phase 13.5's find-bar restyle, when the user
decided future dialogs should join the same custom floating-panel
system rather than stay native. That decision is executed here: a real
FileBrowserPanel, not a one-line QFileDialog call.
Decision¶
Dirty tracking¶
bool m_dirty on EditorViewport, set true at the end of every
batch mutation (insertText, deleteBackward, deleteForward,
cutSelection, replaceAllMatches, applyUndoResult — the last one
per the plan's "kept simple" call: any undo/redo marks dirty too,
rather than tracking the exact saved-stack position). Cleared by a
successful save(). pasteClipboard/replaceCurrentMatch route
through insertText already, so they're covered without their own
call site.
Status bar and title: one signal, wired at one call site¶
EditorViewport gets its first-ever Q_OBJECT/signal:
void statusChanged(int line, int column, bool dirty), 1-based for
display. Rather than annotating every one of the many call sites that
move the cursor or edit the buffer, it's emitted from inside
ensureCursorVisible() itself — every such call site already calls
that (scrolling the cursor into view is meaningless without knowing
where it is), so this is the one true choke point, cheaper than the
plan's original per-call-site sketch. main.cpp connects it to a
QStatusBar label ("Ln %1, Col %2" + " *" when dirty) and the
window title ("<path|untitled> [modified] — Absolute Simple Editor").
Open/Save-As: FileBrowserPanel, a second FloatingPanel¶
New gui/src/file_browser_panel.{h,cpp}, same family as FindBar:
one LetterBadge — reused for both modes via the new
LetterBadge::setLetter rather than two separate badges (FindBar's
approach doesn't generalize as well here since only one mode is ever
active, unlike Find/Replace's two simultaneous fields) — a path field
that doubles as the current-directory display (typing a name into it
after navigating "just works" relative to wherever you are), and a
QListWidget directory listing.
- Listing:
QDir, dirs-first then alphabetical, dotfiles excluded with no toggle (v1 simplification, easy to add later, not a half- built feature — there's no partial "sometimes shows dotfiles" state). - Navigation: activating (Enter or double-click) a directory entry
or
..navigates; activating a file opens it immediately in Open mode, but only fills the path field in Save-As mode — a stray double-click on an existing file shouldn't silently queue an overwrite. The path field's own Enter always commits (open or save) once it resolves to a non-directory path. EditorViewport::openFile: destroys the current buffer/syntax/ undo stack and loads fresh, resetting every piece of per-buffer state (cursors, scroll, find query, dirty). A missing/unreadable path starts empty with that path kept as the save target — the exact tolerancemain.cpp's own initial-launch handling already had (ADR 0006); opening a not-yet-existing file by name is a normal editor action, not an error, and this phase didn't invent a second standard for it.EditorViewport::saveAs: setsm_filePaththen defers to the existingsave()— dirty-clearing and thestatusChangedemit happen in exactly one place, not duplicated.Ctrl+Swith no path set now opens the Save-As panel instead of silently doing nothing (the gap ADR 0006 flagged).Ctrl+Shift+Salways opens Save-As, even with a path already set — "save as" means "let me pick a different one."- No overwrite confirmation for an existing file (v1 gap, documented rather than half-built — a real confirm flow is its own small floating panel, not a quick add-on).
A real bug found and fixed during live verification¶
Both FileBrowserPanel's path field and its list widget originally
handled Return through Qt's native mechanisms (QLineEdit::
returnPressed, QListWidget's built-in Return-triggers-
itemActivated). Both are now handled directly in the shared
eventFilter instead, consuming the key press before Qt's native
handling ever runs. This wasn't a style preference — it fixed a real,
reproducible bug: confirmPath()/activateEntry() can end in
hideBar(), which moves keyboard focus to EditorViewport
synchronously, from inside the very key-press handling still in
progress. With the native signals, Qt's own event dispatch for both
widget types went on to redeliver that same logical key press to the
now-focused EditorViewport afterward — inserting a stray newline
into whatever file had just been opened or saved, and leaving it
marked dirty immediately after a clean load/save. FindBar never hit
this because its Return handling was already filter-based from
Phase 13, not signal-based — this ADR brings FileBrowserPanel in
line with that, and is the reason both floating panels now use the
same pattern.
Consequences¶
Verified live via xdotool: status bar and window title track cursor
position and dirty state correctly through edits, Ctrl+S, and undo;
Ctrl+O opens the panel, keyboard navigation (Tab → Down ×N →
Return) descends into a subdirectory and back via .., and picks a
file — confirmed clean (no stray newline, no false-dirty title) only
after the Return-handling fix above; Ctrl+Shift+S with a typed
absolute path saves to a new file with exact content preserved;
launching with no file and Ctrl+S opens Save-As pre-populated with
the current working directory's listing. Full test suite unaffected
(this phase is GUI-only; no core/ behavior changed) — 8/8 still
passing.
Not built: overwrite confirmation, a hidden-files toggle, breadcrumb
navigation beyond .. (documented v1 gaps, none half-implemented).
Phase 15 (command line + :compile + output panel) is next.