# HG changeset patch # User Olaf Wintermann # Date 1789140432 -7200 # Node ID 43a7874a9d0ecc670a26989954b509f0d32b78fc # Parent 4163776eef4981f145ad413af7f13de243be75ed implement sourcelist2 widget (GTK) diff -r 4163776eef49 -r 43a7874a9d0e application/main.c --- 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); } } diff -r 4163776eef49 -r 43a7874a9d0e ui/gtk/list.c --- 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); } diff -r 4163776eef49 -r 43a7874a9d0e ui/gtk/list.h --- 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); diff -r 4163776eef49 -r 43a7874a9d0e ui/gtk/toolkit.h --- 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) diff -r 4163776eef49 -r 43a7874a9d0e ui/motif/list.h --- 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); diff -r 4163776eef49 -r 43a7874a9d0e ui/ui/list.h --- 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;