--- a/ui/gtk/list.c Sat Oct 04 14:54:25 2025 +0200 +++ b/ui/gtk/list.c Sun Oct 19 21:20:08 2025 +0200 @@ -245,7 +245,10 @@ GtkEventController *focus_controller = gtk_event_controller_focus_new(); g_signal_connect(focus_controller, "leave", G_CALLBACK(cell_entry_leave_focus), entry_data); gtk_widget_add_controller(textfield, focus_controller); - } else { + } else if(type == UI_BOOL_EDITABLE) { + GtkWidget *checkbox = gtk_check_button_new(); + gtk_list_item_set_child(item, checkbox); + }else { GtkWidget *label = gtk_label_new(NULL); gtk_label_set_xalign(GTK_LABEL(label), 0); gtk_list_item_set_child(item, label); @@ -390,6 +393,11 @@ ENTRY_SET_TEXT(child, data); break; } + case UI_BOOL_EDITABLE: { + intptr_t i = (intptr_t)data; + gtk_check_button_set_active(GTK_CHECK_BUTTON(child), (gboolean)i); + break; + } } if(attributes != listview->current_row_attributes) { @@ -417,6 +425,8 @@ entry->listview = NULL; free(entry->previous_value); entry->previous_value = NULL; + } else if(GTK_IS_CHECK_BUTTON(child)) { + } } @@ -435,8 +445,6 @@ } UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - // to simplify things and share code with ui_table_create, we also // use a UiModel for the listview UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1); @@ -464,7 +472,7 @@ GtkSelectionModel *selection_model = create_selection_model(listview, ls, args->multiselection); GtkWidget *view = gtk_list_view_new(GTK_SELECTION_MODEL(selection_model), factory); - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; @@ -513,19 +521,26 @@ GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS SCROLLEDWINDOW_SET_CHILD(scroll_area, view); - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, scroll_area); + if(args->width > 0 || args->height > 0) { + int width = args->width; + int height = args->height; + if(width == 0) { + width = -1; + } + if(height == 0) { + height = -1; + } + gtk_widget_set_size_request(scroll_area, width, height); + } - // ct->current should point to view, not scroll_area, to make it possible - // to add a context menu - current->container->current = view; + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, scroll_area, &layout); return scroll_area; } UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - // to simplify things and share code with ui_tableview_create, we also // use a UiModel for the listview UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1); @@ -553,8 +568,11 @@ GtkWidget *view = gtk_drop_down_new(G_LIST_MODEL(ls), NULL); gtk_drop_down_set_factory(GTK_DROP_DOWN(view), factory); + if(args->width > 0) { + gtk_widget_set_size_request(view, args->width, -1); + } - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; @@ -589,8 +607,10 @@ } // add widget to parent - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, view); + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, view, &layout); + return view; } @@ -604,8 +624,6 @@ } UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - GListStore *ls = g_list_store_new(G_TYPE_OBJECT); //g_list_store_append(ls, v1); @@ -616,7 +634,7 @@ GtkSelectionModel *selection_model = create_selection_model(tableview, ls, args->multiselection); GtkWidget *view = gtk_column_view_new(GTK_SELECTION_MODEL(selection_model)); - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init tableview tableview->widget = view; @@ -697,12 +715,21 @@ GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS SCROLLEDWINDOW_SET_CHILD(scroll_area, view); - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, scroll_area); + if(args->width > 0 || args->height > 0) { + int width = args->width; + int height = args->height; + if(width == 0) { + width = -1; + } + if(height == 0) { + height = -1; + } + gtk_widget_set_size_request(scroll_area, width, height); + } - // ct->current should point to view, not scroll_area, to make it possible - // to add a context menu - current->container->current = view; + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, scroll_area, &layout); return scroll_area; } @@ -1126,8 +1153,6 @@ UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - // create treeview GtkWidget *view = gtk_tree_view_new(); ui_set_name_and_style(view, args->name, args->style_class); @@ -1161,7 +1186,7 @@ G_CALLBACK(ui_listview_destroy), listview); - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; @@ -1221,12 +1246,21 @@ GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS SCROLLEDWINDOW_SET_CHILD(scroll_area, view); - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, scroll_area); + if(args->width > 0 || args->height > 0) { + int width = args->width; + int height = args->height; + if(width == 0) { + width = -1; + } + if(height == 0) { + height = -1; + } + gtk_widget_set_size_request(scroll_area, width, height); + } - // ct->current should point to view, not scroll_area, to make it possible - // to add a context menu - current->container->current = view; + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, scroll_area, &layout); return scroll_area; } @@ -1243,8 +1277,6 @@ } UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - // create treeview GtkWidget *view = gtk_tree_view_new(); @@ -1343,7 +1375,7 @@ #endif - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); //g_signal_connect(view, "drag-begin", G_CALLBACK(drag_begin), NULL); //g_signal_connect(view, "drag-end", G_CALLBACK(drag_end), NULL); @@ -1416,6 +1448,18 @@ GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS SCROLLEDWINDOW_SET_CHILD(scroll_area, view); + if(args->width > 0 || args->height > 0) { + int width = args->width; + int height = args->height; + if(width == 0) { + width = -1; + } + if(height == 0) { + height = -1; + } + gtk_widget_set_size_request(scroll_area, width, height); + } + if(args->contextmenu) { UIMENU menu = ui_contextmenu_create(args->contextmenu, obj, scroll_area); #if GTK_MAJOR_VERSION >= 4 @@ -1425,12 +1469,9 @@ #endif } - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, scroll_area); - - // ct->current should point to view, not scroll_area, to make it possible - // to add a context menu - current->container->current = view; + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, scroll_area, &layout); return scroll_area; } @@ -1476,15 +1517,16 @@ /* --------------------------- ComboBox --------------------------- */ UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs *args) { - UiObject* current = uic_current_obj(obj); - GtkWidget *combobox = gtk_combo_box_new(); + if(args->width > 0) { + gtk_widget_set_size_request(combobox, args->width, -1); + } ui_set_name_and_style(combobox, args->name, args->style_class); ui_set_widget_groups(obj->ctx, combobox, args->groups); - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, combobox); - current->container->current = combobox; + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, combobox, &layout); UiListView *listview = create_listview(obj, args); listview->widget = combobox; @@ -1496,7 +1538,7 @@ G_CALLBACK(ui_listview_destroy), listview); - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->list, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); UiList *list = var ? var->value : NULL; GtkListStore *listmodel = create_list_store(listview, list); if(var) { @@ -2021,6 +2063,18 @@ /* ------------------------------ Source List ------------------------------ */ +static ui_sourcelist_update_func sourcelist_update_finished_callback; + +void ui_sourcelist_set_update_callback(ui_sourcelist_update_func cb) { + sourcelist_update_finished_callback = cb; +} + +static void ui_sourcelist_update_finished(void) { + if(sourcelist_update_finished_callback) { + sourcelist_update_finished_callback(); + } +} + static void ui_destroy_sourcelist(GtkWidget *w, UiListBox *v) { cxListFree(v->sublists); free(v); @@ -2047,7 +2101,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) { @@ -2110,8 +2164,6 @@ } UIEXPORT UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs *args) { - UiObject* current = uic_current_obj(obj); - #ifdef UI_GTK3 GtkWidget *listbox = g_object_new(ui_sidebar_list_box_get_type(), NULL); #else @@ -2130,12 +2182,14 @@ ui_set_name_and_style(listbox, args->name, args->style_class); ui_set_widget_groups(obj->ctx, listbox, args->groups); - UI_APPLY_LAYOUT2(current, args); - current->container->add(current->container, scroll_area); + UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; + UiLayout layout = UI_ARGS2LAYOUT(args); + ct->add(ct, scroll_area, &layout); 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; @@ -2164,7 +2218,7 @@ // fill items ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists)); } else { - UiVar* var = uic_widget_var(obj->ctx, current->ctx, args->dynamic_sublist, args->varname, UI_VAR_LIST); + UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->dynamic_sublist, args->varname, UI_VAR_LIST); if(var) { UiList *list = var->value; list->obj = uilistbox; @@ -2257,9 +2311,11 @@ ui_listbox_update_sublist(listbox, sublist, pos); pos += sublist->numitems; } + + ui_sourcelist_update_finished(); } -static void listbox_button_clicked(GtkWidget *widget, UiEventDataExt *data) { +static void listbox_button_clicked(GtkWidget *button, UiEventDataExt *data) { UiListBoxSubList *sublist = data->customdata0; UiSubListEventData eventdata; @@ -2282,15 +2338,36 @@ if(data->callback2) { data->callback2(&event, data->userdata2); } + + if(data->customdata3) { + UIMENU menu = data->customdata3; + g_object_set_data(G_OBJECT(button), "ui-button-popup", menu); + gtk_popover_popup(GTK_POPOVER(menu)); + } } +#if GTK_CHECK_VERSION(3, 0, 0) +static void button_popover_closed(GtkPopover *popover, GtkWidget *button) { + g_object_set_data(G_OBJECT(button), "ui-button-popup", NULL); + if(g_object_get_data(G_OBJECT(button), "ui-button-invisible")) { + g_object_set_data(G_OBJECT(button), "ui-button-invisible", NULL); + gtk_widget_set_visible(button, FALSE); + } +} +#endif + 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) { @@ -2311,6 +2388,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", @@ -2345,11 +2425,23 @@ G_CALLBACK(listbox_button_clicked), event ); + gtk_widget_set_visible(button, FALSE); + + g_object_set_data(G_OBJECT(row), "ui-listbox-row-button", button); + + // menu + if(item->button_menu) { + UIMENU menu = ui_contextmenu_create(item->button_menu, listbox->obj, button); + event->customdata3 = menu; + g_signal_connect(menu, "closed", G_CALLBACK(button_popover_closed), button); + ui_menubuilder_unref(item->button_menu); + } } } 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; } @@ -2378,6 +2470,62 @@ free(item.badge); } +static void listbox_row_on_enter(GtkWidget *row) { + GtkWidget *button = g_object_get_data(G_OBJECT(row), "ui-listbox-row-button"); + if(button) { + gtk_widget_set_visible(button, TRUE); + } +} + +static void listbox_row_on_leave(GtkWidget *row) { + GtkWidget *button = g_object_get_data(G_OBJECT(row), "ui-listbox-row-button"); + if(button) { + if(!g_object_get_data(G_OBJECT(button), "ui-button-popup")) { + gtk_widget_set_visible(button, FALSE); + } else { + g_object_set_data(G_OBJECT(button), "ui-button-invisible", (void*)1); + } + } +} + +#if GTK_CHECK_VERSION(4, 0, 0) +static void listbox_row_enter( + GtkEventControllerMotion* self, + gdouble x, + gdouble y, + GtkWidget *row) +{ + listbox_row_on_enter(row); +} + +static void listbox_row_leave( + GtkEventControllerMotion* self, + GtkWidget *row) +{ + listbox_row_on_leave(row); +} +#else +static gboolean listbox_row_enter( + GtkWidget *row, + GdkEventCrossing event, + gpointer user_data) +{ + listbox_row_on_enter(row); + return FALSE; +} + + +static gboolean listbox_row_leave( + GtkWidget *row, + GdkEventCrossing *event, + gpointer user_data) +{ + listbox_row_on_leave(row); + return FALSE; +} + +#endif + void ui_listbox_update_sublist(UiListBox *listbox, UiListBoxSubList *sublist, size_t listbox_insert_index) { // clear sublist CxIterator r = cxListIterator(sublist->widgets); @@ -2397,22 +2545,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); + //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); 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) { @@ -2421,10 +2579,25 @@ item.label = strdup(elm); } + if(item.label == NULL && index == -1 && sublist->header) { + item.label = strdup(sublist->header); + } + // create listbox item GtkWidget *row = gtk_list_box_row_new(); - listbox_fill_row(listbox, row, sublist, &item, (int)index); - if(index == 0) { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController *motion_controller = gtk_event_controller_motion_new(); + gtk_widget_add_controller(GTK_WIDGET(row), motion_controller); + g_signal_connect(motion_controller, "enter", G_CALLBACK(listbox_row_enter), row); + g_signal_connect(motion_controller, "leave", G_CALLBACK(listbox_row_leave), row); +#else + gtk_widget_set_events(GTK_WIDGET(row), GDK_POINTER_MOTION_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + g_signal_connect(row, "enter-notify-event", G_CALLBACK(listbox_row_enter), NULL); + g_signal_connect(row, "leave-notify-event", G_CALLBACK(listbox_row_leave), NULL); +#endif + + 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); @@ -2435,9 +2608,9 @@ listbox->first_row = GTK_LIST_BOX_ROW(row); } } - 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); + //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 + header_row); cxListAdd(sublist->widgets, row); // cleanup @@ -2448,7 +2621,7 @@ free(item.badge); // next row - elm = list->next(list); + elm = index >= 0 ? list->next(list) : first; index++; } @@ -2468,6 +2641,8 @@ } else { update_sublist_item(sublist->listbox, sublist, i); } + + ui_sourcelist_update_finished(); } void ui_listbox_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data) { @@ -2482,7 +2657,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;