Skip to content

feat(editor): 2D Editor pane, status instructions, generated shortcut map (#213)#261

Merged
JArmandoAnaya merged 2 commits into
mainfrom
p1-s4-editor2d-shortcuts
Jul 15, 2026
Merged

feat(editor): 2D Editor pane, status instructions, generated shortcut map (#213)#261
JArmandoAnaya merged 2 commits into
mainfrom
p1-s4-editor2d-shortcuts

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Final sprint of Pillar P1 — Interaction & Navigation (epic #250), on top of the merged p1-s1/s2/s3. Runs last so the shortcut registry captures the final bindings.

Closes #213

2D Editor pane

The vertical profile becomes the first page of a tabbed 2D Editor pane; the cross-section and Signal Phase Editor (GW-4 step 4, p4-s5) plug in behind the same Editor2DPage interface.

The host is thin on purpose: a page keeps its own selection subscription and its own commands. That's why ProfilePanel moved without a line changing and test_profile_panel.cpp passes unmodified — the strongest evidence the seam is in the right place. The host only raises a relevant tab, and won't yank a tab you're already using.

The dock is dock.editor2d, not dock.profile. restoreState matches saved geometry to docks by objectName, so reusing the name would let a returning user's stale entry — placed and sized for the old panel — capture a differently-shaped pane. An unknown name is ignored, so the default placement wins. StaleProfileLayoutDoesNotCaptureTheRenamedDock pins the Qt behaviour that decision rests on (MainWindow needs GL and can't be built offscreen, so the test uses a bare QMainWindow).

One table for every binding

app/shortcut_registry.cpp is now the single source: Actions binds from it via setShortcuts, and docs/user-guide/shortcuts.md is rendered from it, with a gtest gate that fails when they drift. Before this, key letters were hand-copied into tooltips and nothing noticed when they went stale.

I verified the gate can fail, not just pass: changing Split's key to G makes it fail with the regenerated table in the message. A gate that can't fail isn't one.

The page cannot be rendered from StandardKey

Generating it exposed a real trap: QKeySequence::keyBindings() returns a different list per platform — Redo is three sequences on Windows, one on macOS — and some are empty (Quit has none on macOS; the platform menu owns ⌘Q, so the page documented a blank). A committed page generated from it would pass the gate on the machine that made it and fail CI on the other two. So standard rows carry the one spelling the docs commit to, with platform variants in the Notes column, while the app still binds natively per OS. (Also: fromLatin1 mangled the UTF-8 ellipsis into Save as⦠— now fromUtf8.)

Status line — the render caught a real defect

Adding a Tool::instruction() and a label wasn't enough. Rendering the window showed why: every tool also emitted the same guidance transiently from activate(), so the status bar carried two wordings of one sentence — first overlapping, then adjacent with the transient one clipped.

instruction() is now the single source per tool. It feeds both the persistent status line and the viewport corner hint (issue #103 — during an interaction your eyes are on the viewport, and removing the activation message would otherwise have regressed that). The transient channel keeps only results and state-dependent guidance ("Merged", refusals). The label is permanent, so showMessage can neither hide nor paint over it. SelectTool::instruction() branches on move_mode_, as its activate() message did — one tool, two modes.

Status bar now, on the sample scene:

Click to select · ⇧ adds, Ctrl toggles · drag a road body to move it, a node handle
to bend it, or empty space for a rubber band        16 roads · 44 lanes · 1 junctions  kernel 0.6.0

Tests

  • NEW test_editor2d_host.cpp (7): tab registration/order, raise-the-relevant-page, keep-a-relevant-tab, no arbitrary pick, the ProfilePanel migration keeping its own subscription, selection-driven raise, the stale-layout guard.
  • NEW test_shortcut_registry.cpp (6): every Id has exactly one row, primary-first with the alternate kept, NoDuplicateActiveBindings, V-is-frame-cursor/Select-is-Q, Actions bind what the table documents, and the page gate (RM_DOCS_DIR compile define).
  • test_profile_panel.cpp unmodified and green.

Verification

  • 837/837 green on ci-macos (-Werror); whole-tree clang-format --dry-run --Werror clean.
  • Removing the eight activate() emissions broke nothing — the tool tests spy after activate(), so they cover the state-dependent messages.
  • Full-window render (--screenshot-ui) checked after each status-bar change; that's what surfaced the overlap.

Acceptance

GW-2 step 7 (profile edits in the 2D Editor pane); the host API is sufficient for GW-4 step 4; GW-1's "documented shortcut map" pass criterion holds, and is now enforced.

Close-out

The GW-1 self-check follows as a comment on #250. It is not a GW-1 pass — every chord is a gesture, so steps 2–6, 10 and 14 need a hand on a real mouse. The results table stays empty until Armando runs it.

… map (#213)

Closes out P1 on top of the merged p1-s1/s2/s3.

The vertical profile moves into a new tabbed "2D Editor" pane, ready for
the cross-section and Signal Phase editors (GW-4 step 4). The host is
thin on purpose: a page keeps its own selection subscription and its own
commands, so ProfilePanel moved without a line changing and
test_profile_panel.cpp passes unmodified. The host only raises a relevant
tab, and never yanks a tab the user is already using.

The dock is dock.editor2d, NOT dock.profile: restoreState matches saved
geometry by objectName, so reusing the name would let a returning user's
stale entry capture a differently-shaped pane. An unknown name is
ignored instead, and the default placement wins.

Every binding now comes from one table (app/shortcut_registry.cpp):
Actions binds from it via setShortcuts, and docs/user-guide/shortcuts.md
is rendered from it, with a gtest gate that fails when the two drift —
before this, key letters were hand-copied into tooltips and nothing
noticed when they went stale.

The page cannot be rendered from QKeySequence::StandardKey: keyBindings()
returns a DIFFERENT list per platform (Redo is three sequences on
Windows, one on macOS) and some are empty (Quit has none on macOS, where
the platform menu owns it), so a committed page would pass the gate only
on the machine that generated it. Standard rows therefore carry the one
spelling the docs commit to, and the app still binds natively.

The status bar gains a persistent instruction line fed from a new
Tool::instruction(). Rendering the window showed why that needed more
than a new label: every tool ALSO emitted the same guidance transiently
from activate(), so the bar carried two wordings of one sentence, the
transient one clipped. instruction() is now the single source — it feeds
both the status line and the viewport corner hint (issue #103) — and the
transient channel keeps only results and state-dependent guidance. The
label is permanent, so showMessage can neither hide nor overlap it.

Refs #213
Windows CI caught this; clang did not. `Qt::KeypadModifier | Qt::Key_8`
yields a QKeyCombination, so storing it in an `int` field went through
QKeyCombination::operator int() — deprecated since Qt 6.0. MSVC reports
the deprecation (C4996) and CI builds with /WX, so the editor lib failed
to compile on Windows while macOS and Linux were green.

The field type was simply wrong, so fix the type rather than silence the
warning. A plain Qt::Key still converts implicitly, so unmodified rows
read as before; `alternate` now spells "none" as Qt::Key_unknown instead
of 0.
@JArmandoAnaya
JArmandoAnaya merged commit 9e0dc52 into main Jul 15, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the p1-s4-editor2d-shortcuts branch July 15, 2026 21:41
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.

p1-s4: 2D Editor pane host + status-bar tool instructions + shortcut map

1 participant