Sun, 07 Dec 2025 14:39:03 +0100
implement dynamic table models (GTK)
| application/main.c | file | annotate | diff | comparison | revisions | |
| ui/cocoa/list.m | file | annotate | diff | comparison | revisions | |
| ui/common/types.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/text.c | file | annotate | diff | comparison | revisions | |
| ui/motif/list.c | file | annotate | diff | comparison | revisions | |
| ui/ui/tree.h | file | annotate | diff | comparison | revisions |
--- a/application/main.c Sun Dec 07 12:24:12 2025 +0100 +++ b/application/main.c Sun Dec 07 14:39:03 2025 +0100 @@ -197,11 +197,14 @@ UiList *items; UiGeneric *web; UiString *list_input; + UiString *list_new_col; UiList *list11; UiString *link; UiString *link_label; UiString *link_uri; UiList *submenulist; + + UiModel *model; } MyDocument; MyDocument *doc1; @@ -349,6 +352,7 @@ doc->web = ui_generic_new(docctx, NULL); doc->list_input = ui_string_new(docctx, "list_input"); + doc->list_new_col = ui_string_new(docctx, NULL); doc->list11 = ui_list_new(docctx, "list11"); ui_list_append(doc->list11, "Item 1"); @@ -471,6 +475,7 @@ UiMenuBuilder *sourcelist_menu; void* table_getvalue(void *row, int col) { + return "test"; switch(col) { case 0: return ui_foldericon(16); case 1: return row; @@ -568,6 +573,13 @@ doc->list2->update(doc->list2, 1); } +static void action_add_col(UiEvent *event, void *userdata) { + MyDocument *doc = event->document; + + char *colname = ui_get(doc->list_new_col); + ui_model_add_column(doc->model, UI_STRING, colname, 200); +} + static void action_list_selection(UiEvent *event, void *userdata) { UiListSelection *sel = event->eventdata; printf("list selection[%d]\n", sel->count); @@ -695,15 +707,20 @@ } } ui_tab(obj, "Tab 1") { - UiModel *model = ui_model(obj->ctx, UI_ICON_TEXT, "col1", UI_INTEGER, "col2", UI_ICON, "col3", UI_ICON_TEXT, "col4", UI_INTEGER, "col5", UI_STRING_EDITABLE, "edit6", UI_STRING_EDITABLE, "edit7", UI_BOOL_EDITABLE, "Check", -1); + //UiModel *model = ui_model(obj->ctx, UI_ICON_TEXT, "col1", UI_INTEGER, "col2", UI_ICON, "col3", UI_ICON_TEXT, "col4", UI_INTEGER, "col5", UI_STRING_EDITABLE, "edit6", UI_STRING_EDITABLE, "edit7", UI_BOOL_EDITABLE, "Check", -1); + UiModel *model = ui_model(obj->ctx, UI_STRING, "Col", -1); model->columnsize[0] = -1; + doc->model = model; ui_table(obj, .model = model, .list = doc->list2, .colspan = 2, .fill = TRUE, .contextmenu = menubuilder, .multiselection = TRUE, .fill = TRUE, .getvalue = table_getvalue, .getstyle = table_getstyle, .onsave = list_save, .onactivate = action_table_activate, .onactivatedata = "activate", .onselection = action_table_activate, .onselectiondata = "selection"); - ui_hbox(obj, .fill = FALSE) { + ui_hbox(obj, .fill = FALSE, .columnspacing = 4) { ui_textfield(obj, .value = doc->list_input); ui_button(obj, .label = "Update List Item 1", .onclick = action_update_list); + ui_label(obj, .label = "Column Name"); + ui_textfield(obj, .value = doc->list_new_col); + ui_button(obj, .label = "Add Column", .onclick = action_add_col); } } ui_tab(obj, "Tab 2") {
--- a/ui/cocoa/list.m Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/cocoa/list.m Sun Dec 07 14:39:03 2025 +0100 @@ -179,7 +179,7 @@ ListDataSource *dataSource = [[ListDataSource alloc] init:cols var:var getvalue:getvalue getvaluedata:getvaluedata]; if(model) { - dataSource.model = ui_model_copy(obj->ctx, model); + dataSource.model = model; } tableview.dataSource = dataSource;
--- a/ui/common/types.c Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/common/types.c Sun Dec 07 14:39:03 2025 +0100 @@ -252,6 +252,15 @@ #define UI_MODEL_DEFAULT_ALLOC_SIZE 16 + +static void model_notify_observer(UiModel *model, int insert, int delete) { + UiModelChangeObserver *obs = model->observer; + while(obs) { + obs->update(model, obs->userdata, insert, delete); + obs = obs->next; + } +} + UiModel* ui_model_new(UiContext *ctx) { UiModel *info = ui_calloc(ctx, 1, sizeof(UiModel)); info->ctx = ctx; @@ -262,16 +271,21 @@ return info; } -void ui_model_add_column(UiContext *ctx, UiModel *model, UiModelType type, const char *title, int width) { +void ui_model_add_column(UiModel *model, UiModelType type, const char *title, int width) { + UiContext *ctx = model->ctx; if(model->columns >= model->alloc) { model->alloc += UI_MODEL_DEFAULT_ALLOC_SIZE; model->types = ui_realloc(ctx, model->types, model->alloc * sizeof(UiModelType)); model->titles = ui_realloc(ctx, model->titles, model->alloc * sizeof(char*)); model->columnsize = ui_realloc(ctx, model->columnsize, model->alloc * sizeof(int)); } - model->types[model->columns] = type; - model->titles[model->columns] = ui_strdup(ctx, title); - model->columnsize[model->columns] = width; + int index = model->columns; + model->types[index] = type; + model->titles[index] = ui_strdup(ctx, title); + model->columnsize[index] = width; + + model_notify_observer(model, index, -1); + model->columns++; } @@ -299,18 +313,63 @@ model->ref++; } -void ui_model_unref(UiModel *model) { +void ui_model_unref(UiModel *model) { if(--model->ref == 0) { ui_model_free(model); } } +void ui_model_add_observer(UiModel *model, ui_model_update_func update, void *data) { + UiModelChangeObserver *observer = ui_malloc(model->ctx, sizeof(UiModelChangeObserver)); + observer->update = update; + observer->userdata = data; + observer->next = NULL; + + if(model->observer) { + UiModelChangeObserver *last = model->observer; + while(last->next) { + last = last->next; + } + last->next = observer; + } else { + model->observer = observer; + } +} + +void ui_model_remove_observer(UiModel *model, void *data) { + if(model->observer) { + UiModelChangeObserver *obs = model->observer; + UiModelChangeObserver *prev = NULL; + while(obs) { + if(obs->userdata == data) { + // remove + if(prev) { + prev->next = obs->next; + } else { + model->observer = obs->next; + } + // free + ui_free(model->ctx, obs); + break; + } + prev = obs; + obs = obs->next; + } + } +} + void ui_model_free(UiModel *mi) { UiContext *ctx = mi->ctx; const CxAllocator* a = ctx->allocator; for(int i=0;i<mi->columns;i++) { ui_free(ctx, mi->titles[i]); } + UiModelChangeObserver *obs = mi->observer; + while(obs) { + UiModelChangeObserver *n = obs->next; + cxFree(a, obs); + obs = n; + } cxFree(a, mi->types); cxFree(a, mi->titles); cxFree(a, mi->columnsize);
--- a/ui/gtk/list.c Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/gtk/list.c Sun Dec 07 14:39:03 2025 +0100 @@ -80,6 +80,7 @@ memset(tableview, 0, sizeof(UiListView)); tableview->obj = obj; tableview->model = args->model; + tableview->multiselection = args->multiselection; tableview->onactivate = args->onactivate; tableview->onactivatedata = args->onactivatedata; tableview->onselection = args->onselection; @@ -98,6 +99,9 @@ tableview->onsave = args->onsave; tableview->onsavedata = args->onsavedata; + tableview->coldata.listview = tableview; + tableview->coldata.column = 0; + if(args->getvalue2) { tableview->getvalue = args->getvalue2; tableview->getvaluedata = args->getvalue2data; @@ -200,7 +204,7 @@ static void column_factory_setup(GtkListItemFactory *factory, GtkListItem *item, gpointer userdata) { UiColData *col = userdata; UiModel *model = col->listview->model; - UiModelType type = model->types[col->model_column]; + UiModelType type = model->types[col->column]; if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) { GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); GtkWidget *image = gtk_image_new(); @@ -281,16 +285,17 @@ UiColData *col = userdata; UiList *list = col->listview->var ? col->listview->var->value : NULL; UiListView *listview = col->listview; + int datacolumn = listview->columns[col->column]; ObjWrapper *obj = gtk_list_item_get_item(item); UiModel *model = col->listview->model; - UiModelType type = model->types[col->model_column]; + UiModelType type = model->types[col->column]; // cache the GtkListItem CxHashKey row_key = cx_hash_key(&obj->i, sizeof(int)); UiRowItems *row = cxMapGet(listview->bound_rows, row_key); if(row) { - if(row->items[col->model_column] == NULL) { + if(row->items[col->column] == NULL) { row->bound++; } } else { @@ -298,10 +303,10 @@ cxMapPut(listview->bound_rows, row_key, row); row->bound = 1; } - row->items[col->model_column] = item; + row->items[col->column] = item; UiBool freevalue = FALSE; - void *data = listview->getvalue(list, obj->data, obj->i, col->data_column, listview->getvaluedata, &freevalue); + void *data = listview->getvalue(list, obj->data, obj->i, datacolumn, listview->getvaluedata, &freevalue); GtkWidget *child = gtk_list_item_get_child(item); PangoAttrList *attributes = NULL; @@ -319,7 +324,7 @@ } } - int style_col = col->data_column; + int style_col = datacolumn; if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) { style_col++; // col->data_column is the icon, we need the next col for the label } @@ -363,7 +368,7 @@ } case UI_ICON_TEXT_FREE: { - void *data2 = listview->getvalue(list, obj->data, obj->i, col->data_column+1, listview->getvaluedata, &freevalue); + void *data2 = listview->getvalue(list, obj->data, obj->i, datacolumn+1, listview->getvaluedata, &freevalue); if(type == UI_ICON_TEXT_FREE) { freevalue = TRUE; } @@ -387,7 +392,7 @@ if(entry) { entry->listview = col->listview; entry->row = obj->i; - entry->col = col->data_column; + entry->col = datacolumn; entry->previous_value = strdup(data); } ENTRY_SET_TEXT(child, data); @@ -411,7 +416,7 @@ CxHashKey row_key = cx_hash_key(&obj->i, sizeof(int)); UiRowItems *row = cxMapGet(listview->bound_rows, row_key); if(row) { - row->items[col->model_column] = NULL; + row->items[col->column] = NULL; row->bound--; if(row->bound == 0) { cxMapRemove(listview->bound_rows, row_key); @@ -457,17 +462,15 @@ } listview->numcolumns = 1; - listview->columns = malloc(sizeof(UiColData)); - listview->columns->listview = listview; - listview->columns->data_column = 0; - listview->columns->model_column = 0; + listview->columns = malloc(sizeof(int)); + listview->columns[0] = 0; listview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128); listview->bound_rows->collection.simple_destructor = (cx_destructor_func)free; GtkListItemFactory *factory = gtk_signal_list_item_factory_new(); - g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), listview->columns); - g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), listview->columns); + g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), &listview->coldata); + g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), &listview->coldata); GtkSelectionModel *selection_model = create_selection_model(listview, ls, args->multiselection); GtkWidget *view = gtk_list_view_new(GTK_SELECTION_MODEL(selection_model), factory); @@ -554,17 +557,15 @@ } listview->numcolumns = 1; - listview->columns = malloc(sizeof(UiColData)); - listview->columns->listview = listview; - listview->columns->data_column = 0; - listview->columns->model_column = 0; + listview->columns = malloc(sizeof(int)); + listview->columns[0] = 0; listview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128); listview->bound_rows->collection.simple_destructor = (cx_destructor_func)free; GtkListItemFactory *factory = gtk_signal_list_item_factory_new(); - g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), listview->columns); - g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), listview->columns); + g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), &listview->coldata); + g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), &listview->coldata); GtkWidget *view = gtk_drop_down_new(G_LIST_MODEL(ls), NULL); gtk_drop_down_set_factory(GTK_DROP_DOWN(view), factory); @@ -623,6 +624,30 @@ gtk_drop_down_set_selected(GTK_DROP_DOWN(dropdown), index); } +static void add_column(UiListView *tableview, int index) { + UiModel *model = tableview->model; + + UiColData *col = malloc(sizeof(UiColData)); + col->listview = tableview; + col->column = index; + + GtkListItemFactory *factory = gtk_signal_list_item_factory_new(); + g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), col); + g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), col); + g_object_set_data_full(G_OBJECT(factory), "coldata", col, (GDestroyNotify)free); + + GtkColumnViewColumn *column = gtk_column_view_column_new(model->titles[index], factory); + gtk_column_view_column_set_resizable(column, true); + gtk_column_view_insert_column(GTK_COLUMN_VIEW(tableview->widget), index, column); + + int size = model->columnsize[index]; + if(size > 0) { + gtk_column_view_column_set_fixed_width(column, size); + } else if(size < 0) { + gtk_column_view_column_set_expand(column, TRUE); + } +} + UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) { GListStore *ls = g_list_store_new(G_TYPE_OBJECT); //g_list_store_append(ls, v1); @@ -650,7 +675,11 @@ // create columns from UiModel UiModel *model = args->model; - int columns = model ? model->columns : 0; + int columns = 0; + if(model) { + columns = model->columns; + ui_model_add_observer(model, ui_listview_update_model, tableview); + } tableview->columns = calloc(columns, sizeof(UiColData)); tableview->numcolumns = columns; @@ -660,30 +689,14 @@ int addi = 0; for(int i=0;i<columns;i++) { - tableview->columns[i].listview = tableview; - tableview->columns[i].model_column = i; - tableview->columns[i].data_column = i+addi; + tableview->columns[i] = i+addi; if(model->types[i] == UI_ICON_TEXT || model->types[i] == UI_ICON_TEXT_FREE) { // icon+text has 2 data columns addi++; } - GtkListItemFactory *factory = gtk_signal_list_item_factory_new(); - UiColData *col = &tableview->columns[i]; - g_signal_connect(factory, "setup", G_CALLBACK(column_factory_setup), col); - g_signal_connect(factory, "bind", G_CALLBACK(column_factory_bind), col); - - GtkColumnViewColumn *column = gtk_column_view_column_new(model->titles[i], factory); - gtk_column_view_column_set_resizable(column, true); - gtk_column_view_append_column(GTK_COLUMN_VIEW(view), column); - - int size = model->columnsize[i]; - if(size > 0) { - gtk_column_view_column_set_fixed_width(column, size); - } else if(size < 0) { - gtk_column_view_column_set_expand(column, TRUE); - } + add_column(tableview, i); } // bind listview to list @@ -734,6 +747,33 @@ return scroll_area; } +void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index) { + UiListView *listview = userdata; + if(insert_index >= listview->numcolumns) { + listview->numcolumns = insert_index+1; + listview->columns = realloc(listview->columns, listview->numcolumns * sizeof(UiColData)); + } + + gtk_column_view_set_model(GTK_COLUMN_VIEW(listview->widget), NULL); + + if(insert_index) { + listview->columns[insert_index] = insert_index; + add_column(listview, insert_index); + // TODO: adjust data_column if insert_index < numcolumns + } // TODO: delete_index + + GListStore *ls = g_list_store_new(G_TYPE_OBJECT); + GtkSelectionModel *selection_model = create_selection_model(listview, ls, listview->multiselection); + gtk_column_view_set_model(GTK_COLUMN_VIEW(listview->widget), selection_model); + listview->selectionmodel = selection_model; + listview->liststore = ls; + + if(listview->var) { + UiList *list = listview->var->value; + ui_list_update(list); + } +} + static UiListSelection selectionmodel_get_selection(GtkSelectionModel *model) { UiListSelection sel = { 0, NULL }; GtkBitset *bitset = gtk_selection_model_get_selection(model); @@ -2046,6 +2086,10 @@ if(v->var) { ui_destroy_boundvar(v->obj->ctx, v->var); } + if(v->model) { + ui_model_remove_observer(v->model, v); + ui_model_unref(v->model); + } if(v->elements) { for(int i=0;i<v->nelm;i++) { free(v->elements[i]);
--- a/ui/gtk/list.h Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/gtk/list.h Sun Dec 07 14:39:03 2025 +0100 @@ -38,6 +38,7 @@ extern "C" { #endif +typedef struct UiListView UiListView; typedef struct UiColData UiColData; #if GTK_CHECK_VERSION(4, 10, 0) @@ -47,11 +48,17 @@ } UiRowItems; #endif -typedef struct UiListView { +struct UiColData { + UiListView *listview; + int column; +}; + +struct UiListView { UiObject *obj; GtkWidget *widget; UiVar *var; UiModel *model; + UiBool multiselection; ui_getvaluefunc2 getvalue; void *getvaluedata; ui_getstylefunc getstyle; @@ -65,8 +72,9 @@ CxMap *bound_rows; GListStore *liststore; GtkSelectionModel *selectionmodel; - UiColData *columns; + int *columns; int numcolumns; + UiColData coldata; PangoAttrList *current_row_attributes; #else int style_offset; @@ -85,12 +93,6 @@ void *onsavedata; UiListSelection selection; -} UiListView; - -struct UiColData { - UiListView *listview; - int model_column; - int data_column; }; typedef struct UiTreeEventData { @@ -136,6 +138,7 @@ void ui_update_liststore(GListStore *liststore, UiList *list); void ui_update_liststore_static(GListStore *liststore, char **elm, size_t nelm); +void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index); void ui_listview_update2(UiList *list, int i); UiListSelection ui_listview_getselection2(UiList *list); void ui_listview_setselection2(UiList *list, UiListSelection selection); @@ -155,6 +158,7 @@ GtkWidget* ui_get_tree_widget(UIWIDGET widget); +void ui_listview_update_model(UiModel *model, void *userdata, int insert_index, int delete_index); void ui_listview_update(UiList *list, int i); UiListSelection ui_listview_getselection(UiList *list); void ui_listview_setselection(UiList *list, UiListSelection selection);
--- a/ui/gtk/text.c Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/gtk/text.c Sun Dec 07 14:39:03 2025 +0100 @@ -32,6 +32,7 @@ #include "text.h" #include "container.h" +#include "widget.h" #include <cx/printf.h>
--- a/ui/motif/list.c Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/motif/list.c Sun Dec 07 14:39:03 2025 +0100 @@ -132,6 +132,7 @@ void ui_listview_destroy(Widget w, UiListView *listview, XtPointer d) { ui_listselection_free(listview->current_selection); if(listview->model) { + ui_model_remove_observer(listview->model, listview); ui_model_unref(listview->model); } free(listview);
--- a/ui/ui/tree.h Sun Dec 07 12:24:12 2025 +0100 +++ b/ui/ui/tree.h Sun Dec 07 14:39:03 2025 +0100 @@ -67,6 +67,15 @@ typedef UiBool (*ui_list_savefunc)(UiList *list, int row, int col, UiCellValue *value, void *userdata); +typedef void (*ui_model_update_func)(UiModel *model, void *userdata, int insert_index, int delete_index); + +typedef struct UiModelChangeObserver UiModelChangeObserver; +struct UiModelChangeObserver { + ui_model_update_func update; + void *userdata; + UiModelChangeObserver *next; +}; + struct UiModel { UiContext *ctx; @@ -98,6 +107,12 @@ int *columnsize; /* + * Model change observers, that will be called when + * columns are added or removed + */ + UiModelChangeObserver *observer; + + /* * reference counter */ int ref; @@ -297,10 +312,12 @@ */ UIEXPORT UiModel* ui_model(UiContext *ctx, ...); UIEXPORT UiModel* ui_model_new(UiContext *ctx); -UIEXPORT void ui_model_add_column(UiContext *ctx, UiModel *model, UiModelType type, const char *title, int width); +UIEXPORT void ui_model_add_column(UiModel *model, UiModelType type, const char *title, int width); UIEXPORT UiModel* ui_model_copy(UiContext *ctx, UiModel* model); UIEXPORT void ui_model_ref(UiModel *model); UIEXPORT void ui_model_unref(UiModel *model); +UIEXPORT void ui_model_add_observer(UiModel *model, ui_model_update_func update, void *data); +UIEXPORT void ui_model_remove_observer(UiModel *model, void *data); UIEXPORT void ui_model_free(UiModel *mi); #define ui_listview(obj, ...) ui_listview_create(obj, &(UiListArgs) { __VA_ARGS__ } )