Skip to content

Commit 2a50cc5

Browse files
Goober5000claude
andcommitted
Keep event annotations attached to their node across tree mutations
Event annotations cache the tree item they point to as a handle (HTREEITEM in FRED, QTreeWidgetItem* in qtFRED). The handle was silently invalidated whenever the tree control deleted-and-recreated an item, so annotations got decoupled. Fix: notify when a node's handle changes. move_branch reports old->new, and free_node2 / the root-delete path report old->null (deleted). The event tree remaps the annotation's handle to follow a move, or clears it on delete (the path is left intact, so a cleared annotation is dropped at save but survives a Cancel). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d5093cf commit 2a50cc5

9 files changed

Lines changed: 81 additions & 2 deletions

File tree

fred2/eventeditor.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,21 @@ int event_annotation_lookup(HTREEITEM handle)
16741674
return -1;
16751675
}
16761676

1677+
// The tree recreated (moved) or deleted the item for a node. Re-point any
1678+
// annotation on the old handle to the new one so it follows the node; a null
1679+
// new_handle means the node was deleted, which clears the handle. The path is
1680+
// left intact, so an annotation cleared this way is dropped at save (its path
1681+
// can no longer be rebuilt) but survives a Cancel (the path still resolves).
1682+
void event_sexp_tree::on_node_handle_changed(HTREEITEM old_handle, HTREEITEM new_handle)
1683+
{
1684+
if (!old_handle)
1685+
return;
1686+
1687+
int i = event_annotation_lookup(old_handle);
1688+
if (i >= 0)
1689+
Event_annotations[i].handle = new_handle;
1690+
}
1691+
16771692
void event_annotation_swap_image(event_sexp_tree *tree, HTREEITEM handle, int annotation_index)
16781693
{
16791694
event_annotation_swap_image(tree, handle, Event_annotations[annotation_index]);

fred2/eventeditor.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class event_sexp_tree : public sexp_tree
3131
virtual void PreSubclassWindow();
3232
virtual void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
3333

34+
// keep event annotations attached to their node when the tree recreates or
35+
// deletes its handle (new_handle == nullptr means the node was deleted)
36+
void on_node_handle_changed(HTREEITEM old_handle, HTREEITEM new_handle) override;
37+
3438
CStringA m_tooltiptextA;
3539
CStringW m_tooltiptextW;
3640

fred2/sexp_tree.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,11 @@ void sexp_tree::free_node2(int node)
479479
Assert(tree_nodes[node].type != SEXPT_UNUSED);
480480
Assert(total_nodes > 0);
481481
*modified = 1;
482+
483+
// the node is being deleted; let subclasses drop anything attached to its handle
484+
if (tree_nodes[node].handle)
485+
on_node_handle_changed(tree_nodes[node].handle, nullptr);
486+
482487
tree_nodes[node].type = SEXPT_UNUSED;
483488
total_nodes--;
484489
if (tree_nodes[node].child != -1)
@@ -2640,6 +2645,9 @@ void sexp_tree::NodeDelete()
26402645

26412646
Assert(theNode >= 0);
26422647
free_node2(theNode);
2648+
// the event-root item is not a tree_nodes entry, so free_node2 above does
2649+
// not cover an annotation attached to the event root itself
2650+
on_node_handle_changed(item_handle, nullptr);
26432651
DeleteItem(item_handle);
26442652
*modified = 1;
26452653
return;
@@ -4668,6 +4676,10 @@ HTREEITEM sexp_tree::move_branch(HTREEITEM source, HTREEITEM parent, HTREEITEM a
46684676
h = insert(GetItemText(source), image1, image2, parent, after);
46694677
}
46704678

4679+
// the item was recreated with a new handle; let subclasses follow it
4680+
// (covers both tree_nodes entries and the event-root item, which is not one)
4681+
on_node_handle_changed(source, h);
4682+
46714683
SetItemData(h, GetItemData(source));
46724684
child = GetChildItem(source);
46734685
while (child) {

fred2/sexp_tree.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@ class sexp_tree : public CTreeCtrl
367367
virtual void NodeReplacePaste();
368368
virtual void NodeAddPaste();
369369

370+
// Notifies that the tree item for a node changed handle (moved/recreated), or
371+
// was deleted (new_handle == nullptr). The event tree overrides this to keep
372+
// event annotations attached to their node. Default: no-op.
373+
virtual void on_node_handle_changed(HTREEITEM /*old_handle*/, HTREEITEM /*new_handle*/) {}
374+
370375
void update_item(HTREEITEM handle);
371376

372377
int load_branch(int index, int parent);

qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,12 @@ void MissionEventsDialogModel::reorderByRootFormulaOrder(const SCP_vector<int>&
609609
// Keep selection reasonable (select the first event after reorder)
610610
setCurrentlySelectedEvent(m_events.empty() ? -1 : 0);
611611

612-
// Rebuild applied annotations against new handles/order if needed
613-
initializeEventAnnotations();
612+
// A root reorder moves top-level items via take/insert, which preserves their
613+
// handles (see sexp_tree::move_root), so the cached annotation handles are still
614+
// valid and still point at the correct nodes. Do NOT re-resolve handles from the
615+
// stored paths here: the paths carry the pre-reorder event index, so re-resolving
616+
// would re-point annotations at the wrong event. applyAnnotations() rebuilds each
617+
// path from its live handle at save time.
614618

615619
set_modified();
616620
}
@@ -1138,6 +1142,20 @@ void MissionEventsDialogModel::setNodeBgColor(IEventTreeOps::Handle h, int r, in
11381142
set_modified();
11391143
}
11401144

1145+
// The tree recreated (moved) or deleted the item for a node. Re-point any
1146+
// annotation on the old handle to the new one so it follows the node; a null
1147+
// new_handle means the node was deleted, which clears the cached handle so
1148+
// applyAnnotations() resolves it from the path (and drops it if the node is gone).
1149+
void MissionEventsDialogModel::onNodeHandleChanged(IEventTreeOps::Handle old_handle, IEventTreeOps::Handle new_handle)
1150+
{
1151+
if (!old_handle)
1152+
return;
1153+
1154+
for (auto& ea : m_event_annotations)
1155+
if (ea.handle == old_handle)
1156+
ea.handle = new_handle;
1157+
}
1158+
11411159
void MissionEventsDialogModel::createMessage()
11421160
{
11431161
MMessage msg;

qtfred/src/mission/dialogs/MissionEventsDialogModel.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ class MissionEventsDialogModel : public AbstractDialogModel {
142142
void setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note);
143143
void setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color);
144144

145+
// Keep annotations attached to their node when the tree recreates (move) or
146+
// deletes (new_handle == nullptr) the item's handle.
147+
void onNodeHandleChanged(IEventTreeOps::Handle old_handle, IEventTreeOps::Handle new_handle);
148+
145149
// Message Management
146150
void createMessage();
147151
void insertMessage();

qtfred/src/ui/dialogs/MissionEventsDialog.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ void MissionEventsDialog::initEventWidgets() {
245245
_model->setNodeBgColor(h, c.red(), c.green(), c.blue(), c.isValid());
246246
});
247247

248+
connect(ui->eventTree, &sexp_tree::nodeHandleChanged, this, [this](void* old_h, void* new_h) {
249+
_model->onNodeHandleChanged(old_h, new_h);
250+
});
251+
248252
connect(ui->eventTree, &sexp_tree::rootOrderChanged, this, [this] {
249253
SCP_vector<int> order;
250254
order.reserve(ui->eventTree->topLevelItemCount());

qtfred/src/ui/widgets/sexp_tree.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,11 @@ void sexp_tree::free_node2(int node) {
673673
Assert(tree_nodes[node].type != SEXPT_UNUSED);
674674
Assert(total_nodes > 0);
675675
modified();
676+
677+
// the node is being deleted; let the model drop anything attached to its handle
678+
if (tree_nodes[node].handle)
679+
nodeHandleChanged(tree_nodes[node].handle, nullptr);
680+
676681
tree_nodes[node].type = SEXPT_UNUSED;
677682
total_nodes--;
678683
if (tree_nodes[node].child != -1) {
@@ -2739,6 +2744,9 @@ QTreeWidgetItem* sexp_tree::move_branch(QTreeWidgetItem* source, QTreeWidgetItem
27392744
h->setData(0, BgColorRole, source->data(0, BgColorRole));
27402745
applyVisuals(h);
27412746

2747+
// the item was recreated with a new handle; let the model follow it
2748+
nodeHandleChanged(source, h);
2749+
27422750
// Move children safely
27432751
while (source->childCount() > 0) {
27442752
auto* child = source->child(0);
@@ -7297,6 +7305,10 @@ void sexp_tree::deleteActionHandler()
72977305
free_node2(formulaNode);
72987306
}
72997307

7308+
// the event-root item is not a tree_nodes entry, so free_node2 above does
7309+
// not cover an annotation attached to the event root itself
7310+
nodeHandleChanged(item, nullptr);
7311+
73007312
// Remove the UI item and reset selection/index
73017313
delete item;
73027314
setCurrentItemIndex(-1);

qtfred/src/ui/widgets/sexp_tree.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ class sexp_tree: public QTreeWidget {
410410
void nodeAnnotationChanged(void* handle, const QString& note);
411411
void nodeBgColorChanged(void* handle, const QColor& color);
412412

413+
// Emitted when a node's tree item changed handle (moved/recreated), or was
414+
// deleted (new_handle == nullptr). Lets the model keep event annotations
415+
// attached to their node across tree mutations.
416+
void nodeHandleChanged(void* old_handle, void* new_handle);
417+
413418
// Generated message map functions
414419
protected:
415420
void keyPressEvent(QKeyEvent* e) override;

0 commit comments

Comments
 (0)