Editor — informed-consent confirmations + status badge + evo gating + 3-digit level
Data-integrity UX pass (Twilight):
- Reusable ConfirmPopup (fragments/general/, added to app.qrc): centered alert modal matching the storage "Boxes Formatted" warn; ask(title, body, action, label) runs action on confirm. One shared instance lives in PokemonDetails; a confirmAsk(...) function (a property var holding a closure) is threaded down PokemonDetails → DetailsPages → MovesTab → PokemonMoveSel so all destructive buttons share it (no popup-per-row).
- Confirmed actions: whole-mon Re-Roll, Reset, Max Out, Correct Data, Evolve, De-Evolve, the move bulk ops (clear-all-but-first / re-roll all / make all valid), and per-move Delete + Validate. Not confirmed: Heal, per-move randomize, individual-field re-rolls (per the "individual fields are fine" rule).
- Evolution group is visible: hasEvolution || hasDeEvolution (hidden for standalone/glitch mons); each arrow stays enabled-gated per direction.
- Status badge on the GlancePane sprite (upper-right): getStatusIcon(status) bit-decode + the assets/icons/status/*.png (same as PokemonBoxView), shown only when afflicted.
- Level field width → 3*font + padding so "100" fits (maxLength was already 3).
- QML-only; tst_qml_screens 17/17, full ctest 67/67 green; screenshot-verified. VERSION → 0.14.0-alpha.
Editor actions — relocate from the Moves tab to the tab header bar
Twilight clarified the actions bar belongs in the DetailsPages header (accent) bar, after the 3 tabs, not in the Moves-tab body. Moved both groups (Data [Reset | Max Out | Correct Data], Evolution [← fish →]) there, right-aligned over the empty header space (anchors.right: bar.right). New HdrBtn component = a flat icon button styled for the accent bar (light icon, light hover wash, light divider); groups get a light border. The fish is a non-interactive Button (hoverEnabled false, no onClicked, background: Item {}) so its icon.color tints it light reliably (an Image would stay dark; avoided a MultiEffect/QtQuick.Effects import). Removed the panel from MovesTab. Now visible on all tabs. QML only; ctest 67/67 green; screenshot-verified. VERSION → 0.13.1-alpha.
Pokémon editor — wider preview, Moves-tab actions section, validate compaction
Bundle of editor changes:
- Preview pane ~10% wider: PokemonDetails split 62/38 → 58/42 (DetailsPages 0.58, GlancePane gets the rest).
- Validate no longer leaves a gap: new PokemonBox::correctMoveAt(ind) = correctMove() (which can clear a duplicate/invalid move) then cleanupMoves() to compact; the per-row validate calls it. Test box_correctMoveAtCompacts.
- Toolkit button removed → actions section in the Moves tab. The footer Toolkit menu (Max Out / Correct Data / Reset / Evolve / De-Evolve) is gone; footer is now Re-Roll + Heal (AppFooterBtn3 → AppFooterBtn2). A new panel below the move rows holds a Data group [Reset | Max Out | Correct
Data] (icons down-left-and-up-right-to-center, up-right-and-down-left-from-center, file-circle-check) and an Evolution control [arrow-left | fish | arrow-right] (de-evolve / evolve, enabled by hasDeEvolution/hasEvolution). Reuses the top bar's SegBtn group styling.
- 5 new FA icons downloaded (Invoke-WebRequest) + added to app.qrc (FA already credited).
- Full ctest 67/67 green; moves tab + wider pane screenshot-verified. VERSION → 0.13.0-alpha.
Moves tab — align reveal group with the top bar + drop the panel edge
Final reveal polish: the slid-in tool group's rightMargin now matches the top bar's (10) so the per-row [validate | random | delete] line up in the same columns as the bulk buttons above (were ~6px off); removed the panel's hairline left edge (the "border effect"). QML-only; ctest 67/67 green. → 0.12.4-alpha.
Moves tab — tool-reveal restyle to match the top bar
Review: the accent slide-out looked out of place + the buttons were huge/uneven (fillWidth stretched them, leftmost widest). Restyled:
- Panel backing is now the row colour (hides the value box behind it; hairline left edge), and the tools are the same bordered group as the top bar — RowBtn with padding:7 + icon 18, even widths, dark icons. Dropped the accent surface + light RowBtn variant (now unused → removed).
- Validate icon → file-circle-check (FA-free, Invoke-WebRequest-downloaded to assets/icons/fontawesome/, added to app.qrc; FA already credited) on the per-row reveal AND the top bar.
- Consistent order: validate · random · destructive, both places — per-row [validate | random |
delete], top bar [validate | random | clear-all].
- tst_gui_moves (5/5) + tst_qml_screens (17/17) + full ctest (67/67) green; rest + revealed screenshot-verified. VERSION → 0.12.3-alpha.
Moves tab — tool-reveal polish (handle + full-cover panel, icons)
Review feedback on the row tool reveal:
- Handle-triggered, full-cover reveal. The previous reveal triggered on whole-row hover and only partly covered the value box. Now a small chevron handle (angle-left) lives in a reserved right-edge lane (mainRow.rightMargin: 26) so the value box is fully visible + editable at rest; hovering the handle OR the panel (handleHover/panelHover) slides in a solid accentColor panel (rounded left, flush right) that fully covers the value cluster, with light icons (RowBtn { light:
true } → white icon + white hover wash). Width 138 chosen to cover value+max+/max completely. Still an overlay + clip:true root → no reflow. The handle fades out as the panel covers it.
- Icon consistency: top-bar clear-all-but-first → trash-alt (matches per-row delete; was broom); make-valid (per-row + top-bar) → magic wand (was check-double, which is the select-all icon).
- All icons already in the FA set + qrc (trash-alt, magic, angle-left), no downloads.
- tst_gui_moves (5/5) + tst_qml_screens (17/17) + full ctest (67/67) green; rest + revealed states screenshot-verified. VERSION → 0.12.2-alpha.
Moves tab — fix PP/PP-Up desync, hover-reveal actions, GUI tests
In-app review surfaced inconsistent, hard-to-repro bugs: the PP / PP-Ups views desynced (flipping views or hitting "max" mixed/added the two fields). Diagnosed + fixed:
- Desync root cause: a SINGLE shared text box wrote pp or ppUp based on showPpUps, and its maximumLength: showPpUps ? 1 : 2 flip on toggle truncated the displayed PP ("30"→"3") which fired onTextChanged and wrote it into ppUp. The "max" buttons fed the same shared box. Fix by construction: two independent boxes (movePP<i> / movePPUp<i>), each bound to + writing ONLY its field, swapped by a StackLayout. No shared state → no possible cross-write.
- Row actions = hover slide-in reveal. Per-row randomize/make-valid/delete moved into an OVERLAY panel anchored to the row's right edge that slides in (x translate + opacity) on HoverHandler, over an opaque backing matching the row (rowColor, passed altRow from MovesTab). It's not in the layout flow
- the row is clip:true, so it's hidden at rest and never reflows on hover. Reclaims the row space.
- Dropped the min/reset (|←) button, kept only →| (max), per the space request.
- GUI testing (tst_gui_moves, registered in CMake): drives the live editor — viewToggle_
neverMutatesData (the desync regression), valueBoxes_areIndependentAndWriteThrough (editing one box never touches the other field; uses values within the move's max since onMoveIdChanged legitimately clamps PP to getMaxPP), modelChange_reflectsInBothBoxes. Boxes carry stable objectNames; find them rooted at the editor via GuiApp::collectItems (itemByName searches the window root, where an instantiated screen isn't parented). Plus move_ppAndPpUpIndependent at the model level.
- Full ctest 67/67 green; both views + the hover reveal screenshot-verified. VERSION → 0.12.1-alpha.
Moves tab — PP/PP-Ups dual view, per-move actions, bulk top bar
Functional pass on the Moves tab to Twilight's spec:
- Fixed-width columns. Type chip is a fixed 58px; the PP editor and action group are fixed; the move name combo fills the remainder — so name + type stop jittering row-to-row (the complaint).
- PP / PP-Ups view toggle (tab-level showPpUps, a SegSel [PP | PP Ups] in the top bar). Each row's editable box edits current/max PP, or current/max PP-Ups (max 3). One state for the whole tab so it stays consistent through drag-reorder (the values follow their move).
- Arrow-to-line min/max flank the value box (|← set to 0, →| set to cap/3), reusing the DV/EV arrow-left/right-to-line icons.
- ⋮ menu removed. Per-move connected action group instead: randomize (dice) / make valid (check-double) / delete (trash). Delete compacts (no gaps) via new PokemonBox::deleteMoveAt().
- Top bar bulk actions: clear-all-but-first (broom → new clearMovesButFirst()), randomize all (dice), make all valid (check-double). The broom's enabled state tests movesAt(1).moveID > 0 since movesCount() isn't QML-callable.
- Icons all already existed in the FA set + app.qrc (broom, check-double, dice, trash-alt, arrow-left/right-to-line) — no downloads. Components SegSel/SegBtn copied from the DV/EV tab; per-row RowBtn is the same flat-segment idea.
- Tests: box_deleteMoveAndClearButFirst. tst_pokemonbox 22/22, tst_qml_screens 17/17, full ctest 66/66. Both views screenshot-verified (PP shows 30 / 35; PP-Ups shows 2 / 3). VERSION → 0.12.0-alpha.
Moves tab — fixes from in-app review (empty/blank rows)
Twilight reviewed the new Moves tab in-app and it rendered empty — a great catch the screen-load test misses (it gates on QML warnings, not on rows being visible). Debugged with the headless screenshooter (rendered the editor Moves tab to a PNG, added temp diagnostics to read each row's boxData/rowMove state). Three separate bugs:
- Zero-width rows. The panel used a plain Column (anchors.left/right) with delegates bound to width: rowsCol.width. A Column derives its OWN width from its children, so a child reading rowsCol.width is circular → 0 width → invisible rows. Fix: ColumnLayout + Layout.fillWidth (exactly OverviewTab's panel).
- id: top collision → monMove always null. Both MovesTab (root) and PokemonMoveSel (root) used id: top. Inside the Repeater delegate — which instantiates a PokemonMoveSel — the inner top shadowed the outer, so movesTab's top.boxData resolved to PokemonMoveSel's own null boxData. The root probe read top.boxData=Y while the delegate read top.boxData=n, which nailed it. Fix: rename the MovesTab root id to movesTab. (Lesson added to qt-patterns.md.)
- Wrong combo move NAME (type + PP correct). brg.moveSelectModel is per-mon and rebuilt async (PokemonDetails.onCompleted → monFromBox flips it from the general list to this mon's specific list). Syncing currentIndex inline raced the rebuild, landing on a wrong/--- Incompatible Moves --- row. Fix: a single Qt.callLater-deferred syncMoveCombo() called from onCompleted / monMove change / model monChanged / species change.
Plus: row height is now a delegate-local readonly property int rowHeight: 44 (reading top.rowH through the root from a delegate went [undefined] transiently and zero-collapsed the row). After the fixes the screenshooter renders Ember / Mega Punch / Flamethrower / Earthquake with correct types + PP in the new style. tst_qml_screens + tst_gui_fidelity + full ctest (66/66) green. QML-only, so VERSION stays 0.11.0-alpha (the redesign commit, still unreleased).
Moves tab — style-language redesign + drag-to-reorder
Modernized the Moves tab to the General/DV-EV language and made moves draggable to reorder (Twilight's ask). Two design forks confirmed up front: grouped panel + type accent (not full pills), and only filled moves reorder (empties parked at the bottom).
- Row visual. PokemonMoveSel went from a whole-row type-colored pill to the controls of a zebra row inside one white grouped panel (MovesTab now a ScrollView → panel → Repeater of 4, with the 16px scrollbar lane reserved). Type identity is kept by a colored left accent strip + a faint type chip instead of the pill. Combo / PP field / / maxPP / ⋮ overflow menu all preserved (PP-Up submenu, per-move restore/re-roll/correct, All-Moves bulk ops).
- Drag-to-reorder. A left grip handle (filled rows only) lifts a row; mechanics copied from the Bag list drag (ItemBoxView): content reparents to Overlay.overlay, a dashed insertion caret marks the slot (top edge = before / bottom edge = after, so the lower half of the last move appends), drop deferred via Qt.callLater. Backed by PokemonBox::reorderMove(from, to) (new Q_INVOKABLE) that anchor-splices the filled slots' (id/pp/ppUp) values among the fixed moves[4] objects — the slot QObjects keep identity so QML's movesAt() pointers stay valid; PP rides with its move. Only the reordered move bytes change (byte-fidelity; tst_gui_fidelity confirms a browse changes no bytes).
- Three QML landmines hit + fixed (all in tst_qml_screens, which gates main):
- movesMax()/movesCount() are plain C++ methods, not Q_INVOKABLE → QML can't call them (TypeError: ... is not a function). Used the constant 4 for the Repeater model.
- The Repeater delegate reads through boxData/monMove, both of which go null/undefined transiently during build & the screen-load test's inject-then-complete. Made PokemonMoveSel fully null-safe (guard every monMove. deref incl. the eagerly-evaluated Menu items) and null-guarded rowMove.
- Even after that, four delegate bindings read top.rowAlt/top.rowH/top.boxData and warned Unable to assign [undefined] — reading through the root top id mid-build/teardown. Inlined the constants (literal tint, row.height) and coerced top.boxData ? … : null. → clean.
- Tests. New box_reorderMove (first→last/append, last→first, mid insert-before, empty-source no-op + compaction). tst_pokemonbox 21/21, tst_qml_screens 17/17, tst_gui_fidelity 9/9, full ctest 66/66. VERSION 0.10.1-alpha → 0.11.0-alpha (MINOR — whole-tab redesign + feature).
DV/EV tab — refinement round (icons, rounded active edges, shiny placement)
Follow-up polish on the DV/EV tab from Twilight's review:
- Min/Max icons → horizontal "arrow-to-line". The vertical double-chevrons read as up/down, not min/max. Replaced with |← (reset to 0) and →| (max), re-ordered low→high as [ |← | dice | →| ]. The set had no such glyph, so I hand-authored two FA-style SVGs (arrow-left-to-line.svg, arrow-right-to-line.svg — a bar + a left/right arrow, plain black so icon.color tints them) and added both to app.qrc. FontAwesome is already credited, so no credits change.
- Fixed the segmented "Market" flat-edge. An active end segment showed a square corner where the group is rounded (the same bug the Pokémart strips have): clip:true on a Rectangle clips to the bounding box, not the rounded radius. Fix: give the segment's fill per-corner radii (topLeftRadius/bottomLeftRadius when first, topRightRadius/bottomRightRadius when a new last flag) — Qt 6.7+. Now the accent fill follows the group's rounded corners. Applied to both SegSel and SegBtn.
- Future Shiny: label moved above the control row (a stacked section, not a left label), and the whole section is DV-tab-only (visible: statKind === "DV") — shininess is derived from DVs, so it has no meaning on the EV tab.
- EV Max/Reset already retarget through the one shared combo, so both kinds pick up the new icons identically (no separate EV path).
tst_qml_screens + full ctest green; screenshot-verified (tmp/screenshots/editor/pokemon_editor_dvev.png). VERSION 0.10.0 → 0.10.1-alpha (PATCH).
Pokémon details "DV/EV" tab — full redesign into the style language
Brought the DV/EV tab (StatsTab.qml) up to the same language as the General tab: one white grouped panel of zebra-striped rows, connected "combo" controls, and no loose buttons or ⋮ overflow menus. Driven by Twilight ("turn this tab proper too… make it look good") across two quick asks (Future Shiny first, then "have DV and EV also be button combos").
What changed
- DV/EV is now a segmented [DV | EV] switch (the new inline SegSel component) instead of a flat toggle button. The active segment fills with the accent; the inactive one carries a hairline divider — same look as the General tab's groups, but text segments rather than icons.
- The per-kind ⋮ menu became a connected [Max | Re-Roll | Reset] icon combo (SegBtn, shared with the General tab): angle-double-up = max all, dice = re-roll, angle-double-down = reset to 0. The combo retargets to DVs or EVs by statKind (maxDVs/maxEVs, etc.), and the Max / Reset segments disable on isMax* / isMin*.
- Future Shiny lost both the checkbox and the ⋮ menu. It's now a segmented [Shiny | Non-Shiny] (SegSel) + a dice re-roll button. The selection is reactive: each segment's active is a binding to boxData.isShiny (not a checkable state), so dragging the DV bars into a (non-)shiny combo flips the selection on its own. Clicking a segment forces that state (makeShiny / unmakeShiny); the dice re-rolls a fresh combination keeping the current state (rollShiny / rollNonShiny).
- The stat sliders (DvStatGroup / EvStatGroup) are unchanged code, re-homed into a zebra-tinted panel row that sizes to the visible group's height.
The key pattern — active as a data binding, not a checkable toggle
A segmented radio whose selection must mirror live data (the shiny state can change from dragging a slider, not just clicking the segment) can't use checkable + ButtonGroup — a click would toggle checked and break the binding. SegSel instead is a plain (non-checkable) Button with property bool active bound to the data; onClicked performs the action, which changes the data, which re-drives active. One source of truth, no drift. (Documented in reference/ui-patterns.md.)
Verify / ship
tst_qml_screens green (pokemonDetails loads clean with the new components); full ctest 66/66; screenshot-verified (tmp/screenshots/editor/pokemon_editor_dvev.png — DV active, Non-Shiny active, combos rendering). VERSION 0.9.1-alpha → 0.10.0-alpha (MINOR — a whole-tab redesign, peer to the General tab's 0.9.0).
Follow-up — narrow the details header (tabs + species picker)
After the redesign narrowed the panes, the details header didn't fit: the General/DV-EV/Moves tabs stretched edge-to-edge (Material's default) with big gaps, and the GlancePane top bar (species / level / status) overflowed the slimmer right pane (status clipped). Fixes: DetailsPages.qml gives each TabButton a content width (implicitContentWidth + 28) so the tabs are snug-left instead of spread; GlancePane.qml narrows the species SelectSpecies from font*14 to font*10 (Gen 1 names are ≤10 chars) so species/level/status all fit. QML-only; tst_qml_screens green. VERSION 0.9.0-alpha → 0.9.1-alpha (PATCH).
Pokémon details "General" tab — full redesign + the type-combo off-by-one fix
A long, iterative redesign of the Pokémon details General tab (OverviewTab.qml), driven by Twilight's feedback across many rounds (with a couple of HTML mockups used to align on direction before building — those landed well, do that more). It ended on a real, long-standing bug: a TypesModel::valToIndex off-by-one that made every type combo display the wrong type.
The headline bug — type combos showed the wrong type (e.g. Charizard as Ghost/Fighting)
Twilight reported the Type "clear" (reset-to-default) "did nothing" — Charizard stayed Ghost/Fighting instead of Fire/Flying. Chased it as a data/correctTypes issue for a while; it was actually a display bug, exactly as Twilight suspected. A diagnostic test (tst_storage::repro_partyMonCorrectTypes, loading BaseSAV) proved the data was right all along: party slot 0 is species=180 (Charizard), type1=20 (Fire), type2=2 (Flying). The combos just rendered it wrong.
Root cause: TypesModel has a "-----" (no-type) placeholder at model row 0, so a type at store index i lives at model row i+1. But valToIndex(val) returned the store index i, not i+1 — so every Select-type combo selected the row one early (Fire→Ghost, Flying→Fighting). One-line fix: ret = i + 1. (My first correctTypes regression test was tautological — it compared the result to chz->toType1->ind, the same source the method reads — so it passed regardless; rewritten to assert the literal ids Fire=20/Flying=2.) Regression guards: tst_models::typesModel_* now asserts valToIndex(Fire) resolves to a row whose ind/name actually is Fire; tst_storage asserts the loaded mon's data. See reference/fix-patterns.md.
The redesign (all on the General tab)
- Connected action-button "combo" groups. The per-field actions are now small segmented controls — a bordered, rounded group of icon segments divided by a hairline (component SegBtn in OverviewTab.qml): Nickname [randomize | clear], Type [randomize | clear], OT pair [randomize | clear], Future Nature [randomize]. This replaced loose floating IconButtonSquares (which read as "a pile of buttons next to a
pile of fields") and the original ⋮ overflow menus. Trash-alt renders fine here (it only ever looked like a "sliver" when right-aligned buttons were clipped off the pane edge — the real lesson was layout, not the icon).
- Grouped-panel rows (Bag/Market feel). The rows live in one bordered panel as a cohesive list: zebra striping, muted right-aligned labels (the chunky grey "option #2" label boxes are gone here), rows touching, action groups aligned in one right column clear of the scrollbar. (Border later dropped at Twilight's request so the rows fill the panel.)
- Sprite pane narrowed to ~38% (PokemonDetails.qml width: parent.width * .62) so the editor gets more room. This exposed a latent GlancePane bug — statsData had no width, so the sprite anchored from the pane's left edge and overlapped the stat labels. Fixed by giving StatsGroup/StatsGroupInvalid a real implicitWidth (their grid's) and making them transparent, then sizing statsData to the visible group.
- Exp slider now sits between Lv. N / Lv. N+1 markers (progress between levels) and shows its value on hover (not just on drag).
- Catch rate → "Catch Difficulty" slider (General tab label only, by request): an Easy↔Hard slider (0–255) with the markers above the bar (like the Exp markers) and the byte on hover. Inverted (value = 255 - byte) so Easy (high catch rate = easier) sits on the left as asked.
- Type clear → species DB default via a new Q_INVOKABLE PokemonBox::correctTypes() — standalone so QML calls it cleanly (the multi-bool update() is unreliable from QML; see the in-code note). Type / Nature randomize roll via the combos (randomizeTypes/randomizeNature).
Verify / ship
- New regression tests green: tst_models (valToIndex row correctness), tst_pokemonbox (correctTypes → Fire/Flying, non-tautological), tst_storage (loaded-mon data). tst_qml_screens green (pokemonDetails clean). Full ctest before FF main.
- Driving the live app to confirm was blocked by Windows synthetic-input limits (clicks didn't reach the Qt buttons; keyboard shortcuts blocked by foreground rules) — verified instead via the headless screenshooter (renders the real save) + unit tests. Recent-file shortcut, for reference: Ctrl+Shift+0.
- VERSION 0.8.15-alpha → 0.9.0-alpha (MINOR — a whole-screen redesign + a real bug fix, not a small change).