implement dynamic table models (GTK)

Sun, 07 Dec 2025 14:39:03 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 07 Dec 2025 14:39:03 +0100
changeset 965
5d4419042d9b
parent 964
c563220d9aea
child 966
e411ed7c5f10

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__ } )

mercurial