Skip to content

Fix deck editor tabs vanishing, deck name bleed, and the commander tab crashFix/deck editor state - #11896

Open
leriomaggio wants to merge 11 commits into
Card-Forge:masterfrom
leriomaggio:fix/deck-editor-state
Open

leriomaggio wants to merge 11 commits into
Card-Forge:masterfrom
leriomaggio:fix/deck-editor-state

Conversation

@leriomaggio

@leriomaggio leriomaggio commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Three related defects in the desktop deck editor. They share a
root cause: state that belongs to the layout or to one editor is held in objects
shared by every editor, with no handshake about owners of the current state.

Fixes #11892
Fixes #11893
Fixes #11894

#11892, tabs vanish permanently

Limited and quest editors take the deck lists, Deck Gen and Probabilities out of
the layout while they are open. Any layout save during that window wrote their
absence to editor.xml, and every later loadLayout then produced a Deck Editor
without them.

Two commits:

  • 5730c22 moves the bookkeeping out of the controllers into SHiddenTabs. Ten
    controllers each kept their own DragCell fields, which cannot work when two
    editors are constructed for the same screen and the one that restores is not the
    one that hid. hide is now idempotent, and because it records each cell's
    bounds, a cell torn down by the hide is restored into whichever cell grew to
    cover that space. Net 214 lines removed. ACEditorBase.removeTab is deprecated,
    and only the unreachable CEditorCommander still calls it. This method can be removed as soon as Name the Home and lobby axes "Play Mode" and "Game Format", consolidate the deck editor screens #11885 is merged.
  • c21ce79 is the actual fix. SLayoutIO.save now asks SHiddenTabs what is
    hidden and writes each doc into the cell it will be restored to, so the file
    describes where tabs belong rather than where they momentarily are not.

SHiddenTabs lives in forge.gui.framework rather than under the deck editor,
since it is layout state and SLayoutIO needs it, with no direct dependency with
forge.screens.deckeditor.

#11893, deck name bleed

VCurrentDeck is one panel shared by every editor, and
DeckController.updateCaptions is the only thing that writes its title box.
CEditorConstructed.update() reaches it via refreshModel(), CEditorLimited
did not, so the draft and sealed editors showed the previous editor's deck name.
SEditorIO.saveDeck takes the name to save under from that box, and
CEditorLimited disables it, so the wrong name could not be corrected first.

4d8e096 adds the missing call. Only CEditorLimited needs it:
CEditorQuestCardShop, CEditorTokenViewer and CEditorNetworkDraft return null
from getDeckController().

#11894, commander tab crash

DragCell.setSelected calls doc.populate() inside its loop over allDocs.
VCommanderDecks.populate() reaches ensureScreenActive(DECK_EDITOR_CONSTRUCTED),
which on any other screen really switches, so loadLayout clears the list being
iterated and the iteration throws.

  • ef3cf1f fixes the crash. VAllDecks.populate already guarded its own call for
    this reason; the guard now lives inside the shared editPreferredDeck, so all
    five deck list docs are covered by one check instead of five copies.
  • 02154048 closes the visibility gap behind it. CEditorQuest,
    CEditorQuestCardShop, CEditorQuestLimited, CEditorTokenViewer,
    CEditorVariant and CEditorWinstonProcess hid only VAllDecks and
    VDeckgen, leaving the four commander lists on screen. The limited editors have
    always hidden all six. The gap dates from 29263d2.

Note for anyone already affected

This stops the loss but cannot undo it. A layout already saved without the tabs
needs Preferences, Reset Deck Editor Layout, once.

leriomaggio and others added 10 commits September 13, 2026 06:45
Limited and quest editors take the deck list, deck generator and probabilities
tabs out of the layout while they are open. Each controller kept its own
DragCell fields to put them back, but that record is per-controller while the
layout is global, so the tabs could be lost permanently:

- update() runs whenever an editor becomes current, and a second editor can be
  constructed for a screen before the first has restored. The second removeTab
  found the docs already gone and cached null, so resetUIChanges re-added
  nothing.
- removeTab tears down the parent cell when the removal empties it, so the
  cached cell may no longer be in FView. Re-adding to it put the tab in a cell
  nothing draws and SLayoutIO does not save.

SHiddenTabs now keeps that record for every editor. Hiding an already hidden
doc does nothing, and when the original cell has been torn down, restore picks
whichever cell grew to cover the space it occupied.
It lives in forge.gui.framework alongside DragCell and SLayoutIO,
because it is layout state rather than deck editor state.

The ten live controllers lose their DragCell fields. removeTab is deprecated;
only the unreachable CEditorCommander still calls it (dropped in Card-Forge#11885).
Editors take some tabs out of the layout while they are open. Any layout save
during that window wrote them out of the file permanently, because save
serialises the live cells and the hidden docs are not in them.

The save is easy to trigger without meaning to.
SRearrangingUtil.endRearrange runs on a plain tab click,
with no drag, and saves whenever the click changed which tab is selected.

Tests: four writes in twenty seconds while clicking
between the Deck, Statistics and Probabilities tabs during a draft, each one
recording the selection change and each one dropping the six hidden docs.
SResizingUtil.endResize saves unconditionally on any release over a cell's 5px
border strip, so a stray click in a gutter does it too.

save() now asks SHiddenTabs which docs are hidden and writes each into the cell
it will be restored to, so the file describes where tabs belong.
VCurrentDeck is one panel shared by every editor, and DeckController's
updateCaptions is the only thing that writes its title box. CEditorConstructed
ends update() with refreshModel, which reaches updateCaptions; CEditorLimited
did not, so switching into the draft or sealed editor left the previous
editor's deck name in the box.

SEditorIO.saveDeck takes the name to save under straight
from that box, and CEditorLimited disables it, so the wrong name could not even
be corrected before saving.

Reproduced by opening the draft deck editor, switching to the Deck Editor and
switching back.
Every deck list doc calls VAllDecks.editPreferredDeck from populate(), which
routes through DeckManager.editDeck to DECK_EDITOR_CONSTRUCTED. On any other
screen that switches screens from inside populate(), while
DragCell.setSelected is still iterating that cell's docs: loadLayout clears the
list underneath it and the iteration throws ConcurrentModificationException.

Reproduced by opening the Token Previewer and clicking its Commander tab, which
threw and then dumped the user on the Deck Editor.

VAllDecks already guarded its own call for this reason. The guard now lives in
editPreferredDeck itself, so the commander, oathbreaker, brawl and tiny leaders
lists are covered too, and the caller-side check is no longer needed.
CEditorQuest, CEditorQuestCardShop, CEditorQuestLimited, CEditorTokenViewer,
CEditorVariant and CEditorWinstonProcess hid only the constructed deck list and
the deck generator, leaving Commander, Oathbreaker, Brawl and Tiny Leaders on
screen. The four limited editors have always hidden all six.

It was invisible while layouts were losing the docs anyway; now
that they survive a save, a Commander Decks tab shows up in places like the
Token Previewer and the Spell Shop. This commit fixes that.
@leriomaggio leriomaggio added GUI Deck Editor Anything related to the mobile or desktop deck editors Desktop labels Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Deck Editor Anything related to the mobile or desktop deck editors Desktop GUI

Projects

None yet

1 participant