ui/gtk/list.c

changeset 1246
43a7874a9d0e
parent 1245
4163776eef49
child 1247
070d0472e6af
--- a/ui/gtk/list.c	Thu Sep 10 17:36:06 2026 +0200
+++ b/ui/gtk/list.c	Fri Sep 11 17:27:12 2026 +0200
@@ -3046,6 +3046,10 @@
     free(v);
 }
 
+static void ui_destroy_listbox2_row(UiListBox2Row *row) {
+    cxListFree(row->children);
+}
+
 UIWIDGET ui_sourcelist2_create(UiObject *obj, UiSourceList2Args *args) {
 #ifdef UI_GTK3
     GtkWidget *listbox = g_object_new(ui_sidebar_list_box_get_type(), NULL);
@@ -3071,11 +3075,10 @@
     UiListBox2 *uilistbox = malloc(sizeof(UiListBox2));
     uilistbox->obj = obj;
     uilistbox->listbox = GTK_LIST_BOX(listbox);
-    uilistbox->rows = cxArrayListCreate(NULL, CX_STORE_POINTERS, 64);
+    uilistbox->rows = cxArrayListCreate(NULL, sizeof(UiListBox2Row), 32);
+    cxSetDestructor(uilistbox->rows, ui_destroy_listbox2_row);
     uilistbox->getvalue = args->getvalue;
     uilistbox->getvaluedata = args->getvaluedata;
-    uilistbox->getdepth = args->getdepth;
-    uilistbox->getdepthdata = args->getdepthdata;
     uilistbox->onactivate = args->onactivate;
     uilistbox->onactivatedata = args->onactivatedata;
     uilistbox->onactivate_action = args->onactivate_action ? strdup(args->onactivate_action) : NULL;
@@ -3111,7 +3114,7 @@
                 G_CALLBACK(ui_destroy_sourcelist2),
                 uilistbox);
     
-    if(args->onactivate) {
+    if(args->onactivate || args->onactivate_action) {
         g_signal_connect(
                 listbox,
                 "row-activated",
@@ -3122,16 +3125,125 @@
     return scroll_area;
 }
 
-
-static int listbox2_update_item(UiListBox2 *listbox, GtkWidget *row, UiList *list, void *elm, int index) {
-    UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL };
-    if(listbox->getvalue) {
-        listbox->getvalue(list,elm, index, &item, listbox->getvaluedata);
+static void listbox2_button_clicked(GtkWidget *button, UiListBox2Row *data) {
+    UiEvent event;
+    event.obj = data->listbox->obj;
+    event.window = event.obj->window;
+    event.document = event.obj->ctx->document;
+    event.eventdata = NULL;
+    event.eventdatatype = UI_EVENT_DATA_NULL;
+    event.intval = data->index;
+    event.set = ui_get_setop();
+    
+    if(data->listbox->onbuttonclick) {
+        data->listbox->onbuttonclick(&event, data->listbox->onbuttonclickdata);
     }
     
-    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
+    if(data->listbox->onbuttonclick_action) {
+        uic_action_callback(&event, data->listbox->onbuttonclick_action);
+    }
+}
+
+
+static void listbox2_node_children_update_visibility(UiListBox2Row *rowdata, int visible) {
+    CxIterator i = cxListIterator(rowdata->children);
+    int child_visible = visible ? rowdata->expanded : 0;
+    cx_foreach(UiListBox2Row *, child, i) {
+        gtk_widget_set_visible(child->row, child_visible);
+        listbox2_node_children_update_visibility(child, child_visible);
+    }
+}
+
+#if GTK_CHECK_VERSION(4, 0, 0)
+
+void listbox2_node_expand(
+        GtkGestureClick *self,
+        gint n_press,
+        gdouble x,
+        gdouble y,
+        UiListBox2Row *rowdata)
+{
+    rowdata->expanded = !rowdata->expanded;
+    if(rowdata->expanded) {
+        gtk_image_set_from_icon_name(GTK_IMAGE(rowdata->expander), "pan-down-symbolic");
+    } else {
+        gtk_image_set_from_icon_name(GTK_IMAGE(rowdata->expander), "pan-end-symbolic");
+    }
+    listbox2_node_children_update_visibility(rowdata, rowdata->expanded);
+}
+
+#endif
+#if GTK_MAJOR_VERSION == 3
+
+#endif
+
+#define LISTBOX2_ITEM_HBOX_SPACING 10
+static void listbox2_update_item(UiListBox2Row *rowdata, GtkWidget *row, UiSourceListItem *item, int identation) { 
+    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, LISTBOX2_ITEM_HBOX_SPACING);
     
-    return 0;
+    rowdata->expanded = 1;
+    GtkWidget *expander = gtk_image_new_from_icon_name("pan-down-symbolic");
+    int exp_width = 0;
+#if GTK_CHECK_VERSION(4, 0, 0)
+    gtk_widget_measure(expander,
+            GTK_ORIENTATION_HORIZONTAL,
+            -1,
+            &exp_width,
+            NULL,
+            NULL,
+            NULL);
+    
+    GtkGesture *click = gtk_gesture_click_new();
+    gtk_widget_add_controller(expander, GTK_EVENT_CONTROLLER(click));
+    g_signal_connect(click, "released", G_CALLBACK(listbox2_node_expand), rowdata);
+#else
+    int exp_height;
+    gtk_widget_get_preferred_width(expander, &exp_width, &exp_height);
+#endif
+    gtk_widget_set_margin_start(hbox, identation * (exp_width+LISTBOX2_ITEM_HBOX_SPACING));
+    
+    WIDGET_NO_SHOW_ALL(expander);
+    gtk_widget_set_visible(expander, FALSE);
+    // normal items have the expander icon in the front
+    if(!item->is_header) {
+        BOX_ADD(hbox, expander);
+    }
+    rowdata->expander = expander;
+    
+    if(item->icon) {
+        GtkWidget *icon = ICON_IMAGE(item->icon);
+        BOX_ADD(hbox, icon);
+    }
+    GtkWidget *label = gtk_label_new(item->label);
+    gtk_widget_set_halign(label, GTK_ALIGN_START);
+    
+    BOX_ADD_EXPAND(hbox, label);
+    
+    // button
+    if(item->button_label || item->button_icon) {
+        GtkWidget *button = gtk_button_new();
+        gtk_button_set_label(GTK_BUTTON(button), item->button_label);
+        ui_button_set_icon_name(button, item->button_icon);
+        WIDGET_ADD_CSS_CLASS(button, "flat");
+        BOX_ADD(hbox, button);
+        g_signal_connect(
+                button,
+                "clicked",
+                G_CALLBACK(listbox2_button_clicked),
+                rowdata);
+        gtk_widget_set_visible(button, FALSE);
+        WIDGET_NO_SHOW_ALL(button);
+    }
+    
+    // headings have the expander at the end
+    if(item->is_header) {
+        BOX_ADD(hbox, expander);
+        WIDGET_ADD_CSS_CLASS(label, "ui-listbox-header-row");
+        rowdata->heading = TRUE;
+    }
+    
+    LISTBOX_ROW_SET_CHILD(row, hbox);
+    g_object_set_data(G_OBJECT(row), "ui-listbox-row-data", rowdata);
 }
 
 static void listbox2_update_all(UiListBox2 *listbox) {
@@ -3145,29 +3257,104 @@
         return;
     }
     
+    UiListBox2Row **stack;
+    size_t stack_size = 16;
+    stack = calloc(stack_size, sizeof(UiListBox2Row*));
     
     int index = 0;
+    UiBool heading = FALSE;
     void *elm = list->first(list);
     while(elm) {
+        UiSourceListItem item = { 0 };
+        if(listbox->getvalue) {
+            listbox->getvalue(list, elm, index, &item, listbox->getvaluedata);
+        }
+        
         GtkWidget *row = gtk_list_box_row_new();
-        cxListAdd(listbox->rows, row);
         gtk_list_box_append(listbox->listbox, row);
-        int node_depth = listbox2_update_item(listbox, row, list, elm, index);
+        UiListBox2Row row_data = {0};
+        row_data.listbox = listbox;
+        row_data.row = row;
+        row_data.index = index;
+        row_data.depth = item.depth;
+        cxListAdd(listbox->rows, &row_data);
+        UiListBox2Row *row_data_ptr = cxListLast(listbox->rows);
+        listbox2_update_item(row_data_ptr, row, &item, item.depth - (item.depth > 0 ? heading : 0));
+        
+        if(item.depth >= stack_size) {
+            stack_size += 8;
+            stack = realloc(stack, stack_size * sizeof(UiListBox2Row*));
+        }
+        stack[item.depth] = row_data_ptr;
+        
+        if(item.depth > 0) { 
+            // add row to parent
+            UiListBox2Row *parent = item.depth-1 < stack_size ? stack[item.depth-1] : NULL;
+            if(parent) {
+                if(!parent->children) {
+                    parent->children = cxArrayListCreate(NULL, CX_STORE_POINTERS, 8);
+                    gtk_widget_set_visible(parent->expander, TRUE);
+                    WIDGET_ENABLE_SHOW_ALL(parent->expander);
+                }
+                cxListAdd(parent->children, row_data_ptr);
+            } else {
+                fprintf(stderr, "Error: sourcelist2: illegal depth: no parent found\n");
+            }
+        } else {
+            heading = item.is_header != 0;
+        }
+        
+        free(item.label);
+        free(item.icon);
+        free(item.badge);
+        free(item.button_label);
+        free(item.button_icon);
         
         elm = list->next(list);
         index++;
     }
+    
+    cxListShrink(listbox->rows);
+    free(stack);
 }
 
 void ui_listbox2_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data) {
+    UiListBox2Row *rowdata = g_object_get_data(G_OBJECT(row), "ui-listbox-row-data");
+    if(!rowdata) {
+        return;
+    }
+    UiListBox2 *listbox = rowdata->listbox;
+    UiVar *var = listbox->var;
+    if(!var) {
+        return; // shouldn't happen
+    }
     
+    UiList *list = var->value;
+    void *elm = list->get(list, rowdata->index);
+    
+    UiEvent event;
+    event.obj = listbox->obj;
+    event.window = event.obj->window;
+    event.document = event.obj->ctx->document;
+    event.eventdatatype = UI_EVENT_DATA_LIST_ELM;
+    event.eventdata = elm;
+    event.intval = rowdata->index;
+    event.set = ui_get_setop();
+    
+    if(listbox->onactivate) {
+        listbox->onactivate(&event, listbox->onactivatedata);
+    }
+    if(listbox->onactivate_action) {
+        uic_action_callback(&event, listbox->onactivate_action);
+    }
 }
 
 
 void ui_listbox2_update(UiList *list, int i) {
     UiListBox2 *listbox = list->obj;
     if(i >= 0) {
-        // TODO
+        // TODO: update single row
+        listbox2_update_all(listbox);
     } else {
         listbox2_update_all(listbox);
     }

mercurial