Module: Undo (nui_undo), Selection (nui_selection), Clipboard (nui_clipboard)
Das Original modelliert einen rückgängig machbaren Vorgang als selbstinvertierende Action: Do() vertauscht intern gespeicherten Zustand, sodass ein zweiter Aufruf von Do() automatisch wieder rückgängig macht. Kompakt, aber fragil — ActionApplyControl::Do etwa rekonstruiert von Hand rund 15 einzelne Felder, und das Muster "temporäre Actions sammeln → IsActionDone prüfen → hinzufügen-oder-verwerfen" ist im Original an rund zehn Stellen dupliziert.
NUIStudio verwendet stattdessen explizite, getrennte Apply()/Undo()/Redo()-Methoden.
class ICommand
{
public:
virtual void Apply() = 0;
virtual void Undo() = 0;
virtual void Redo() { Apply(); } // Standard: erneut anwenden
virtual std::string Describe() const = 0;
};
Mehrere Commands lassen sich zu einem Undo-Schritt bündeln — genau wie im Original ("ein Undo-Schritt = eine Gruppe von Aktionen"), hier aber als eigenständige, wiederverwendbare Klasse statt dupliziertem Boilerplate:
class CommandGroup final : public ICommand
{
public:
void Add(std::unique_ptr<ICommand> command);
// Apply()/Redo(): in Einfügereihenfolge
// Undo(): in umgekehrter Reihenfolge
};
Nui/Undo/Commands.h)| Command | Zweck |
|---|---|
SetPropertyCommand |
Eine einzelne Eigenschaft eines Controls ändern |
MoveControlsCommand |
Position/Größe eines oder mehrerer Controls ändern |
AddControlCommand |
Ein neues Control einfügen |
DeleteControlCommand |
Ein Control entfernen |
Im Original verhält sich Verschieben anders als Größenändern: Verschieben mutiert das Modell bei jedem Maus-Tick sofort, Größenändern zeigt dagegen nur eine Vorschau bis zum Loslassen. EditTransaction vereinheitlicht das zu einem einzigen, konsistenten Muster:
EditTransaction tx(undoStack, "Control verschieben");
// Bei Mausklick: Command erzeugen, EINMAL Apply() aufrufen, tracken
auto cmd = std::make_unique<MoveControlsCommand>(...);
cmd->Apply();
tx.Track(std::move(cmd));
// Bei jedem weiteren Maus-Tick während des Ziehens:
// direkt auf dem lebenden Modell mutieren (sofortiges visuelles Feedback),
// OHNE erneut auf den Undo-Stack zu drücken
// Bei Loslassen:
tx.Commit(); // genau EIN Undo-Eintrag für die gesamte Ziehbewegung
Wird die Transaktion verworfen, ohne Commit() aufzurufen (z. B. weil der Vorgang abgebrochen wurde), macht der Destruktor automatisch Cancel() — alle getrackten Commands werden rückgängig gemacht, und es landet nichts auf dem Undo-Stack. Das ersetzt die rund zehn duplizierten Boilerplate-Stellen aus dem Original durch einen einzigen, wiederverwendbaren Mechanismus.
UndoStack::Push(...) legt einen neuen Undo-Eintrag ab und kappt dabei den Redo-Zweig — ein neuer bearbeiteter Zustand nach einem Undo macht die zuvor "redo-baren" Schritte ungültig, wie in jedem Standard-Undo-System.
struct NodeRef { bool isRoot; size_t index; }; // Wurzel oder Index in document.children
class SelectionModel
{
public:
core::Signal<SelectionChangedEvent>& Changed();
void Select(std::vector<NodeRef> nodes);
void Toggle(NodeRef node);
bool IsSelected(NodeRef node) const;
void Clear();
};
Canvas, Hierarchie und Inspector abonnieren unabhängig voneinander das Changed()-Signal — keiner der drei kennt die anderen beiden direkt. Das ersetzt die globale MainFrame()/ChildFrame()-Makro-Kopplung des Originals, die dessen Logik praktisch untestbar machte.
Kopieren/Einfügen serialisiert die ausgewählte Teilstruktur durch denselben Nui/Serializer, den auch das Speichern verwendet — dadurch überleben auch unbekannte Eigenschaften eines kopierten Controls den Zwischenablage-Vorgang, was die Ad-hoc-Kopierlogik des Originals nicht garantieren konnte.