implement sourcelist2 widget (GTK)

Fri, 11 Sep 2026 17:27:12 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 11 Sep 2026 17:27:12 +0200
changeset 1246
43a7874a9d0e
parent 1245
4163776eef49
child 1247
070d0472e6af

implement sourcelist2 widget (GTK)

application/main.c file | annotate | diff | comparison | revisions
ui/gtk/list.c file | annotate | diff | comparison | revisions
ui/gtk/list.h file | annotate | diff | comparison | revisions
ui/gtk/toolkit.h file | annotate | diff | comparison | revisions
ui/motif/list.h file | annotate | diff | comparison | revisions
ui/ui/list.h file | annotate | diff | comparison | revisions
--- a/application/main.c	Thu Sep 10 17:36:06 2026 +0200
+++ b/application/main.c	Fri Sep 11 17:27:12 2026 +0200
@@ -222,6 +222,7 @@
 typedef struct TreeNode {
     char *name;
     int depth;
+    int heading;
 } TreeNode;
 
 int threadfunc(void *data) {
@@ -399,28 +400,45 @@
     TreeNode *root = malloc(sizeof(TreeNode));
     root->name = "Root";
     root->depth = 0;
+    root->heading = 0;
     ui_list_append(doc->tree, root);
     
     TreeNode *node_a = malloc(sizeof(TreeNode));
     node_a->name = "Folder";
     node_a->depth = 1;
+    node_a->heading = 0;
     ui_list_append(doc->tree, node_a);
     
     TreeNode *node_a0 = malloc(sizeof(TreeNode));
     node_a0->name = "Leaf 1";
     node_a0->depth = 2;
+    node_a0->heading = 0;
     ui_list_append(doc->tree, node_a0);
     
     TreeNode *node_a1 = malloc(sizeof(TreeNode));
     node_a1->name = "Leaf 2";
     node_a1->depth = 2;
+    node_a1->heading = 0;
     ui_list_append(doc->tree, node_a1);
     
     TreeNode *node_1 = malloc(sizeof(TreeNode));
     node_1->name = "Leaf 3";
     node_1->depth = 1;
+    node_1->heading = 0;
     ui_list_append(doc->tree, node_1);
     
+    TreeNode *root2 = malloc(sizeof(TreeNode));
+    root2->name = "Root 2";
+    root2->depth = 0;
+    root2->heading = 1;
+    ui_list_append(doc->tree, root2);
+    
+    TreeNode *node_b = malloc(sizeof(TreeNode));
+    node_b->name = "Item";
+    node_b->depth = 1;
+    node_b->heading = 0;
+    ui_list_append(doc->tree, node_b);
+    
     
     //doc->text = ui_text_new(docctx, "text");
     return doc;
@@ -575,9 +593,12 @@
     return node->name;
 }
 
-void sourcelist2_getvalue(UiList *list, void *elm, int index, UiSubListItem *item, void *userdata) {
+void sourcelist2_getvalue(UiList *list, void *elm, int index, UiSourceListItem *item, void *userdata) {
     TreeNode *node = elm;
-    item->label = node->name;
+    item->label = strdup(node->name);
+    item->button_icon = strdup("call-start");
+    item->depth = node->depth;
+    item->is_header = node->heading;
 }
 
 int tree_getdepth(UiList *list, void *elm, int row, void *userdata) {
@@ -593,6 +614,10 @@
     item->eventdata = sublistdata;
 }
 
+void sourcelist2_activate(UiEvent *event, void *userdata) {
+    printf("sourcelist2 activate: %d\n", event->intval);
+}
+
 typedef struct Item {
     UiObject *obj;
     MyDocument *doc;
@@ -958,7 +983,7 @@
             //UiModel *model = ui_model(obj->ctx, UI_STRING, "test", -1);
             //ui_treeview(obj, .fill = TRUE, .model = model, .show_header = TRUE, .getvalue = tree_getvalue, .getdepth = tree_getdepth, .varname = "tree");
             
-            ui_sourcelist2(obj, .fill = TRUE, .getvalue = sourcelist2_getvalue, .getdepth = tree_getdepth, .varname = "tree");
+            ui_sourcelist2(obj, .fill = TRUE, .getvalue = sourcelist2_getvalue, .varname = "tree", .onactivate = sourcelist2_activate);
         }
     }
     
--- 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);
     }
--- a/ui/gtk/list.h	Thu Sep 10 17:36:06 2026 +0200
+++ b/ui/gtk/list.h	Fri Sep 11 17:27:12 2026 +0200
@@ -152,8 +152,6 @@
     CxList                       *rows;
     ui_sourcelist_getvalue_func  getvalue;
     void                         *getvaluedata;
-    ui_tree_getdepthfunc         getdepth;
-    void                         *getdepthdata;
     
     ui_callback                  onactivate;
     void                         *onactivatedata;
@@ -165,10 +163,13 @@
 
 typedef struct UiListBox2Row {
     UiListBox2    *listbox;
-    CxList        *children; // type: int (index)
+    GtkWidget     *row;
+    GtkWidget     *expander;
+    CxList        *children; // type: UiListBox2Row*
     int           index;
     int           depth;
-    int           expanded;
+    UiBool        expanded;
+    UiBool        heading;
 } UiListBox2Row;
 
 void* ui_null_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult);
--- a/ui/gtk/toolkit.h	Thu Sep 10 17:36:06 2026 +0200
+++ b/ui/gtk/toolkit.h	Fri Sep 11 17:27:12 2026 +0200
@@ -59,6 +59,7 @@
 #define WINDOW_DESTROY(window) gtk_window_destroy(GTK_WINDOW(window))
 #define WINDOW_SET_CONTENT(window, child) gtk_window_set_child(GTK_WINDOW(window), child)
 #define WIDGET_NO_SHOW_ALL(widget)
+#define WIDGET_ENABLE_SHOW_ALL(widget)
 #define BOX_ADD(box, child) gtk_box_append(GTK_BOX(box), child)
 #define BOX_ADD_EXPAND(box, child) gtk_widget_set_hexpand(child, TRUE); gtk_widget_set_vexpand(child, TRUE); gtk_box_append(GTK_BOX(box), child)
 #define BOX_ADD_NO_EXPAND(box, child) gtk_box_append(GTK_BOX(box), child)
@@ -89,6 +90,7 @@
 #define WINDOW_DESTROY(window) gtk_widget_destroy(window)
 #define WINDOW_SET_CONTENT(window, child) gtk_container_add(GTK_CONTAINER(window), child)
 #define WIDGET_NO_SHOW_ALL(widget) gtk_widget_set_no_show_all(widget, TRUE)
+#define WIDGET_ENABLE_SHOW_ALL(widget) gtk_widget_set_no_show_all(widget, FALSE)
 #define BOX_ADD(box, child) gtk_container_add(GTK_CONTAINER(box), child)
 #define BOX_ADD_EXPAND(box, child) gtk_box_pack_start(GTK_BOX(box), child, TRUE, TRUE, 0)
 #define BOX_ADD_NO_EXPAND(box, child) gtk_box_pack_start(GTK_BOX(box), child, TRUE, FALSE, 0)  
--- a/ui/motif/list.h	Thu Sep 10 17:36:06 2026 +0200
+++ b/ui/motif/list.h	Fri Sep 11 17:27:12 2026 +0200
@@ -58,6 +58,10 @@
     ui_callback ondrop;
     void* ondropsdata;
     UiBool multiselection;
+    
+    ui_tree_getdepthfunc getdepth;
+    void *getdepthdata;
+    UiBool is_treeview;
 } UiListView;
 
 void ui_listview_destroy(Widget w, UiListView *listview, XtPointer d);
--- a/ui/ui/list.h	Thu Sep 10 17:36:06 2026 +0200
+++ b/ui/ui/list.h	Fri Sep 11 17:27:12 2026 +0200
@@ -45,6 +45,7 @@
 
 typedef struct UiSubList          UiSubList;
 typedef struct UiSubListItem      UiSubListItem;
+typedef struct UiSourceListItem   UiSourceListItem;
 
 typedef enum UiModelType {
     UI_STRING = 0,
@@ -194,7 +195,7 @@
 
 typedef void (*ui_sublist_getvalue_func)(UiList *list, void *sublist_userdata, void *rowdata, int index, UiSubListItem *item, void *userdata);
 
-typedef void (*ui_sourcelist_getvalue_func)(UiList *list, void *elm, int index, UiSubListItem *item, void *userdata);
+typedef void (*ui_sourcelist_getvalue_func)(UiList *list, void *elm, int index, UiSourceListItem *item, void *userdata);
 
 struct UiSubList {
     UiList *value;
@@ -311,6 +312,23 @@
     const int *visibility_states;;
 };
 
+/*
+ * list item members must be filled by the sublist getvalue func
+ * all members must be allocated (by malloc, strdup, ...) the pointer
+ * will be passed to free
+ */
+struct UiSourceListItem {
+    char          *icon;
+    char          *label;
+    char          *button_icon;
+    char          *button_label;
+    UiMenuBuilder *button_menu;
+    char          *badge;
+    void          *eventdata;
+    UiBool        is_header;
+    int           depth;
+};
+
 struct UiSourceList2Args {
     UiBool fill;
     UiBool hexpand;
@@ -345,17 +363,6 @@
     void *getvaluedata;
     
     /*
-     * callback for each list item, that determines the depth
-     * and indirectly the parent for the item
-     */
-    ui_tree_getdepthfunc getdepth;
-    
-    /*
-     * getdepth func userdata
-     */
-    void *getdepthdata;
-    
-    /*
      * activated when a list item is selected
      */
     ui_callback onactivate;

mercurial