From 960ec7fe4cc6de0fc2a050d7d69db21cf07ef4a2 Mon Sep 17 00:00:00 2001
From: andrew <andrew@arobeia.co.uk>
Date: Tue, 10 Nov 2026 20:04:44 +0000
Subject: [PATCH 2/2] gtk4: update the unit selection dialog live with all unit
 changes as they happen

Modify unit_select_dialog_update to make incremental change to the dialog rather than
recreating it, so that changes can be reflected without disturbing the tree open state
that the user currently has

See RM #2475
---
 client/gui-gtk-4.0/unitselect.c | 374 +++++++++++++++++++++++++++++---
 client/packhand.c               |   4 +-
 2 files changed, 340 insertions(+), 38 deletions(-)

diff --git a/client/gui-gtk-4.0/unitselect.c b/client/gui-gtk-4.0/unitselect.c
index a88594c40e..5efb8391e9 100644
--- a/client/gui-gtk-4.0/unitselect.c
+++ b/client/gui-gtk-4.0/unitselect.c
@@ -164,6 +164,11 @@ static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
                                    const struct unit *punit,
                                    bool transported, GtkTreeIter *it,
                                    GtkTreeIter *parent);
+static void usdlg_tab_update_units(struct unit_select_dialog *pdialog,
+                                   enum unit_select_location_mode loc,
+                                   enum unit_activity act,
+                                   const struct unit *punit,
+                                   bool transported, GtkTreeIter *it);
 
 static void usdlg_cmd_ready(GObject *object, gpointer data);
 static void usdlg_cmd_sentry(GObject *object, gpointer data);
@@ -567,8 +572,106 @@ static GtkTreeStore *usdlg_tab_store_new(void)
   return store;
 }
 
+struct retain_set {
+  int count;
+  int max;
+  int *ids;
+};
+
+static void retain_set_init(struct retain_set* retain_list, int n) {
+  retain_list->max = n;
+  retain_list->count = 0;
+  if (n > 0) {
+    retain_list->ids = fc_calloc(sizeof(int), n);
+  }
+}
+
+static void retain_set_free(struct retain_set* retain_list) {
+  if (retain_list->max > 0) {
+    free(retain_list->ids);
+  }
+}
+
+static void retain_set_add(struct retain_set* rs, int id) {
+  fc_assert(rs->count < rs->max);
+  rs->ids[rs->count++] = id;
+}
+
+static bool retain_set_contains(struct retain_set* retain_list, int id) {
+  int i;
+  for (i = 0; i < retain_list->count; ++i) {
+    if (retain_list->ids[i] == id) return true;
+  }
+  return false;
+}
+
 /*************************************************************************//**
-  Update on tab of the dialog.
+  Populate an existing tree containing units. Can be top level on the
+  first tab, or third level on the others.
+  it_unit points to the beginning of the current tree
+  it_unit is only valid if valid_unit is true
+  parent is the parent of the tree, NULL for the first tab.
+  Units not of utype_id will be left alone, not updated or removed.
+  (for first tab, this is called once for each unit type to update all types)
+*****************************************************************************/
+static void refresh_tree(struct unit_select_dialog *pdialog,
+                         enum unit_select_location_mode loc,
+                         GtkTreeStore* store,
+                         bool valid_unit,
+                         GtkTreeIter* it_unit,
+                         GtkTreeIter* parent,
+                         int utype_id,
+                         struct usdata* data,  // note can validly be null!
+                         int act,
+                         struct retain_set* retain_unit_rows)
+{
+  while (valid_unit) {
+    int v_utype;
+    int v_unit;
+    bool retain;
+
+    gtk_tree_model_get(GTK_TREE_MODEL(store), it_unit,
+                       3, &v_utype,
+                       4, &v_unit, -1);
+    if (v_utype == utype_id) {
+      retain = false;
+      if (data) {
+        unit_list_iterate(data->units[act], punit) {
+          if ((punit->id == v_unit) &&
+              (!unit_transported(punit))) {
+            usdlg_tab_update_units(pdialog, loc, act, punit, FALSE, it_unit);
+
+            retain = true;
+            retain_set_add(retain_unit_rows, punit->id);
+            break;
+          }
+        } unit_list_iterate_end;
+      }
+    } else {
+      retain = true; // for now, it's another type
+    }
+
+    if (retain) {
+      valid_unit = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), it_unit);
+    } else {
+      valid_unit = gtk_tree_store_remove(store, it_unit);
+    }
+  }
+
+  if (data) {
+    unit_list_iterate(data->units[act], punit) {
+      GtkTreeIter it_unit_add;
+
+      if (!retain_set_contains(retain_unit_rows, punit->id)) {
+        usdlg_tab_append_units(pdialog, loc, act, punit, FALSE,
+                               &it_unit_add, parent);
+      }
+    } unit_list_iterate_end;
+  }
+}
+
+/*************************************************************************//**
+  Update one tab of the dialog.
 *****************************************************************************/
 static bool usdlg_tab_update(struct unit_select_dialog *pdialog,
                              struct usdata_hash *ushash,
@@ -576,60 +679,188 @@ static bool usdlg_tab_update(struct unit_select_dialog *pdialog,
 {
   bool show = FALSE;
   GtkTreeStore *store;
+  bool valid_utype;
+  bool valid_act;
+  bool valid_unit;
+  int v_utype;
 
   fc_assert_ret_val(ushash, FALSE);
   fc_assert_ret_val(pdialog != NULL, FALSE);
 
   store = pdialog->tabs[loc].store;
 
-  /* clear current store. */
-  gtk_tree_store_clear(GTK_TREE_STORE(store));
-
   /* Iterate over all unit types. */
   if (loc == SELLOC_UNITS) {
-    /* Special case - show all units on this tile in their transports. */
+    /* Special case - show all units on this tile in their transports.
+       All units are stored in the hash with activity "IDLE", whatever their actual activity */
+    int act = ACTIVITY_IDLE;
+
+    /* Keep track of which rows in the tree still matter */
+    int old_unit_count = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), NULL);
+    struct retain_set retain_unit_rows;
+    retain_set_init(&retain_unit_rows, old_unit_count);
+
+
     unit_type_iterate(utype) {
       struct usdata *data;
 
       usdata_hash_lookup(ushash, utype_index(utype), &data);
 
+      /* remove all existing rows from the tree of this type that are not
+       * part of the new dataset, and update any that are.
+       */
+      GtkTreeIter it_unit;
+      valid_unit = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &it_unit);
+
+      refresh_tree(pdialog,
+                   loc,
+                   store,
+                   valid_unit,
+                   &it_unit,
+                   NULL,
+                   utype_index(utype),
+                   data,
+                   act,
+                   &retain_unit_rows);
+
+    } unit_type_iterate_end;
+    retain_set_free(&retain_unit_rows);
+    show = TRUE;
+  } else {
+    int old_utype_count = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), NULL);
+    struct retain_set retain_utype_rows;
+    retain_set_init(&retain_utype_rows, old_utype_count);
+
+    GtkTreeIter it_utype;
+    valid_utype = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &it_utype);
+    while (valid_utype) {
+      gtk_tree_model_get(GTK_TREE_MODEL(store), &it_utype,
+                         3, &v_utype, -1);
+
+      struct usdata *data;
+      usdata_hash_lookup(ushash, v_utype, &data);
+
       if (!data) {
-        continue;
-      }
+        valid_utype = gtk_tree_store_remove(store, &it_utype);
+      } else {
+        /* this unit type is to be populated */
+        show = TRUE;
+        retain_set_add(&retain_utype_rows, v_utype);
 
-      activity_type_iterate(act) {
-        if (unit_list_size(data->units[act]) == 0) {
-          continue;
+        int old_act_count = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), &it_utype);
+        struct retain_set retain_act_rows;
+        retain_set_init(&retain_act_rows, old_act_count);
+
+        GtkTreeIter it_action;
+        valid_act = gtk_tree_model_iter_children(GTK_TREE_MODEL(store), &it_action, &it_utype);
+        while (valid_act) {
+          int act;
+          gtk_tree_model_get(GTK_TREE_MODEL(store), &it_action,
+                             6, &act, -1);
+
+          if (unit_list_size(data->units[act]) == 0) {
+            valid_act = gtk_tree_store_remove(store, &it_action);
+          } else {
+            /* This activity is to be populated */
+            retain_set_add(&retain_act_rows, act);
+            
+            /* Keep track of which rows in the tree still matter */
+            int old_unit_count = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), &it_action);
+            struct retain_set retain_unit_rows;
+            retain_set_init(&retain_unit_rows, old_unit_count);
+
+            GtkTreeIter it_unit;
+            valid_unit = gtk_tree_model_iter_children(GTK_TREE_MODEL(store), &it_unit, &it_action);
+
+            refresh_tree(pdialog,
+                         loc,
+                         store,
+                         valid_unit,
+                         &it_unit,
+                         &it_action,
+                         v_utype,
+                         data,
+                         act,
+                         &retain_unit_rows);
+
+            valid_act = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &it_action);
+            retain_set_free(&retain_unit_rows);
+          } /* next activity */
         }
 
-        unit_list_iterate(data->units[act], punit) {
-          GtkTreeIter it_unit;
+        /* at this point we have correctly populated every activity that was already
+         * existing in the tree for the unit type. We now need to add the activities
+         * that were not present */
+
+        if (data) { /* if there are no units of this type, we are done */
+          int count = 0;
+          activity_type_iterate(act) {
+            if (unit_list_size(data->units[act]) == 0) {
+              continue;
+            }
+            count += unit_list_size(data->units[act]);
+
+            if (retain_set_contains(&retain_act_rows, act)) {
+              /* this activity is already done */
+              continue;
+            }
+
+            /* Display unit activities. */
+            usdlg_tab_append_activity(GTK_TREE_STORE(store), loc, data->utype,
+                                      act, unit_list_size(data->units[act]),
+                                      &it_action, &it_utype);
+
+            /* Level 3: Display all units with this activity
+             *          (and transported units in further level(s)). */
+            unit_list_iterate(data->units[act], punit) {
+              GtkTreeIter it_unit;
+              usdlg_tab_append_units(pdialog, loc, act, punit, FALSE,
+                                     &it_unit, &it_action);
+            } unit_list_iterate_end;
+
+            /* Expand to the activities. */
+            GtkTreePath *path;
+            path
+              = gtk_tree_model_get_path(GTK_TREE_MODEL(store),
+                                        &it_utype);
+            gtk_tree_view_expand_row(GTK_TREE_VIEW(pdialog->tabs[loc].view), path,
+                                     FALSE);
+            gtk_tree_path_free(path);
+
+            show = TRUE;
+          } activity_type_iterate_end;
+
+          /* Update sum of units with this type. */
+          gtk_tree_store_set(GTK_TREE_STORE(store), &it_utype, 2, count, -1);
+        }
+        retain_set_free(&retain_act_rows);
+        valid_utype = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &it_utype);
+      } /* next unit type */
+      
+    }
 
-          usdlg_tab_append_units(pdialog, loc, act, punit, FALSE,
-                                 &it_unit, NULL);
-        } unit_list_iterate_end;
+    /* at this point we have correctly populated every unit type that was already
+     * existing in the tree We now need to add the unit types
+     * that were not present */
 
-        /* Show this tab. */
-        show = TRUE;
-      } activity_type_iterate_end;
-    } unit_type_iterate_end;
-  } else {
     unit_type_iterate(utype) {
       struct usdata *data;
       bool first = TRUE;
-      GtkTreeIter it_utype;
       GtkTreePath *path;
       int count = 0;
 
+      if (retain_set_contains(&retain_utype_rows, utype_index(utype))) {
+        continue;
+      }
       usdata_hash_lookup(ushash, utype_index(utype), &data);
-
       if (!data) {
         continue;
       }
 
       activity_type_iterate(act) {
         GtkTreeIter it_act;
-
+        GtkTreeIter it_utype_add;
+        
         if (unit_list_size(data->units[act]) == 0) {
           continue;
         }
@@ -637,14 +868,14 @@ static bool usdlg_tab_update(struct unit_select_dialog *pdialog,
         /* Level 1: Display unit type. */
         if (first) {
           usdlg_tab_append_utype(GTK_TREE_STORE(store), loc, data->utype,
-                                 &it_utype);
+                                 &it_utype_add);
           first = FALSE;
         }
 
         /* Level 2: Display unit activities. */
         usdlg_tab_append_activity(GTK_TREE_STORE(store), loc, data->utype,
                                   act, unit_list_size(data->units[act]),
-                                  &it_act, &it_utype);
+                                  &it_act, &it_utype_add);
 
         /* Level 3: Display all units with this activity
          *          (and transported units in further level(s)). */
@@ -658,12 +889,13 @@ static bool usdlg_tab_update(struct unit_select_dialog *pdialog,
         count += unit_list_size(data->units[act]);
 
         /* Update sum of units with this type. */
-        gtk_tree_store_set(GTK_TREE_STORE(store), &it_utype, 2, count, -1);
+        gtk_tree_store_set(GTK_TREE_STORE(store), &it_utype_add, 2, count, -1);
+
 
         /* Expand to the activities. */
         path
           = gtk_tree_model_get_path(GTK_TREE_MODEL(pdialog->tabs[loc].store),
-                                    &it_utype);
+                                    &it_utype_add);
         gtk_tree_view_expand_row(GTK_TREE_VIEW(pdialog->tabs[loc].view), path,
                                  FALSE);
         gtk_tree_path_free(path);
@@ -671,7 +903,10 @@ static bool usdlg_tab_update(struct unit_select_dialog *pdialog,
         /* Show this tab. */
         show = TRUE;
       } activity_type_iterate_end;
+
     } unit_type_iterate_end;
+
+    retain_set_free(&retain_utype_rows);
   }
 
   return show;
@@ -828,14 +1063,15 @@ const char *usdlg_get_unit_descr(const struct unit *punit)
 }
 
 /*************************************************************************//**
-  Append units (recursively).
+  update units (recursively).
+  "it" is already valid, ready to be set.
+  May already have children
 *****************************************************************************/
-static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
+static void usdlg_tab_update_units(struct unit_select_dialog *pdialog,
                                    enum unit_select_location_mode loc,
                                    enum unit_activity act,
                                    const struct unit *punit,
-                                   bool transported, GtkTreeIter *it,
-                                   GtkTreeIter *parent)
+                                   bool transported, GtkTreeIter *it)
 {
   const char *text;
   GdkPixbuf *pix;
@@ -849,10 +1085,6 @@ static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
 
   store = pdialog->tabs[loc].store;
 
-
-  /* Add this item. */
-  gtk_tree_store_append(GTK_TREE_STORE(store), it, parent);
-
   /* Unit gfx */
   pix = usdlg_get_unit_image(punit);
 
@@ -879,13 +1111,58 @@ static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
                      -1);
   g_object_unref(pix);
 
+  /* Keep track of which rows in this part of the tree still matter */
+  int old_unit_count = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store), it);
+  struct retain_set retain_unit_rows;
+  retain_set_init(&retain_unit_rows, old_unit_count);
+
+  if (old_unit_count > 0) {
+
+    GtkTreeIter it_unit;
+    bool valid_unit = gtk_tree_model_iter_children(GTK_TREE_MODEL(store), &it_unit, it);
+    int v_utype;
+    int v_unit;
+    int v_act;
+    bool retain;
+
+    while (valid_unit) {
+      gtk_tree_model_get(GTK_TREE_MODEL(store), &it_unit,
+                         3, &v_utype,
+                         4, &v_unit,
+                         6, &v_act, -1);
+
+      retain = false;
+      if (get_transporter_occupancy(punit) > 0) {
+        unit_list_iterate(unit_transport_cargo(punit), pcargo) {
+          if (pcargo->id == v_unit) {
+            usdlg_tab_update_units(pdialog, loc, act, pcargo, FALSE, &it_unit);
+
+            retain = true;
+            retain_set_add(&retain_unit_rows, pcargo->id);
+            // break;
+          }
+        } unit_list_iterate_end;
+      }
+      if (retain) {
+        valid_unit = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &it_unit);
+      } else {
+        valid_unit = gtk_tree_store_remove(GTK_TREE_STORE(store), &it_unit);
+      }
+    }
+  }
+  
   if (get_transporter_occupancy(punit) > 0) {
     unit_list_iterate(unit_transport_cargo(punit), pcargo) {
       GtkTreeIter it_cargo;
 
-      usdlg_tab_append_units(pdialog, loc, act, pcargo, TRUE, &it_cargo, it);
+      if ((unit_transport_get(pcargo) == punit) &&
+          !retain_set_contains(&retain_unit_rows, pcargo->id)) {
+        usdlg_tab_append_units(pdialog, loc, act, pcargo, TRUE,
+                               &it_cargo, it);
+      }
     } unit_list_iterate_end;
   }
+  retain_set_free(&retain_unit_rows);
 
   if (!transported && unit_is_in_focus(punit)
       && get_num_units_in_focus() == 1) {
@@ -900,6 +1177,31 @@ static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
   }
 }
 
+
+
+/*************************************************************************//**
+  Append units (recursively).
+*****************************************************************************/
+static void usdlg_tab_append_units(struct unit_select_dialog *pdialog,
+                                   enum unit_select_location_mode loc,
+                                   enum unit_activity act,
+                                   const struct unit *punit,
+                                   bool transported, GtkTreeIter *it,
+                                   GtkTreeIter *parent)
+{
+  GtkTreeStore *store;
+
+  fc_assert_ret(pdialog != NULL);
+  fc_assert_ret(punit != NULL);
+
+  store = pdialog->tabs[loc].store;
+
+  /* Add this item. */
+  gtk_tree_store_append(GTK_TREE_STORE(store), it, parent);
+
+  usdlg_tab_update_units(pdialog, loc, act, punit, transported, it);
+}
+
 /*************************************************************************//**
   Callback for the ready button.
 *****************************************************************************/
diff --git a/client/packhand.c b/client/packhand.c
index 0bbc6eff0c..3b306c62e1 100644
--- a/client/packhand.c
+++ b/client/packhand.c
@@ -2112,12 +2112,12 @@ static bool handle_unit_packet_common(struct unit *packet_unit)
       || get_focus_unit_on_tile(unit_tile(punit))
       || (moved && get_focus_unit_on_tile(old_tile))) {
     update_unit_info_label(get_units_in_focus());
-    /* Update (an possible active) unit select dialog. */
-    unit_select_dialog_update();
   }
 
   if (repaint_unit) {
     refresh_unit_mapcanvas(punit, unit_tile(punit), TRUE, FALSE);
+    /* Update (an possible active) unit select dialog. */
+    unit_select_dialog_update();
   }
 
   if ((check_focus || get_num_units_in_focus() == 0)
-- 
2.47.3

