add support for selectable source list header rows (GTK)

Fri, 17 Oct 2025 15:44:30 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 17 Oct 2025 15:44:30 +0200
changeset 853
380ec881faa2
parent 852
a04cb4398034
child 854
9291921f21c5

add support for selectable source list header rows (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.c file | annotate | diff | comparison | revisions
ui/ui/tree.h file | annotate | diff | comparison | revisions
--- a/application/main.c	Thu Oct 16 15:57:05 2025 +0200
+++ b/application/main.c	Fri Oct 17 15:44:30 2025 +0200
@@ -444,7 +444,9 @@
 void action_sourcelist_activate(UiEvent *event, void *userdata) {
     UiSubListEventData *eventdata = event->eventdata;
     printf("sourcelist %s index %d\n", eventdata->row_data, eventdata->row_index);
-    ui_list_update_row(eventdata->list, eventdata->row_index);
+    if(eventdata->row_index >= 0) {
+        ui_list_update_row(eventdata->list, eventdata->row_index);
+    }
 }
 
 void action_table_activate(UiEvent *event, void *userdata) {
@@ -609,6 +611,7 @@
     ui_sidebar(obj, .margin = 0, .spacing = 0) {
         ui_sourcelist(obj, .fill = TRUE,
                 .getvalue = sourcelist_getvalue,
+                .header_is_item = TRUE,
                 .sublists = UI_SUBLISTS(UI_SUBLIST(.varname = "srclist1", .header = "Header 1", .userdata = "Sublist1"), UI_SUBLIST(.varname = "srclist2", .header = "Header 2", .userdata = "Sublist2")),
                 .onactivate = action_sourcelist_activate);
     }
--- a/ui/gtk/list.c	Thu Oct 16 15:57:05 2025 +0200
+++ b/ui/gtk/list.c	Fri Oct 17 15:44:30 2025 +0200
@@ -2089,7 +2089,7 @@
     if(sublist->separator) {
         GtkWidget *separator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
         gtk_list_box_row_set_header(row, separator);
-    } else if(sublist->header) {
+    } else if(sublist->header && !listbox->header_is_item) {
         GtkWidget *header = gtk_label_new(sublist->header);
         gtk_widget_set_halign(header, GTK_ALIGN_START);
         if(row == listbox->first_row) {
@@ -2177,6 +2177,7 @@
     UiListBox *uilistbox = malloc(sizeof(UiListBox));
     uilistbox->obj = obj;
     uilistbox->listbox = GTK_LIST_BOX(listbox);
+    uilistbox->header_is_item = args->header_is_item;
     uilistbox->getvalue = args->getvalue;
     uilistbox->getvaluedata = args->getvaluedata;
     uilistbox->onactivate = args->onactivate;
@@ -2326,12 +2327,17 @@
 }
 
 static void listbox_fill_row(UiListBox *listbox, GtkWidget *row, UiListBoxSubList *sublist, UiSubListItem *item, int index) {
+    UiBool is_header = index < 0;
+    
     GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
     if(item->icon) {
         GtkWidget *icon = ICON_IMAGE(item->icon);
         BOX_ADD(hbox, icon);
     }
     GtkWidget *label = gtk_label_new(item->label);
+    if(is_header) {
+        WIDGET_ADD_CSS_CLASS(label, "ui-listbox-header-row");
+    }
     gtk_widget_set_halign(label, GTK_ALIGN_START);
     BOX_ADD_EXPAND(hbox, label);
     if(item->badge) {
@@ -2352,6 +2358,9 @@
     event->userdata2 = listbox->onbuttonclickdata;
     event->value0 = index;
     
+    // TODO: semi-memory leak when listbox_fill_row is called again for the same row
+    // each row update will create a new UiEventDataExt object and a separate destroy handler
+    
     g_signal_connect(
             row,
             "destroy",
@@ -2390,7 +2399,8 @@
 }
 
 static void update_sublist_item(UiListBox *listbox, UiListBoxSubList *sublist, int index) {
-    GtkListBoxRow *row = gtk_list_box_get_row_at_index(listbox->listbox, sublist->startpos + index);
+    int header_row = listbox->header_is_item && sublist->header ? 1 : 0;
+    GtkListBoxRow *row = gtk_list_box_get_row_at_index(listbox->listbox, sublist->startpos + index + header_row);
     if(!row) {
         return;
     }
@@ -2438,22 +2448,32 @@
         return;
     }
     
-    size_t index = 0;
+    int index = 0;
     void *elm = list->first(list);
+    void *first = elm;
     
-    if(!elm && sublist->header) {
+    if(sublist->header && !listbox->header_is_item && !elm) {
         // empty row for header
         GtkWidget *row = gtk_list_box_row_new();
         cxListAdd(sublist->widgets, row);
         g_object_set_data(G_OBJECT(row), "ui_listbox", listbox);
         g_object_set_data(G_OBJECT(row), "ui_listbox_sublist", sublist);
         intptr_t rowindex = listbox_insert_index + index;
-        g_object_set_data(G_OBJECT(row), "ui_listbox_row_index", (gpointer)rowindex);
+        //g_object_set_data(G_OBJECT(row), "ui_listbox_row_index", (gpointer)rowindex);
         gtk_list_box_insert(listbox->listbox, row, listbox_insert_index + index);
         sublist->numitems = 1;
         return;
     }
     
+    int first_index = 0;
+    int header_row = 0;
+    if(listbox->header_is_item && sublist->header) {
+        index = -1;
+        first_index = -1;
+        header_row = 1;
+        elm = sublist->header;
+    }
+    
     while(elm) {
         UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL };
         if(listbox->getvalue) {
@@ -2464,8 +2484,8 @@
         
         // create listbox item
         GtkWidget *row = gtk_list_box_row_new();
-        listbox_fill_row(listbox, row, sublist, &item, (int)index);
-        if(index == 0) {
+        listbox_fill_row(listbox, row, sublist, &item, index);
+        if(index == first_index) {
             // first row in the sublist, set ui_listbox data to the row
             // which is then used by the headerfunc
             g_object_set_data(G_OBJECT(row), "ui_listbox", listbox);
@@ -2477,8 +2497,8 @@
             }
         }
         intptr_t rowindex = listbox_insert_index + index;
-        g_object_set_data(G_OBJECT(row), "ui_listbox_row_index", (gpointer)rowindex);
-        gtk_list_box_insert(listbox->listbox, row, listbox_insert_index + index);
+        //g_object_set_data(G_OBJECT(row), "ui_listbox_row_index", (gpointer)rowindex);
+        gtk_list_box_insert(listbox->listbox, row, listbox_insert_index + index + header_row);
         cxListAdd(sublist->widgets, row);
         
         // cleanup
@@ -2489,7 +2509,7 @@
         free(item.badge);
         
         // next row
-        elm = list->next(list);
+        elm = index >= 0 ? list->next(list) : first;
         index++;
     }
     
@@ -2523,7 +2543,7 @@
     eventdata.sublist_index = sublist->index;
     eventdata.row_index = data->value0;
     eventdata.sublist_userdata = sublist->userdata;
-    eventdata.row_data = eventdata.list->get(eventdata.list, eventdata.row_index);
+    eventdata.row_data = eventdata.row_index >= 0 ? eventdata.list->get(eventdata.list, eventdata.row_index) : NULL;
     eventdata.event_data = data->customdata2;
     
     UiEvent event;
--- a/ui/gtk/list.h	Thu Oct 16 15:57:05 2025 +0200
+++ b/ui/gtk/list.h	Fri Oct 17 15:44:30 2025 +0200
@@ -126,6 +126,7 @@
     ui_callback              onbuttonclick;
     void                     *onbuttonclickdata;
     GtkListBoxRow            *first_row;
+    UiBool                   header_is_item;
 };
 
 
--- a/ui/gtk/toolkit.c	Thu Oct 16 15:57:05 2025 +0200
+++ b/ui/gtk/toolkit.c	Fri Oct 17 15:44:30 2025 +0200
@@ -394,6 +394,9 @@
 "  margin-top: 4px;\n"
 "  margin-bottom: 10px;\n"
 "}\n"
+".ui-listbox-header-row {\n"
+"  font-weight: bold;\n"
+"}\n"
 ".ui-badge {\n"
 "  background-color: #e53935;\n"
 "  color: white;\n"
@@ -444,6 +447,9 @@
 "  margin-top: 4px;\n"
 "  margin-bottom: 10px;\n"
 "}\n"
+".ui-listbox-header-row {\n"
+"  font-weight: bold;\n"
+"}\n"
 ".ui-badge {\n"
 "  background-color: #e53935;\n"
 "  color: white;\n"
--- a/ui/ui/tree.h	Thu Oct 16 15:57:05 2025 +0200
+++ b/ui/ui/tree.h	Fri Oct 17 15:44:30 2025 +0200
@@ -185,12 +185,13 @@
  * will be passed to free
  */
 struct UiSubListItem {
-    char *icon;
-    char *label;
-    char *button_icon;
-    char *button_label;
-    char *badge;
-    void *eventdata;
+    char          *icon;
+    char          *label;
+    char          *button_icon;
+    char          *button_label;
+    UiMenuBuilder *button_menu;
+    char          *badge;
+    void          *eventdata;
 };
 
 struct UiSourceListArgs {
@@ -255,6 +256,11 @@
     void *getvaluedata;
     
     /*
+     * is a sublist header a selectable item
+     */
+    UiBool header_is_item;
+    
+    /*
      * activated when a list item is selected
      */
     ui_callback onactivate;

mercurial