Sun, 07 Dec 2025 15:45:30 +0100
rename combobox to dropdown
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2017 Olaf Wintermann. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdarg.h> #include "../common/context.h" #include "../common/object.h" #include "container.h" #include <cx/array_list.h> #include <cx/linked_list.h> #include "list.h" #include "button.h" #include "icon.h" #include "menu.h" #include "dnd.h" static void* getvalue_wrapper(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) { ui_getvaluefunc getvalue = (ui_getvaluefunc)userdata; return getvalue(elm, col); } static void* str_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) { return elm; } static void* null_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) { return NULL; } /* static GtkTargetEntry targetentries[] = { { "STRING", 0, 0 }, { "text/plain", 0, 1 }, { "text/uri-list", 0, 2 }, }; */ static void listview_copy_static_elements(UiListView *listview, char **elm, size_t nelm) { listview->elements = calloc(nelm, sizeof(char*)); listview->nelm = nelm; for(int i=0;i<nelm;i++) { listview->elements[i] = strdup(elm[i]); } } static UiListView* create_listview(UiObject *obj, UiListArgs *args) { UiListView *tableview = malloc(sizeof(UiListView)); 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; tableview->onselectiondata = args->onselectiondata; tableview->ondragstart = args->ondragstart; tableview->ondragstartdata = args->ondragstartdata; tableview->ondragcomplete = args->ondragcomplete; tableview->ondragcompletedata = args->ondragcompletedata; tableview->ondrop = args->ondrop; tableview->ondropdata = args->ondropdata; tableview->selection.count = 0; tableview->selection.rows = NULL; tableview->current_row = -1; tableview->getstyle = args->getstyle; tableview->getstyledata = args->getstyledata; 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; } else if(args->getvalue) { tableview->getvalue = getvalue_wrapper; tableview->getvaluedata = (void*)args->getvalue; } else { tableview->getvalue = null_getvalue; } return tableview; } #if GTK_CHECK_VERSION(4, 10, 0) /* BEGIN GObject wrapper for generic pointers */ typedef struct _ObjWrapper { GObject parent_instance; void *data; int i; } ObjWrapper; typedef struct _ObjWrapperClass { GObjectClass parent_class; } ObjWrapperClass; G_DEFINE_TYPE(ObjWrapper, obj_wrapper, G_TYPE_OBJECT) static void obj_wrapper_class_init(ObjWrapperClass *klass) { } static void obj_wrapper_init(ObjWrapper *self) { self->data = NULL; } ObjWrapper* obj_wrapper_new(void* data, int i) { ObjWrapper *obj = g_object_new(obj_wrapper_get_type(), NULL); obj->data = data; obj->i = i; return obj; } /* END GObject wrapper for generic pointers */ typedef struct UiCellEntry { GtkEntry *entry; UiListView *listview; char *previous_value; int row; int col; } UiCellEntry; static void cell_save_value(UiCellEntry *data, int restore) { if(data->listview && data->listview->onsave) { UiVar *var = data->listview->var; UiList *list = var ? var->value : NULL; const char *str = ENTRY_GET_TEXT(data->entry); UiCellValue value; value.string = str; value.type = UI_STRING_EDITABLE; if(data->listview->onsave(list, data->row, data->col, &value, data->listview->onsavedata)) { free(data->previous_value); data->previous_value = strdup(str); } else if(restore) { ENTRY_SET_TEXT(data->entry, data->previous_value); } } } static void cell_entry_leave_focus( GtkEventControllerFocus *self, UiCellEntry *data) { // TODO: use a different singal to track focus // we only want to call cell_save_value, when another entry is selected, // not when the window loses focus or something like that cell_save_value(data, TRUE); } static void cell_entry_destroy(GtkWidget *object, UiCellEntry *data) { free(data->previous_value); free(data); } static void cell_entry_unmap(GtkWidget *w, UiCellEntry *data) { const char *text = ENTRY_GET_TEXT(w); cell_save_value(data, FALSE); } static void cell_entry_activate( GtkEntry *self, UiCellEntry *data) { cell_save_value(data, TRUE); } static void column_factory_setup(GtkListItemFactory *factory, GtkListItem *item, gpointer userdata) { UiColData *col = userdata; UiModel *model = col->listview->model; 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(); GtkWidget *label = gtk_label_new(NULL); BOX_ADD(hbox, image); BOX_ADD(hbox, label); gtk_list_item_set_child(item, hbox); g_object_set_data(G_OBJECT(hbox), "image", image); g_object_set_data(G_OBJECT(hbox), "label", label); } else if(type == UI_ICON) { GtkWidget *image = gtk_image_new(); gtk_list_item_set_child(item, image); } else if(type == UI_STRING_EDITABLE) { GtkWidget *textfield = gtk_entry_new(); gtk_widget_add_css_class(textfield, "ui-table-entry"); gtk_list_item_set_child(item, textfield); UiCellEntry *entry_data = malloc(sizeof(UiCellEntry)); entry_data->entry = GTK_ENTRY(textfield); entry_data->listview = NULL; entry_data->previous_value = NULL; entry_data->col = 0; entry_data->row = 0; g_object_set_data(G_OBJECT(textfield), "ui_entry_data", entry_data); g_signal_connect( textfield, "destroy", G_CALLBACK(cell_entry_destroy), entry_data); g_signal_connect( textfield, "activate", G_CALLBACK(cell_entry_activate), entry_data); g_signal_connect( textfield, "unmap", G_CALLBACK(cell_entry_unmap), entry_data); 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 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); } } PangoAttrList* textstyle2pangoattributes(UiTextStyle style) { PangoAttrList *attr = pango_attr_list_new(); if(style.text_style & UI_TEXT_STYLE_BOLD) { pango_attr_list_insert(attr, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); } if(style.text_style & UI_TEXT_STYLE_ITALIC) { pango_attr_list_insert(attr, pango_attr_style_new(PANGO_STYLE_ITALIC)); } if(style.text_style & UI_TEXT_STYLE_UNDERLINE) { pango_attr_list_insert(attr, pango_attr_underline_new(PANGO_UNDERLINE_SINGLE)); } // foreground color, convert from 8bit to 16bit guint16 r = (guint16)style.fg.red * 257; guint16 g = (guint16)style.fg.green * 257; guint16 b = (guint16)style.fg.blue * 257; pango_attr_list_insert(attr, pango_attr_foreground_new(r, g, b)); return attr; } static void column_factory_bind(GtkListItemFactory *unused, GtkListItem *item, gpointer userdata) { 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->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->column] == NULL) { row->bound++; } } else { row = calloc(1, sizeof(UiRowItems) + listview->numcolumns * sizeof(GtkListItem*)); cxMapPut(listview->bound_rows, row_key, row); row->bound = 1; } row->items[col->column] = item; UiBool freevalue = FALSE; void *data = listview->getvalue(list, obj->data, obj->i, datacolumn, listview->getvaluedata, &freevalue); GtkWidget *child = gtk_list_item_get_child(item); PangoAttrList *attributes = NULL; UiTextStyle style = { 0, 0 }; if(listview->getstyle) { // query current row style, if it wasn't already queried if(obj->i != listview->current_row) { listview->current_row = obj->i; listview->row_style = (UiTextStyle){ 0, 0 }; listview->apply_row_style = listview->getstyle(list, obj->data, obj->i, -1, listview->getstyledata, &listview->row_style); style = listview->row_style; if(listview->apply_row_style) { pango_attr_list_unref(listview->current_row_attributes); listview->current_row_attributes = textstyle2pangoattributes(style); } } 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 } // get the column style if(listview->getstyle(list, obj->data, obj->i, style_col, listview->getstyledata, &style)) { attributes = textstyle2pangoattributes(style); } else if(listview->apply_row_style) { attributes = listview->current_row_attributes; } } switch(type) { case UI_STRING_FREE: { freevalue = TRUE; } case UI_STRING: { gtk_label_set_label(GTK_LABEL(child), data); if(freevalue) { free(data); } gtk_label_set_attributes(GTK_LABEL(child), attributes); break; } case UI_INTEGER: { intptr_t intvalue = (intptr_t)data; char buf[32]; snprintf(buf, 32, "%d", (int)intvalue); gtk_label_set_label(GTK_LABEL(child), buf); gtk_label_set_attributes(GTK_LABEL(child), attributes); break; } case UI_ICON: { UiIcon *icon = data; if(icon) { gtk_image_set_from_paintable(GTK_IMAGE(child), GDK_PAINTABLE(icon->info)); } break; } case UI_ICON_TEXT: { } case UI_ICON_TEXT_FREE: { void *data2 = listview->getvalue(list, obj->data, obj->i, datacolumn+1, listview->getvaluedata, &freevalue); if(type == UI_ICON_TEXT_FREE) { freevalue = TRUE; } GtkWidget *image = g_object_get_data(G_OBJECT(child), "image"); GtkWidget *label = g_object_get_data(G_OBJECT(child), "label"); if(data && image) { UiIcon *icon = data; gtk_image_set_from_paintable(GTK_IMAGE(image), GDK_PAINTABLE(icon->info)); } if(data2 && label) { gtk_label_set_label(GTK_LABEL(label), data2); gtk_label_set_attributes(GTK_LABEL(label), attributes); } if(freevalue) { free(data2); } break; } case UI_STRING_EDITABLE: { UiCellEntry *entry = g_object_get_data(G_OBJECT(child), "ui_entry_data"); if(entry) { entry->listview = col->listview; entry->row = obj->i; entry->col = datacolumn; entry->previous_value = strdup(data); } 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) { pango_attr_list_unref(attributes); } } static void column_factory_unbind(GtkSignalListItemFactory *self, GtkListItem *item, UiColData *col) { ObjWrapper *obj = gtk_list_item_get_item(item); UiListView *listview = col->listview; CxHashKey row_key = cx_hash_key(&obj->i, sizeof(int)); UiRowItems *row = cxMapGet(listview->bound_rows, row_key); if(row) { row->items[col->column] = NULL; row->bound--; if(row->bound == 0) { cxMapRemove(listview->bound_rows, row_key); } } // else: should not happen GtkWidget *child = gtk_list_item_get_child(item); UiCellEntry *entry = g_object_get_data(G_OBJECT(child), "ui_entry_data"); if(entry) { cell_save_value(entry, FALSE); entry->listview = NULL; free(entry->previous_value); entry->previous_value = NULL; } else if(GTK_IS_CHECK_BUTTON(child)) { } } static GtkSelectionModel* create_selection_model(UiListView *listview, GListStore *liststore, bool multiselection) { GtkSelectionModel *selection_model; if(multiselection) { selection_model = GTK_SELECTION_MODEL(gtk_multi_selection_new(G_LIST_MODEL(liststore))); } else { selection_model = GTK_SELECTION_MODEL(gtk_single_selection_new(G_LIST_MODEL(liststore))); gtk_single_selection_set_can_unselect(GTK_SINGLE_SELECTION(selection_model), TRUE); gtk_single_selection_set_autoselect(GTK_SINGLE_SELECTION(selection_model), FALSE); } g_signal_connect(selection_model, "selection-changed", G_CALLBACK(ui_listview_selection_changed), listview); return selection_model; } UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) { // 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); args->model = model; GListStore *ls = g_list_store_new(G_TYPE_OBJECT); UiListView *listview = create_listview(obj, args); if(!args->getvalue && !args->getvalue2) { listview->getvalue = str_getvalue; } listview->numcolumns = 1; 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->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); UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; listview->var = var; listview->liststore = ls; listview->selectionmodel = selection_model; g_signal_connect( view, "destroy", G_CALLBACK(ui_listview_destroy), listview); // bind listview to list if(var && var->value) { UiList *list = var->value; list->obj = listview; list->update = ui_listview_update2; list->getselection = ui_listview_getselection2; list->setselection = ui_listview_setselection2; ui_update_liststore(ls, list); } else if (args->static_elements && args->static_nelm > 0) { listview_copy_static_elements(listview, args->static_elements, args->static_nelm); listview->getvalue = str_getvalue; // force string values ui_update_liststore_static(ls, listview->elements, listview->nelm); } // event handling if(args->onactivate) { // columnview and listview can use the same callback function, because // the first parameter (which is technically a different pointer type) // is ignored g_signal_connect(view, "activate", G_CALLBACK(ui_columnview_activate), listview); } if(args->contextmenu) { UIMENU menu = ui_contextmenu_create(args->contextmenu, obj, view); ui_widget_set_contextmenu(view, menu); } // add widget to parent GtkWidget *scroll_area = SCROLLEDWINDOW_NEW(); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scroll_area), GTK_POLICY_AUTOMATIC, 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); } UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; UiLayout layout = UI_ARGS2LAYOUT(args); ct->add(ct, scroll_area, &layout); return scroll_area; } UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) { // 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); args->model = model; GListStore *ls = g_list_store_new(G_TYPE_OBJECT); UiListView *listview = create_listview(obj, args); if(!args->getvalue && !args->getvalue2) { listview->getvalue = str_getvalue; } listview->numcolumns = 1; 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->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); if(args->width > 0) { gtk_widget_set_size_request(view, args->width, -1); } UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; listview->var = var; listview->liststore = ls; listview->selectionmodel = NULL; g_signal_connect( view, "destroy", G_CALLBACK(ui_listview_destroy), listview); // bind listview to list if(var && var->value) { UiList *list = var->value; list->obj = listview; list->update = ui_listview_update2; list->getselection = ui_dropdown_getselection; list->setselection = ui_dropdown_setselection; ui_update_liststore(ls, list); } else if (args->static_elements && args->static_nelm > 0) { listview_copy_static_elements(listview, args->static_elements, args->static_nelm); listview->getvalue = str_getvalue; // force string values ui_update_liststore_static(ls, listview->elements, listview->nelm); } // event handling if(args->onactivate) { g_signal_connect(view, "notify::selected", G_CALLBACK(ui_dropdown_notify), listview); } // add widget to parent UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; UiLayout layout = UI_ARGS2LAYOUT(args); ct->add(ct, view, &layout); return view; } void ui_listview_select(UIWIDGET listview, int index) { GtkSelectionModel *model = gtk_list_view_get_model(GTK_LIST_VIEW(listview)); gtk_selection_model_select_item(model, index, TRUE); } void ui_dropdown_select(UIWIDGET dropdown, int index) { 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); // create obj to store all relevant data we need for handling events // and list updates UiListView *tableview = create_listview(obj, args); 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, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init tableview tableview->widget = view; tableview->var = var; tableview->liststore = ls; tableview->selectionmodel = selection_model; g_signal_connect( view, "destroy", G_CALLBACK(ui_listview_destroy), tableview); // create columns from UiModel UiModel *model = args->model; int columns = 0; if(model) { columns = model->columns; ui_model_add_observer(model, ui_listview_update_model, tableview); } tableview->columns = calloc(columns, sizeof(int)); tableview->numcolumns = columns; tableview->bound_rows = cxHashMapCreate(NULL, CX_STORE_POINTERS, 128); tableview->bound_rows->collection.simple_destructor = (cx_destructor_func)free; int addi = 0; for(int i=0;i<columns;i++) { 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++; } add_column(tableview, i); } // bind listview to list if(var && var->value) { UiList *list = var->value; list->obj = tableview; list->update = ui_listview_update2; list->getselection = ui_listview_getselection2; list->setselection = ui_listview_setselection2; ui_update_liststore(ls, list); } // event handling if(args->onactivate) { g_signal_connect(view, "activate", G_CALLBACK(ui_columnview_activate), tableview); } if(args->contextmenu) { UIMENU menu = ui_contextmenu_create(args->contextmenu, obj, view); ui_widget_set_contextmenu(view, menu); } // add widget to parent GtkWidget *scroll_area = SCROLLEDWINDOW_NEW(); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scroll_area), GTK_POLICY_AUTOMATIC, 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); } UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; UiLayout layout = UI_ARGS2LAYOUT(args); ct->add(ct, scroll_area, &layout); 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); int n = gtk_bitset_get_size(bitset); printf("bitset %d\n", n); gtk_bitset_unref(bitset); return sel; } static void listview_event(ui_callback cb, void *cbdata, UiListView *view) { UiEvent event; event.obj = view->obj; event.document = event.obj->ctx->document; event.window = event.obj->window; event.intval = view->selection.count; event.eventdata = &view->selection; event.eventdatatype = UI_EVENT_DATA_LIST_SELECTION; event.set = ui_get_setop(); if(cb) { cb(&event, cbdata); } } static void listview_update_selection(UiListView *view) { free(view->selection.rows); view->selection.count = 0; view->selection.rows = NULL; CX_ARRAY_DECLARE(int, newselection); cx_array_initialize(newselection, 8); size_t nitems = g_list_model_get_n_items(G_LIST_MODEL(view->liststore)); for(size_t i=0;i<nitems;i++) { if(gtk_selection_model_is_selected(view->selectionmodel, i)) { int s = (int)i; cx_array_simple_add(newselection, s); } } if(newselection_size > 0) { view->selection.count = newselection_size; view->selection.rows = newselection; } else { free(newselection); } } void ui_dropdown_notify(GtkWidget *dropdown, GObject *pspec, gpointer userdata) { UiListView *view = userdata; guint index = gtk_drop_down_get_selected(GTK_DROP_DOWN(dropdown)); GObject *item = gtk_drop_down_get_selected_item(GTK_DROP_DOWN(dropdown)); if(item && view->onactivate) { ObjWrapper *eventdata = (ObjWrapper*)item; UiEvent event; event.obj = view->obj; event.document = event.obj->ctx->document; event.window = event.obj->window; event.intval = index; event.eventdata = eventdata->data; event.eventdatatype = UI_EVENT_DATA_LIST_ELM; event.set = ui_get_setop(); view->onactivate(&event, view->onactivatedata); } } void ui_columnview_activate(void *ignore, guint position, gpointer userdata) { UiListView *view = userdata; if(view->selection.count == 0) { listview_update_selection(view); } listview_event(view->onactivate, view->onactivatedata, view); } void ui_listview_selection_changed(GtkSelectionModel* self, guint position, guint n_items, gpointer userdata) { UiListView *view = userdata; listview_update_selection(view); listview_event(view->onselection, view->onselectiondata, view); } void ui_dropdown_activate(GtkDropDown* self, gpointer userdata) { UiListView *view = userdata; guint selection = gtk_drop_down_get_selected(GTK_DROP_DOWN(view->widget)); UiListSelection sel = { 0, NULL }; int sel2 = (int)selection; if(selection != GTK_INVALID_LIST_POSITION) { sel.count = 1; sel.rows = &sel2; } if(view->onactivate) { UiEvent event; event.obj = view->obj; event.document = event.obj->ctx->document; event.window = event.obj->window; event.intval = view->selection.count; event.eventdata = &view->selection; event.eventdatatype = UI_EVENT_DATA_LIST_SELECTION; event.set = ui_get_setop(); view->onactivate(&event, view->onactivatedata); } } void ui_update_liststore(GListStore *liststore, UiList *list) { g_list_store_remove_all(liststore); int i = 0; void *elm = list->first(list); while(elm) { ObjWrapper *obj = obj_wrapper_new(elm, i++); g_list_store_append(liststore, obj); elm = list->next(list); } } void ui_update_liststore_static(GListStore *liststore, char **elm, size_t nelm) { g_list_store_remove_all(liststore); for(int i=0;i<nelm;i++) { ObjWrapper *obj = obj_wrapper_new(elm[i], i); g_list_store_append(liststore, obj); } } void ui_listview_update2(UiList *list, int i) { UiListView *view = list->obj; view->current_row = -1; if(i < 0) { ui_update_liststore(view->liststore, list); } else { void *value = list->get(list, i); if(value) { ObjWrapper *obj = g_list_model_get_item(G_LIST_MODEL(view->liststore), i); if(obj) { obj->data = value; } CxHashKey row_key = cx_hash_key(&i, sizeof(int)); UiRowItems *row = cxMapGet(view->bound_rows, row_key); if(row) { UiColData coldata; coldata.listview = view; for(int c=0;c<view->numcolumns;c++) { if(row->items[c] != NULL) { coldata.column = c; column_factory_bind(NULL, row->items[c], &coldata); } } } } } } UiListSelection ui_listview_getselection2(UiList *list) { UiListView *view = list->obj; UiListSelection selection; selection.count = view->selection.count; selection.rows = calloc(selection.count, sizeof(int)); memcpy(selection.rows, view->selection.rows, selection.count*sizeof(int)); return selection; } void ui_listview_setselection2(UiList *list, UiListSelection selection) { ui_setop_enable(TRUE); UiListView *view = list->obj; UiListSelection newselection; newselection.count = view->selection.count; if(selection.count > 0) { newselection.rows = calloc(newselection.count, sizeof(int)); memcpy(newselection.rows, selection.rows, selection.count*sizeof(int)); } else { newselection.rows = NULL; } free(view->selection.rows); view->selection = newselection; gtk_selection_model_unselect_all(view->selectionmodel); if(selection.count > 0) { for(int i=0;i<selection.count;i++) { gtk_selection_model_select_item(view->selectionmodel, selection.rows[i], FALSE); } } ui_setop_enable(FALSE); } UiListSelection ui_dropdown_getselection(UiList *list) { UiListView *view = list->obj; guint selection = gtk_drop_down_get_selected(GTK_DROP_DOWN(view->widget)); UiListSelection sel = { 0, NULL }; if(selection != GTK_INVALID_LIST_POSITION) { sel.count = 1; sel.rows = malloc(sizeof(int)); sel.rows[0] = (int)selection; } return sel; } void ui_dropdown_setselection(UiList *list, UiListSelection selection) { ui_setop_enable(TRUE); UiListView *view = list->obj; if(selection.count > 0) { gtk_drop_down_set_selected(GTK_DROP_DOWN(view->widget), selection.rows[0]); } else { gtk_drop_down_set_selected(GTK_DROP_DOWN(view->widget), GTK_INVALID_LIST_POSITION); } ui_setop_enable(FALSE); } #else static void update_list_row(UiListView *listview, GtkListStore *store, GtkTreeIter *iter, UiList *list, void *elm, int row) { UiModel *model = listview->model; ui_getstylefunc getstyle = listview->getstyle; // get the row style UiBool style_set = FALSE; UiTextStyle style = { 0, 0 }; if(getstyle) { style_set = getstyle(list, elm, row, -1, listview->getstyledata, &style); } // set column values int c = 0; for(int i=0;i<model->columns;i++,c++) { UiBool freevalue = FALSE; void *data = listview->getvalue(list, elm, row, c, listview->getvaluedata, &freevalue); UiModelType type = model->types[i]; if(getstyle) { // in case the column is icon+text, only get a style for the text column int style_col = c; if(type == UI_ICON_TEXT || type == UI_ICON_TEXT_FREE) { style_col++; } // Get the individual column style // The column style overrides the row style, however if no column style // is provided, we stick with the row style if(getstyle(list, elm, row, style_col, listview->getstyledata, &style)) { style_set = TRUE; } } GValue value = G_VALUE_INIT; switch(type) { case UI_STRING_FREE: { freevalue = TRUE; } case UI_STRING: { g_value_init(&value, G_TYPE_STRING); g_value_set_string(&value, data); if(freevalue) { free(data); } break; } case UI_INTEGER: { g_value_init(&value, G_TYPE_INT); intptr_t intptr = (intptr_t)data; g_value_set_int(&value, (int)intptr); break; } case UI_ICON: { g_value_init(&value, G_TYPE_OBJECT); UiIcon *icon = data; #if GTK_MAJOR_VERSION >= 4 g_value_set_object(&value, icon->info); // TODO: does this work? #else if(!icon->pixbuf && icon->info) { GError *error = NULL; GdkPixbuf *pixbuf = gtk_icon_info_load_icon(icon->info, &error); icon->pixbuf = pixbuf; } if(icon->pixbuf) { g_value_set_object(&value, icon->pixbuf); } #endif break; } case UI_ICON_TEXT: case UI_ICON_TEXT_FREE: { UiIcon *icon = data; #if GTK_MAJOR_VERSION >= 4 if(icon) { GValue iconvalue = G_VALUE_INIT; g_value_init(&iconvalue, G_TYPE_OBJECT); g_value_set_object(&iconvalue, ui_icon_pixbuf(icon)); gtk_list_store_set_value(store, &iter, c, &iconvalue); } #else GValue pixbufvalue = G_VALUE_INIT; if(icon) { if(!icon->pixbuf && icon->info) { GError *error = NULL; GdkPixbuf *pixbuf = gtk_icon_info_load_icon(icon->info, &error); icon->pixbuf = pixbuf; } g_value_init(&pixbufvalue, G_TYPE_OBJECT); g_value_set_object(&pixbufvalue, icon->pixbuf); gtk_list_store_set_value(store, iter, c, &pixbufvalue); } #endif c++; freevalue = FALSE; char *str = listview->getvalue(list, elm, row, c, listview->getvaluedata, &freevalue); g_value_init(&value, G_TYPE_STRING); g_value_set_string(&value, str); if(model->types[i] == UI_ICON_TEXT_FREE || freevalue) { free(str); } break; } } gtk_list_store_set_value(store, iter, c, &value); if(style_set) { int soff = listview->style_offset + i*6; GValue style_set_value = G_VALUE_INIT; g_value_init(&style_set_value, G_TYPE_BOOLEAN); g_value_set_boolean(&style_set_value, TRUE); gtk_list_store_set_value(store, iter, soff, &style_set_value); GValue style_weight_value = G_VALUE_INIT; g_value_init(&style_weight_value, G_TYPE_INT); if(style.text_style & UI_TEXT_STYLE_BOLD) { g_value_set_int(&style_weight_value, 600); } else { g_value_set_int(&style_weight_value, 400); } gtk_list_store_set_value(store, iter, soff + 1, &style_weight_value); GValue style_underline_value = G_VALUE_INIT; g_value_init(&style_underline_value, G_TYPE_INT); if(style.text_style & UI_TEXT_STYLE_UNDERLINE) { g_value_set_int(&style_underline_value, PANGO_UNDERLINE_SINGLE); } else { g_value_set_int(&style_underline_value, PANGO_UNDERLINE_NONE); } gtk_list_store_set_value(store, iter, soff + 2, &style_underline_value); GValue style_italic_value = G_VALUE_INIT; g_value_init(&style_italic_value, G_TYPE_INT); if(style.text_style & UI_TEXT_STYLE_ITALIC) { g_value_set_int(&style_italic_value, PANGO_STYLE_ITALIC); } else { g_value_set_int(&style_italic_value, PANGO_STYLE_NORMAL); } gtk_list_store_set_value(store, iter, soff + 3, &style_italic_value); GValue style_fgset_value = G_VALUE_INIT; g_value_init(&style_fgset_value, G_TYPE_BOOLEAN); g_value_set_boolean(&style_fgset_value, style.fg_set); gtk_list_store_set_value(store, iter, soff + 4, &style_fgset_value); if(style.fg_set) { char buf[8]; snprintf(buf, 8, "#%02X%02X%02X", (int)style.fg.red, (int)style.fg.green, (int)style.fg.blue); GValue style_fg_value = G_VALUE_INIT; g_value_init(&style_fg_value, G_TYPE_STRING); g_value_set_string(&style_fg_value, buf); gtk_list_store_set_value(store, iter, soff + 5, &style_fg_value); } } } } static GtkListStore* create_list_store(UiListView *listview, UiList *list) { UiModel *model = listview->model; int columns = model->columns; GType *types = calloc(columns*8, sizeof(GType)); int c = 0; for(int i=0;i<columns;i++,c++) { switch(model->types[i]) { case UI_STRING: case UI_STRING_FREE: types[c] = G_TYPE_STRING; break; case UI_INTEGER: types[c] = G_TYPE_INT; break; case UI_ICON: types[c] = G_TYPE_OBJECT; break; case UI_ICON_TEXT: case UI_ICON_TEXT_FREE: { types[c] = G_TYPE_OBJECT; types[++c] = G_TYPE_STRING; } } } int s = 0; for(int i=0;i<columns;i++) { types[listview->style_offset+s] = G_TYPE_BOOLEAN; s++; // *-set types[listview->style_offset+s] = G_TYPE_INT; s++; // weight types[listview->style_offset+s] = G_TYPE_INT; s++; // underline types[listview->style_offset+s] = G_TYPE_INT; s++; // style types[listview->style_offset+s] = G_TYPE_BOOLEAN; s++; // foreground-set types[listview->style_offset+s] = G_TYPE_STRING; s++; // foreground } GtkListStore *store = gtk_list_store_newv(c+s, types); free(types); if(list) { void *elm = list->first(list); int i = 0; while(elm) { // insert new row GtkTreeIter iter; gtk_list_store_insert (store, &iter, -1); update_list_row(listview, store, &iter, list, elm, i++); // next row elm = list->next(list); } } return store; } UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) { // create treeview GtkWidget *view = gtk_tree_view_new(); ui_set_name_and_style(view, args->name, args->style_class); ui_set_widget_groups(obj->ctx, view, args->groups); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(NULL, renderer, "text", 0, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), FALSE); #ifdef UI_GTK3 #if GTK_MINOR_VERSION >= 8 //gtk_tree_view_set_activate_on_single_click(GTK_TREE_VIEW(view), TRUE); #else // TODO: implement for older gtk3 #endif #else // TODO: implement for gtk2 #endif UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1); UiListView *listview = create_listview(obj, args); listview->style_offset = 1; if(!args->getvalue && !args->getvalue2) { listview->getvalue = str_getvalue; } listview->model = model; g_signal_connect( view, "destroy", G_CALLBACK(ui_listview_destroy), listview); UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST); // init listview listview->widget = view; listview->var = var; UiList *list = var ? var->value : NULL; GtkListStore *listmodel = create_list_store(listview, list); gtk_tree_view_set_model(GTK_TREE_VIEW(view), GTK_TREE_MODEL(listmodel)); g_object_unref(listmodel); // bind var list->update = ui_listview_update; list->getselection = ui_listview_getselection; list->setselection = ui_listview_setselection; list->obj = listview; // add callback UiTreeEventData *event = malloc(sizeof(UiTreeEventData)); event->obj = obj; event->activate = args->onactivate; event->activatedata = args->onactivatedata; event->selection = args->onselection; event->selectiondata = args->onselectiondata; g_signal_connect( view, "destroy", G_CALLBACK(ui_destroy_userdata), event); if(args->onactivate) { g_signal_connect( view, "row-activated", G_CALLBACK(ui_listview_activate_event), event); } if(args->onselection) { GtkTreeSelection *selection = gtk_tree_view_get_selection( GTK_TREE_VIEW(view)); g_signal_connect( selection, "changed", G_CALLBACK(ui_listview_selection_event), event); } if(args->contextmenu) { UIMENU menu = ui_contextmenu_create(args->contextmenu, obj, view); ui_widget_set_contextmenu(view, menu); } // add widget to the current container GtkWidget *scroll_area = SCROLLEDWINDOW_NEW(); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scroll_area), GTK_POLICY_AUTOMATIC, 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); } UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; UiLayout layout = UI_ARGS2LAYOUT(args); ct->add(ct, scroll_area, &layout); return scroll_area; } void ui_listview_select(UIWIDGET listview, int index) { GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview)); GtkTreePath *path = gtk_tree_path_new_from_indicesv(&index, 1); gtk_tree_selection_select_path(sel, path); //g_object_unref(path); } void ui_dropdown_select(UIWIDGET dropdown, int index) { gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), index); } UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) { // create treeview GtkWidget *view = gtk_tree_view_new(); UiModel *model = args->model; int columns = model ? model->columns : 0; // find the last data column index int addi = 0; int style_offset = 0; int i = 0; for(;i<columns;i++) { if(model->types[i] == UI_ICON_TEXT || model->types[i] == UI_ICON_TEXT_FREE) { addi++; } } style_offset = i+addi; // create columns and init cell renderers addi = 0; for(i=0;i<columns;i++) { GtkTreeViewColumn *column = NULL; if(model->types[i] == UI_ICON_TEXT || model->types[i] == UI_ICON_TEXT_FREE) { column = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(column, model->titles[i]); GtkCellRenderer *iconrenderer = gtk_cell_renderer_pixbuf_new(); GtkCellRenderer *textrenderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_end(column, textrenderer, TRUE); gtk_tree_view_column_pack_start(column, iconrenderer, FALSE); gtk_tree_view_column_add_attribute(column, iconrenderer, "pixbuf", addi + i); gtk_tree_view_column_add_attribute(column, textrenderer, "text", addi + i+1); if(args->getstyle) { int soff = style_offset + i*6; gtk_tree_view_column_add_attribute(column, textrenderer, "weight-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "underline-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "style-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "weight", soff + 1); gtk_tree_view_column_add_attribute(column, textrenderer, "underline", soff + 2); gtk_tree_view_column_add_attribute(column, textrenderer, "style", soff + 3); gtk_tree_view_column_add_attribute(column, textrenderer, "foreground-set", soff + 4); gtk_tree_view_column_add_attribute(column, textrenderer, "foreground", soff + 5); } addi++; } else if (model->types[i] == UI_ICON) { GtkCellRenderer *iconrenderer = gtk_cell_renderer_pixbuf_new(); column = gtk_tree_view_column_new_with_attributes( model->titles[i], iconrenderer, "pixbuf", i + addi, NULL); } else { GtkCellRenderer *textrenderer = gtk_cell_renderer_text_new(); column = gtk_tree_view_column_new_with_attributes( model->titles[i], textrenderer, "text", i + addi, NULL); if(args->getstyle) { int soff = style_offset + i*6; gtk_tree_view_column_add_attribute(column, textrenderer, "weight-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "underline-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "style-set", soff); gtk_tree_view_column_add_attribute(column, textrenderer, "weight", soff + 1); gtk_tree_view_column_add_attribute(column, textrenderer, "underline", soff + 2); gtk_tree_view_column_add_attribute(column, textrenderer, "style", soff + 3); gtk_tree_view_column_add_attribute(column, textrenderer, "foreground-set", soff + 4); gtk_tree_view_column_add_attribute(column, textrenderer, "foreground", soff + 5); } } int colsz = model->columnsize[i]; if(colsz > 0) { gtk_tree_view_column_set_fixed_width(column, colsz); } else if(colsz < 0) { gtk_tree_view_column_set_expand(column, TRUE); } gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); } //gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), FALSE); #ifdef UI_GTK3 //gtk_tree_view_set_activate_on_single_click(GTK_TREE_VIEW(view), TRUE); #else #endif 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); // add TreeView as observer to the UiList to update the TreeView if the // data changes UiListView *tableview = create_listview(obj, args); tableview->widget = view; tableview->style_offset = style_offset; g_signal_connect( view, "destroy", G_CALLBACK(ui_listview_destroy), tableview); UiList *list = var ? var->value : NULL; GtkListStore *listmodel = create_list_store(tableview, list); gtk_tree_view_set_model(GTK_TREE_VIEW(view), GTK_TREE_MODEL(listmodel)); g_object_unref(listmodel); // bind var list->update = ui_listview_update; list->getselection = ui_listview_getselection; list->setselection = ui_listview_setselection; list->obj = tableview; // add callback UiTreeEventData *event = ui_malloc(obj->ctx, sizeof(UiTreeEventData)); event->obj = obj; event->activate = args->onactivate; event->selection = args->onselection; event->activatedata = args->onactivatedata; event->selectiondata = args->onselectiondata; if(args->onactivate) { g_signal_connect( view, "row-activated", G_CALLBACK(ui_listview_activate_event), event); } if(args->onselection) { GtkTreeSelection *selection = gtk_tree_view_get_selection( GTK_TREE_VIEW(view)); g_signal_connect( selection, "changed", G_CALLBACK(ui_listview_selection_event), event); } // TODO: destroy callback if(args->ondragstart) { ui_listview_add_dnd(tableview, args); } if(args->ondrop) { ui_listview_enable_drop(tableview, args); } GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(view)); if(args->multiselection) { gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE); } // add widget to the current container GtkWidget *scroll_area = SCROLLEDWINDOW_NEW(); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scroll_area), GTK_POLICY_AUTOMATIC, 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 ui_widget_set_contextmenu(scroll_area, menu); #else ui_widget_set_contextmenu(view, menu); #endif } UiContainerPrivate *ct = (UiContainerPrivate*)obj->container_end; UiLayout layout = UI_ARGS2LAYOUT(args); ct->add(ct, scroll_area, &layout); return scroll_area; } void ui_listview_update(UiList *list, int i) { UiListView *view = list->obj; if(i < 0) { GtkListStore *store = create_list_store(view, list); gtk_tree_view_set_model(GTK_TREE_VIEW(view->widget), GTK_TREE_MODEL(store)); g_object_unref(G_OBJECT(store)); } else { void *elm = list->get(list, i); GtkTreeModel *store = gtk_tree_view_get_model(GTK_TREE_VIEW(view->widget)); GtkTreeIter iter; if(gtk_tree_model_iter_nth_child(store, &iter, NULL, i)) { update_list_row(view, GTK_LIST_STORE(store), &iter, list, elm, i); } } } UiListSelection ui_listview_getselection(UiList *list) { UiListView *view = list->obj; UiListSelection selection = ui_listview_selection( gtk_tree_view_get_selection(GTK_TREE_VIEW(view->widget)), NULL); return selection; } void ui_listview_setselection(UiList *list, UiListSelection selection) { ui_setop_enable(TRUE); UiListView *view = list->obj; GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(view->widget)); GtkTreePath *path = gtk_tree_path_new_from_indicesv(selection.rows, selection.count); gtk_tree_selection_select_path(sel, path); //g_object_unref(path); ui_setop_enable(FALSE); } /* --------------------------- ComboBox --------------------------- */ UIWIDGET ui_dropdown_create(UiObject *obj, UiListArgs *args) { 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); 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; listview->style_offset = 1; listview->model = ui_model(obj->ctx, UI_STRING, "", -1); g_signal_connect( combobox, "destroy", G_CALLBACK(ui_listview_destroy), listview); 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) { listview->var = var; list->update = ui_combobox_modelupdate; list->getselection = ui_dropdown_getselection; list->setselection = ui_dropdown_setselection; list->obj = listview; list->update(list, -1); } else if(args->static_nelm > 0) { listview_copy_static_elements(listview, args->static_elements, args->static_nelm); for(int i=0;i<args->static_nelm;i++) { GtkTreeIter iter; GValue value = G_VALUE_INIT; gtk_list_store_insert(listmodel, &iter, -1); g_value_init(&value, G_TYPE_STRING); g_value_set_string(&value, listview->elements[i]); gtk_list_store_set_value(listmodel, &iter, 0, &value); } } if(listmodel) { gtk_combo_box_set_model(GTK_COMBO_BOX(combobox), GTK_TREE_MODEL(listmodel)); g_object_unref(listmodel); } GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combobox), renderer, TRUE); gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT(combobox), renderer, "text", 0, NULL); gtk_combo_box_set_active(GTK_COMBO_BOX(combobox), 0); // add callback if(args->onactivate) { UiEventData *event = ui_malloc(obj->ctx, sizeof(UiEventData)); event->obj = obj; event->userdata = args->onactivatedata; event->callback = args->onactivate; event->value = 0; event->customdata = listview; g_signal_connect( combobox, "changed", G_CALLBACK(ui_combobox_change_event), event); } return combobox; } void ui_combobox_change_event(GtkComboBox *widget, UiEventData *e) { int index = gtk_combo_box_get_active(widget); UiListView *listview = e->customdata; void *eventdata = NULL; if(listview->var && listview->var->value) { UiList *list = listview->var->value; eventdata = ui_list_get(list, index); } else if(listview->elements && listview->nelm > index) { eventdata = listview->elements[index]; } UiEvent event; event.obj = e->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = eventdata; event.intval = index; event.set = ui_get_setop(); e->callback(&event, e->userdata); } void ui_combobox_modelupdate(UiList *list, int i) { UiListView *view = list->obj; GtkListStore *store = create_list_store(view, list); gtk_combo_box_set_model(GTK_COMBO_BOX(view->widget), GTK_TREE_MODEL(store)); g_object_unref(store); } UiListSelection ui_dropdown_getselection(UiList *list) { UiListView *combobox = list->obj; UiListSelection ret; ret.rows = malloc(sizeof(int*)); ret.count = 1; ret.rows[0] = gtk_combo_box_get_active(GTK_COMBO_BOX(combobox->widget)); return ret; } void ui_dropdown_setselection(UiList *list, UiListSelection selection) { ui_setop_enable(TRUE); UiListView *combobox = list->obj; if(selection.count > 0) { gtk_combo_box_set_active(GTK_COMBO_BOX(combobox->widget), selection.rows[0]); } ui_setop_enable(FALSE); } void ui_listview_activate_event( GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, UiTreeEventData *event) { UiListSelection selection = ui_listview_selection( gtk_tree_view_get_selection(treeview), event); UiEvent e; e.obj = event->obj; e.window = event->obj->window; e.document = event->obj->ctx->document; e.eventdata = &selection; e.intval = selection.count > 0 ? selection.rows[0] : -1; e.set = ui_get_setop(); event->activate(&e, event->activatedata); if(selection.count > 0) { free(selection.rows); } } void ui_listview_selection_event( GtkTreeSelection *treeselection, UiTreeEventData *event) { UiListSelection selection = ui_listview_selection(treeselection, event); UiEvent e; e.obj = event->obj; e.window = event->obj->window; e.document = event->obj->ctx->document; e.eventdata = &selection; e.intval = selection.count > 0 ? selection.rows[0] : -1; e.set = ui_get_setop(); event->selection(&e, event->selectiondata); if(selection.count > 0) { free(selection.rows); } } UiListSelection ui_listview_selection( GtkTreeSelection *selection, UiTreeEventData *event) { GList *rows = gtk_tree_selection_get_selected_rows(selection, NULL); UiListSelection ls; ls.count = g_list_length(rows); ls.rows = calloc(ls.count, sizeof(int)); GList *r = rows; int i = 0; while(r) { GtkTreePath *path = r->data; ls.rows[i] = ui_tree_path_list_index(path); r = r->next; i++; } return ls; } int ui_tree_path_list_index(GtkTreePath *path) { int depth = gtk_tree_path_get_depth(path); if(depth == 0) { fprintf(stderr, "UiError: treeview selection: depth == 0\n"); return -1; } int *indices = gtk_tree_path_get_indices(path); return indices[depth - 1]; } #endif #if GTK_MAJOR_VERSION >= 4 static GdkContentProvider *ui_listview_dnd_prepare(GtkDragSource *source, double x, double y, void *data) { //printf("drag prepare\n"); UiListView *listview = data; UiDnD *dnd = ui_create_dnd(); GdkContentProvider *provider = NULL; if(listview->ondragstart) { UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = dnd; event.eventdatatype = UI_EVENT_DATA_DND; event.intval = 0; event.set = ui_get_setop(); listview->ondragstart(&event, listview->ondragstartdata); } size_t numproviders = cxListSize(dnd->providers); if(numproviders > 0) { GdkContentProvider **providers = (GdkContentProvider**)cxListAt(dnd->providers, 0); provider = gdk_content_provider_new_union(providers, numproviders); } ui_dnd_free(dnd); return provider; } static void ui_listview_drag_begin(GtkDragSource *self, GdkDrag *drag, gpointer userdata) { //printf("drag begin\n"); } static void ui_listview_drag_end(GtkDragSource *self, GdkDrag *drag, gboolean delete_data, gpointer user_data) { //printf("drag end\n"); UiListView *listview = user_data; if(listview->ondragcomplete) { UiDnD dnd; dnd.target = NULL; dnd.value = NULL; dnd.providers = NULL; dnd.selected_action = gdk_drag_get_selected_action(drag); dnd.delete = delete_data; dnd.accept = FALSE; UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &dnd; event.eventdatatype = UI_EVENT_DATA_DND; event.intval = 0; event.set = ui_get_setop(); listview->ondragcomplete(&event, listview->ondragcompletedata); } } static gboolean ui_listview_drop( GtkDropTarget *target, const GValue* value, gdouble x, gdouble y, gpointer user_data) { UiListView *listview = user_data; UiDnD dnd; dnd.providers = NULL; dnd.target = target; dnd.value = value; dnd.selected_action = 0; dnd.delete = FALSE; dnd.accept = FALSE; if(listview->ondrop) { dnd.accept = TRUE; UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &dnd; event.eventdatatype = UI_EVENT_DATA_DND; event.intval = 0; event.set = ui_get_setop(); listview->ondrop(&event, listview->ondropdata); } return dnd.accept; } void ui_listview_add_dnd(UiListView *listview, UiListArgs *args) { GtkDragSource *dragsource = gtk_drag_source_new(); gtk_widget_add_controller(listview->widget, GTK_EVENT_CONTROLLER(dragsource)); g_signal_connect (dragsource, "prepare", G_CALLBACK (ui_listview_dnd_prepare), listview); g_signal_connect( dragsource, "drag-begin", G_CALLBACK(ui_listview_drag_begin), listview); g_signal_connect( dragsource, "drag-end", G_CALLBACK(ui_listview_drag_end), listview); } void ui_listview_enable_drop(UiListView *listview, UiListArgs *args) { GtkDropTarget *target = gtk_drop_target_new(G_TYPE_INVALID, GDK_ACTION_COPY); gtk_widget_add_controller(listview->widget, GTK_EVENT_CONTROLLER(target)); GType default_types[2] = { GDK_TYPE_FILE_LIST, G_TYPE_STRING }; gtk_drop_target_set_gtypes(target, default_types, 2); g_signal_connect(target, "drop", G_CALLBACK(ui_listview_drop), listview); } #else static GtkTargetEntry targetentries[] = { { "STRING", 0, 0 }, { "text/plain", 0, 1 }, { "text/uri-list", 0, 2 }, }; static void ui_listview_drag_getdata( GtkWidget* self, GdkDragContext* context, GtkSelectionData* data, guint info, guint time, gpointer user_data) { UiListView *listview = user_data; UiDnD dnd; dnd.context = context; dnd.data = data; dnd.selected_action = 0; dnd.delete = FALSE; dnd.accept = FALSE; if(listview->ondragstart) { UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &dnd; event.intval = 0; event.set = ui_get_setop(); listview->ondragstart(&event, listview->ondragstartdata); } } static void ui_listview_drag_end( GtkWidget *widget, GdkDragContext *context, guint time, gpointer user_data) { UiListView *listview = user_data; UiDnD dnd; dnd.context = context; dnd.data = NULL; dnd.selected_action = gdk_drag_context_get_selected_action(context); dnd.delete = dnd.selected_action == UI_DND_ACTION_MOVE ? TRUE : FALSE; dnd.accept = FALSE; if(listview->ondragcomplete) { UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &dnd; event.intval = 0; event.set = ui_get_setop(); listview->ondragcomplete(&event, listview->ondragcompletedata); } } void ui_listview_add_dnd(UiListView *listview, UiListArgs *args) { gtk_tree_view_enable_model_drag_source( GTK_TREE_VIEW(listview->widget), GDK_BUTTON1_MASK, targetentries, 2, GDK_ACTION_COPY); g_signal_connect(listview->widget, "drag-data-get", G_CALLBACK(ui_listview_drag_getdata), listview); g_signal_connect(listview->widget, "drag-end", G_CALLBACK(ui_listview_drag_end), listview); } static void ui_listview_drag_data_received( GtkWidget *self, GdkDragContext *context, gint x, gint y, GtkSelectionData *data, guint info, guint time, gpointer user_data) { UiListView *listview = user_data; UiDnD dnd; dnd.context = context; dnd.data = data; dnd.selected_action = 0; dnd.delete = FALSE; dnd.accept = FALSE; if(listview->ondrop) { dnd.accept = TRUE; UiEvent event; event.obj = listview->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &dnd; event.intval = 0; event.set = ui_get_setop(); listview->ondrop(&event, listview->ondropdata); } } void ui_listview_enable_drop(UiListView *listview, UiListArgs *args) { gtk_tree_view_enable_model_drag_dest( GTK_TREE_VIEW(listview->widget), targetentries, 3, GDK_ACTION_COPY); if(listview->ondrop) { g_signal_connect(listview->widget, "drag_data_received", G_CALLBACK(ui_listview_drag_data_received), listview); } } #endif GtkWidget* ui_get_tree_widget(UIWIDGET widget) { return SCROLLEDWINDOW_GET_CHILD(widget); } static char** targets2array(char *target0, va_list ap, int *nelm) { int al = 16; char **targets = calloc(16, sizeof(char*)); targets[0] = target0; int i = 1; char *target; while((target = va_arg(ap, char*)) != NULL) { if(i >= al) { al *= 2; targets = realloc(targets, al*sizeof(char*)); } targets[i] = target; i++; } *nelm = i; return targets; } /* static GtkTargetEntry* targetstr2gtktargets(char **str, int nelm) { GtkTargetEntry *targets = calloc(nelm, sizeof(GtkTargetEntry)); for(int i=0;i<nelm;i++) { targets[i].target = str[i]; } return targets; } */ void ui_table_dragsource(UIWIDGET tablewidget, int actions, char *target0, ...) { va_list ap; va_start(ap, target0); int nelm; char **targets = targets2array(target0, ap, &nelm); va_end(ap); // disabled //ui_table_dragsource_a(tablewidget, actions, targets, nelm); free(targets); } /* void ui_table_dragsource_a(UIWIDGET tablewidget, int actions, char **targets, int nelm) { GtkTargetEntry* t = targetstr2gtktargets(targets, nelm); gtk_tree_view_enable_model_drag_source( GTK_TREE_VIEW(ui_get_tree_widget(tablewidget)), GDK_BUTTON1_MASK, t, nelm, GDK_ACTION_COPY|GDK_ACTION_MOVE|GDK_ACTION_LINK); free(t); } void ui_table_dragdest(UIWIDGET tablewidget, int actions, char *target0, ...) { va_list ap; va_start(ap, target0); int nelm; char **targets = targets2array(target0, ap, &nelm); va_end(ap); ui_table_dragdest_a(tablewidget, actions, targets, nelm); free(targets); } void ui_table_dragdest_a(UIWIDGET tablewidget, int actions, char **targets, int nelm) { GtkTargetEntry* t = targetstr2gtktargets(targets, nelm); gtk_tree_view_enable_model_drag_dest( GTK_TREE_VIEW(ui_get_tree_widget(tablewidget)), t, nelm, GDK_ACTION_COPY|GDK_ACTION_MOVE|GDK_ACTION_LINK); free(t); } */ void ui_listview_destroy(GtkWidget *w, UiListView *v) { //gtk_tree_view_set_model(GTK_TREE_VIEW(w), NULL); 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]); } free(v->elements); } #if GTK_CHECK_VERSION(4, 10, 0) free(v->columns); pango_attr_list_unref(v->current_row_attributes); cxMapFree(v->bound_rows); #endif free(v->selection.rows); free(v); } /* ------------------------------ 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); } static void sublist_destroy(UiObject *obj, UiListBoxSubList *sublist) { free(sublist->header); ui_destroy_boundvar(obj->ctx, sublist->var); cxListFree(sublist->widgets); } static void listbox_create_header(GtkListBoxRow* row, GtkListBoxRow* before, gpointer user_data) { // first rows in sublists have the ui_listbox property UiListBox *listbox = g_object_get_data(G_OBJECT(row), "ui_listbox"); if(!listbox) { return; } UiListBoxSubList *sublist = g_object_get_data(G_OBJECT(row), "ui_listbox_sublist"); if(!sublist) { return; } if(sublist->separator) { GtkWidget *separator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); gtk_list_box_row_set_header(row, separator); } 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) { WIDGET_ADD_CSS_CLASS(header, "ui-listbox-header-first"); } else { WIDGET_ADD_CSS_CLASS(header, "ui-listbox-header"); } gtk_list_box_row_set_header(row, header); } } #ifdef UI_GTK3 typedef struct _UiSidebarListBoxClass { GtkListBoxClass parent_class; } UiSidebarListBoxClass; typedef struct _UiSidebarListBox { GtkListBox parent_instance; } UiSidebarListBox; G_DEFINE_TYPE(UiSidebarListBox, ui_sidebar_list_box, GTK_TYPE_LIST_BOX) /* Initialize the instance */ static void ui_sidebar_list_box_class_init(UiSidebarListBoxClass *klass) { GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); gtk_widget_class_set_css_name (widget_class, "placessidebar"); } static void ui_sidebar_list_box_init(UiSidebarListBox *self) { } #endif static void add_sublist(UiListBox *uilistbox, CxList *sublists, UiSubList *sublist) { UiListBoxSubList uisublist; uisublist.var = uic_widget_var( uilistbox->obj->ctx, uilistbox->obj->ctx, sublist->value, sublist->varname, UI_VAR_LIST); uisublist.numitems = 0; uisublist.header = sublist->header ? strdup(sublist->header) : NULL; uisublist.separator = sublist->separator; uisublist.widgets = cxLinkedListCreateSimple(CX_STORE_POINTERS); uisublist.listbox = uilistbox; uisublist.userdata = sublist->userdata; uisublist.index = cxListSize(sublists); uisublist.startpos = 0; cxListAdd(sublists, &uisublist); // bind UiList UiListBoxSubList *sublist_ptr = cxListAt(uilistbox->sublists, cxListSize(sublists)-1); if(uisublist.var && uisublist.var->value) { UiList *list = uisublist.var->value; list->obj = sublist_ptr; list->update = ui_listbox_list_update; list->getselection = ui_listbox_list_getselection; list->setselection = ui_listbox_list_setselection; } } UIEXPORT UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs *args) { #ifdef UI_GTK3 GtkWidget *listbox = g_object_new(ui_sidebar_list_box_get_type(), NULL); #else GtkWidget *listbox = gtk_list_box_new(); #endif if(!args->style_class) { #if GTK_MAJOR_VERSION >= 4 WIDGET_ADD_CSS_CLASS(listbox, "navigation-sidebar"); #else WIDGET_ADD_CSS_CLASS(listbox, "sidebar"); #endif } gtk_list_box_set_header_func(GTK_LIST_BOX(listbox), listbox_create_header, NULL, NULL); GtkWidget *scroll_area = SCROLLEDWINDOW_NEW(); SCROLLEDWINDOW_SET_CHILD(scroll_area, listbox); ui_set_name_and_style(listbox, args->name, args->style_class); ui_set_widget_groups(obj->ctx, listbox, args->states); 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; uilistbox->onactivatedata = args->onactivatedata; uilistbox->onbuttonclick = args->onbuttonclick; uilistbox->onbuttonclickdata = args->onbuttonclickdata; uilistbox->sublists = cxArrayListCreateSimple(sizeof(UiListBoxSubList), 4); uilistbox->sublists->collection.advanced_destructor = (cx_destructor_func2)sublist_destroy; uilistbox->sublists->collection.destructor_data = obj; uilistbox->first_row = NULL; if(args->sublists) { // static sublist initalization if(args->numsublists == 0 && args->sublists) { args->numsublists = INT_MAX; } for(int i=0;i<args->numsublists;i++) { UiSubList sublist = args->sublists[i]; if(!sublist.varname && !sublist.value) { break; } add_sublist(uilistbox, uilistbox->sublists, &sublist); } // fill items ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists)); } else { 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; list->update = ui_listbox_dynamic_update; list->getselection = ui_listbox_dynamic_getselection; list->setselection = ui_listbox_dynamic_setselection; ui_listbox_dynamic_update(list, -1); } } // register uilistbox for both widgets, so it doesn't matter which // widget is used later g_object_set_data(G_OBJECT(scroll_area), "ui_listbox", uilistbox); g_object_set_data(G_OBJECT(listbox), "ui_listbox", uilistbox); if(args->contextmenu) { UIMENU menu = ui_contextmenu_create(args->contextmenu, obj, listbox); ui_widget_set_contextmenu(listbox, menu); } // signals g_signal_connect( listbox, "destroy", G_CALLBACK(ui_destroy_sourcelist), uilistbox); if(args->onactivate) { g_signal_connect( listbox, "row-activated", G_CALLBACK(ui_listbox_row_activate), NULL); } return scroll_area; } void ui_listbox_dynamic_update(UiList *list, int x) { UiListBox *uilistbox = list->obj; // unbind/free previous list vars CxIterator i = cxListIterator(uilistbox->sublists); cx_foreach(UiListBoxSubList *, s, i) { // TODO: "unbind/free previous list vars" will also remove // the widget list. This makes the widget optimization // in ui_listbox_update_sublist pointless // Is it actually possible to not recreate the whole list? CxIterator r = cxListIterator(s->widgets); cx_foreach(GtkWidget*, widget, r) { LISTBOX_REMOVE(uilistbox->listbox, widget); } if(s->var) { UiList *sl = s->var->value; sl->obj = NULL; sl->update = NULL; if(s->var->type == UI_VAR_SPECIAL) { ui_free(s->var->from_ctx, s->var); } } } cxListFree(uilistbox->sublists); CxList *new_sublists = cxArrayListCreateSimple(sizeof(UiListBoxSubList), list->count(list)); uilistbox->sublists = new_sublists; UiSubList *sublist = list->first(list); while(sublist) { add_sublist(uilistbox, new_sublists, sublist); sublist = list->next(list); } ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists)); } void ui_listbox_dynamic_setselection(UiList *list, UiListSelection sel) { UiListBox *uilistbox = list->obj; gtk_list_box_unselect_all(uilistbox->listbox); if(sel.count > 0) { int index = sel.rows[0]; if(index >= 0) { GtkListBoxRow *row = gtk_list_box_get_row_at_index(uilistbox->listbox, index); if(row) { gtk_list_box_select_row(uilistbox->listbox, row); } } } } UiListSelection ui_listbox_dynamic_getselection(UiList *list) { UiListSelection sel = { 0, NULL }; UiListBox *uilistbox = list->obj; GtkListBoxRow *row = gtk_list_box_get_selected_row(uilistbox->listbox); if(row) { sel.count = 1; sel.rows = malloc(sizeof(int)); sel.rows[0] = gtk_list_box_row_get_index(row); } return sel; } void ui_listbox_update(UiListBox *listbox, int from, int to) { CxIterator i = cxListIterator(listbox->sublists); size_t pos = 0; cx_foreach(UiListBoxSubList *, sublist, i) { if(i.index < from) { pos += sublist->numitems; continue; } if(i.index > to) { break; } // reload sublist sublist->startpos = pos; ui_listbox_update_sublist(listbox, sublist, pos); pos += sublist->numitems; } ui_sourcelist_update_finished(); } static void listbox_button_clicked(GtkWidget *button, UiEventDataExt *data) { UiListBoxSubList *sublist = data->customdata0; UiSubListEventData *eventdata = &sublist->listbox->current_eventdata; eventdata->list = sublist->var->value; 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->event_data = data->customdata2; UiEvent event; event.obj = data->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = eventdata; event.eventdatatype = UI_EVENT_DATA_SUBLIST; event.intval = data->value0; event.set = ui_get_setop(); if(data->callback2) { data->callback2(&event, data->userdata2); } if(data->customdata3) { uic_set_tmp_eventdata(eventdata, UI_EVENT_DATA_SUBLIST); 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(4, 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); } } #else static void popup_hide(GtkWidget *self, 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) { } LISTBOX_ROW_SET_CHILD(row, hbox); // signals UiEventDataExt *event = malloc(sizeof(UiEventDataExt)); memset(event, 0, sizeof(UiEventDataExt)); event->obj = listbox->obj; event->customdata0 = sublist; event->customdata1 = sublist->var; event->customdata2 = item->eventdata; event->callback = listbox->onactivate; event->userdata = listbox->onactivatedata; event->callback2 = listbox->onbuttonclick; 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", G_CALLBACK(ui_destroy_userdata), event); g_object_set_data(G_OBJECT(row), "ui-listbox-row-eventdata", event); // badge if(item->badge) { GtkWidget *badge = gtk_label_new(item->badge); WIDGET_ADD_CSS_CLASS(badge, "ui-badge"); #if GTK_CHECK_VERSION(3, 14, 0) gtk_widget_set_valign(badge, GTK_ALIGN_CENTER); BOX_ADD(hbox, badge); #else GtkWidget *align = gtk_alignment_new(0.5, 0.5, 0, 0); gtk_container_add(GTK_CONTAINER(align), badge); BOX_ADD(hbox, align); #endif } // button if(item->button_icon || item->button_label) { 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(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; #if GTK_CHECK_VERSION(4, 0, 0) g_signal_connect(menu, "closed", G_CALLBACK(button_popover_closed), button); #else g_signal_connect(menu, "hide", G_CALLBACK(popup_hide), button); #endif ui_menubuilder_unref(item->button_menu); } } } static void update_sublist_item(UiListBox *listbox, UiListBoxSubList *sublist, int 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; } UiList *list = sublist->var->value; if(!list) { return; } void *elm = list->get(list, index); UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL }; if(listbox->getvalue) { listbox->getvalue(list, sublist->userdata, elm, index, &item, listbox->getvaluedata); } else { item.label = strdup(elm); } LISTBOX_ROW_REMOVE_CHILD(row); listbox_fill_row(listbox, GTK_WIDGET(row), sublist, &item, index); // cleanup free(item.label); free(item.icon); free(item.button_label); free(item.button_icon); 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); cx_foreach(GtkWidget*, widget, r) { LISTBOX_REMOVE(listbox->listbox, widget); } cxListClear(sublist->widgets); sublist->numitems = 0; // create items for each UiList element if(!sublist->var) { return; } UiList *list = sublist->var->value; if(!list) { return; } int index = 0; void *elm = list->first(list); void *first = elm; 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); 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) { listbox->getvalue(list, sublist->userdata, elm, index, &item, listbox->getvaluedata); } else { 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(); #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); g_object_set_data(G_OBJECT(row), "ui_listbox_sublist", sublist); if(listbox_insert_index == 0) { // first row in the GtkListBox 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 + header_row); cxListAdd(sublist->widgets, row); // cleanup free(item.label); free(item.icon); free(item.button_label); free(item.button_icon); free(item.badge); // next row elm = index >= 0 ? list->next(list) : first; index++; } sublist->numitems = cxListSize(sublist->widgets); } void ui_listbox_list_update(UiList *list, int i) { UiListBoxSubList *sublist = list->obj; if(i < 0) { ui_listbox_update_sublist(sublist->listbox, sublist, sublist->startpos); size_t pos = 0; CxIterator it = cxListIterator(sublist->listbox->sublists); cx_foreach(UiListBoxSubList *, ls, it) { ls->startpos = pos; pos += ls->numitems; } } else { update_sublist_item(sublist->listbox, sublist, i); } ui_sourcelist_update_finished(); } void ui_listbox_list_setselection(UiList *list, UiListSelection sel) { UiListBoxSubList *sublist = list->obj; UiListBox *uilistbox = sublist->listbox; gtk_list_box_unselect_all(uilistbox->listbox); if(sel.count > 0) { int index = sel.rows[0]; if(index >= 0 && index < sublist->numitems) { int global_index = sublist->startpos + index; GtkListBoxRow *row = gtk_list_box_get_row_at_index(uilistbox->listbox, global_index); if(row) { gtk_list_box_select_row(uilistbox->listbox, row); } } } } UiListSelection ui_listbox_list_getselection(UiList *list) { UiListSelection sel = { 0, NULL }; UiListBoxSubList *sublist = list->obj; UiListBox *uilistbox = sublist->listbox; GtkListBoxRow *row = gtk_list_box_get_selected_row(uilistbox->listbox); if(row) { int index = gtk_list_box_row_get_index(row); size_t startpos = sublist->startpos; if(index >= startpos && index < startpos+sublist->numitems) { sel.count = 1; sel.rows = malloc(sizeof(int)); sel.rows[0] = index - startpos; } } return sel; } void ui_listbox_row_activate(GtkListBox *self, GtkListBoxRow *row, gpointer user_data) { UiEventDataExt *data = g_object_get_data(G_OBJECT(row), "ui-listbox-row-eventdata"); if(!data) { return; } UiListBoxSubList *sublist = data->customdata0; UiSubListEventData eventdata; eventdata.list = sublist->var->value; eventdata.sublist_index = sublist->index; eventdata.row_index = data->value0; eventdata.sublist_userdata = sublist->userdata; eventdata.row_data = eventdata.row_index >= 0 ? eventdata.list->get(eventdata.list, eventdata.row_index) : NULL; eventdata.event_data = data->customdata2; UiEvent event; event.obj = data->obj; event.window = event.obj->window; event.document = event.obj->ctx->document; event.eventdata = &eventdata; event.eventdatatype = UI_EVENT_DATA_SUBLIST; event.intval = data->value0; event.set = ui_get_setop(); if(data->callback) { data->callback(&event, data->userdata); } }