--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/gtk/list.c Thu Oct 03 18:08:29 2024 +0200 @@ -0,0 +1,666 @@ +/* + * 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 "list.h" +#include "image.h" + + +void* ui_strmodel_getvalue(void *elm, int column) { + return column == 0 ? elm : NULL; +} + +static GtkListStore* create_list_store(UiList *list, UiModel *model) { + int columns = model->columns; + GType types[2*columns]; + 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; + } + } + } + + GtkListStore *store = gtk_list_store_newv(c, types); + + if(list) { + void *elm = list->first(list); + while(elm) { + // insert new row + GtkTreeIter iter; + gtk_list_store_insert (store, &iter, -1); + + // set column values + int c = 0; + for(int i=0;i<columns;i++,c++) { + void *data = model->getvalue(elm, c); + + GValue value = G_VALUE_INIT; + switch(model->types[i]) { + case UI_STRING: + case UI_STRING_FREE: { + g_value_init(&value, G_TYPE_STRING); + g_value_set_string(&value, data); + if(model->types[i] == UI_STRING_FREE) { + free(data); + } + break; + } + case UI_INTEGER: { + g_value_init(&value, G_TYPE_INT); + int *intptr = data; + g_value_set_int(&value, *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 + 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->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++; + + char *str = model->getvalue(elm, c); + g_value_init(&value, G_TYPE_STRING); + g_value_set_string(&value, str); + if(model->types[i] == UI_ICON_TEXT_FREE) { + free(str); + } + break; + } + } + + gtk_list_store_set_value(store, &iter, c, &value); + } + + // next row + elm = list->next(list); + } + } + + return store; +} + + +UIWIDGET ui_listview_create(UiObject *obj, UiListArgs args) { + UiObject* current = uic_current_obj(obj); + + // create treeview + GtkWidget *view = gtk_tree_view_new(); + 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); + model->getvalue = args.getvalue; + + UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.list, args.varname, UI_VAR_LIST); + + UiList *list = var ? var->value : NULL; + GtkListStore *listmodel = create_list_store(list, model); + gtk_tree_view_set_model(GTK_TREE_VIEW(view), GTK_TREE_MODEL(listmodel)); + + UiListView *listview = malloc(sizeof(UiListView)); + listview->obj = obj; + listview->widget = view; + listview->var = var; + listview->model = model; + g_signal_connect( + view, + "destroy", + G_CALLBACK(ui_listview_destroy), + listview); + + // bind var + list->update = ui_listview_update; + list->getselection = ui_listview_getselection; + list->obj = listview; + + // add callback + if(args.onactivate) { + UiTreeEventData *event = ui_malloc(obj->ctx, sizeof(UiTreeEventData)); + event->obj = obj; + event->activatedata = args.onactivatedata; + event->activate = args.onactivate; + event->selection = NULL; + + g_signal_connect( + view, + "row-activated", + G_CALLBACK(ui_listview_activate_event), + event); + } + + // 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); + + UI_APPLY_LAYOUT1(current, args); + current->container->add(current->container, scroll_area, FALSE); + + // ct->current should point to view, not scroll_area, to make it possible + // to add a context menu + current->container->current = view; + + return scroll_area; +} + +/* +static void drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer udata) { + printf("drag begin\n"); + +} + +static void drag_end( + GtkWidget *widget, + GdkDragContext *context, + guint time, + gpointer udata) +{ + printf("drag end\n"); + +} +*/ + +/* +static GtkTargetEntry targetentries[] = + { + { "STRING", 0, 0 }, + { "text/plain", 0, 1 }, + { "text/uri-list", 0, 2 }, + }; +*/ + +UIWIDGET ui_table_create(UiObject *obj, UiListArgs args) { + UiObject* current = uic_current_obj(obj); + + // create treeview + GtkWidget *view = gtk_tree_view_new(); + + UiModel *model = args.model; + int columns = model ? model->columns : 0; + + int addi = 0; + for(int i=0;i<columns;i++) { + GtkTreeViewColumn *column = NULL; + if(model->types[i] == UI_ICON_TEXT) { + 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", i); + gtk_tree_view_column_add_attribute(column, textrenderer, "text", i+1); + + 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 *renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes( + model->titles[i], + renderer, + "text", + i + addi, + NULL); + } + 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, current->ctx, args.list, args.varname, UI_VAR_LIST); + + UiList *list = var ? var->value : NULL; + GtkListStore *listmodel = create_list_store(list, model); + gtk_tree_view_set_model(GTK_TREE_VIEW(view), GTK_TREE_MODEL(listmodel)); + + //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 = malloc(sizeof(UiListView)); + tableview->obj = obj; + tableview->widget = view; + tableview->var = var; + tableview->model = model; + g_signal_connect( + view, + "destroy", + G_CALLBACK(ui_listview_destroy), + tableview); + + // bind var + list->update = ui_listview_update; + list->getselection = ui_listview_getselection; + 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 + + + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(view)); + 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); + + UI_APPLY_LAYOUT1(current, args); + current->container->add(current->container, scroll_area, FALSE); + + // ct->current should point to view, not scroll_area, to make it possible + // to add a context menu + current->container->current = view; + + return scroll_area; +} + + +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_update(UiList *list, int i) { + UiListView *view = list->obj; + GtkListStore *store = create_list_store(list, view->model); + gtk_tree_view_set_model(GTK_TREE_VIEW(view->widget), GTK_TREE_MODEL(store)); + g_object_unref(G_OBJECT(store)); + // TODO: free old model +} + +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_destroy(GtkWidget *w, UiListView *v) { + gtk_tree_view_set_model(GTK_TREE_VIEW(w), NULL); + ui_destroy_boundvar(v->obj->ctx, v->var); + // TODO: destroy model? + free(v); +} + +void ui_combobox_destroy(GtkWidget *w, UiListView *v) { + gtk_combo_box_set_model(GTK_COMBO_BOX(w), NULL); + ui_destroy_boundvar(v->obj->ctx, v->var); + // TODO: destroy model? + free(v); +} + + +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; + 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; + 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]; +} + + +/* --------------------------- ComboBox --------------------------- */ + +UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs args) { + UiObject* current = uic_current_obj(obj); + + UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1); + model->getvalue = args.getvalue; + + UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.list, args.varname, UI_VAR_LIST); + + GtkWidget *combobox = ui_create_combobox(obj, model, var, args.onactivate, args.onactivatedata); + UI_APPLY_LAYOUT1(current, args); + current->container->add(current->container, combobox, FALSE); + current->container->current = combobox; + return combobox; +} + +GtkWidget* ui_create_combobox(UiObject *obj, UiModel *model, UiVar *var, ui_callback f, void *udata) { + GtkWidget *combobox = gtk_combo_box_new(); + + UiListView *uicbox = malloc(sizeof(UiListView)); + uicbox->obj = obj; + uicbox->widget = combobox; + + UiList *list = var ? var->value : NULL; + GtkListStore *listmodel = create_list_store(list, model); + + if(listmodel) { + gtk_combo_box_set_model(GTK_COMBO_BOX(combobox), GTK_TREE_MODEL(listmodel)); + } + + uicbox->var = var; + uicbox->model = model; + + g_signal_connect( + combobox, + "destroy", + G_CALLBACK(ui_combobox_destroy), + uicbox); + + // bind var + if(list) { + list->update = ui_combobox_modelupdate; + // TODO: combobox getselection + list->obj = uicbox; + } + + 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(f) { + UiEventData *event = ui_malloc(obj->ctx, sizeof(UiEventData)); + event->obj = obj; + event->userdata = udata; + event->callback = f; + event->value = 0; + event->customdata = NULL; + + g_signal_connect( + combobox, + "changed", + G_CALLBACK(ui_combobox_change_event), + event); + } + + return combobox; +} + +void ui_combobox_change_event(GtkComboBox *widget, UiEventData *e) { + UiEvent event; + event.obj = e->obj; + event.window = event.obj->window; + event.document = event.obj->ctx->document; + event.eventdata = NULL; + event.intval = gtk_combo_box_get_active(widget); + e->callback(&event, e->userdata); +} + +void ui_combobox_modelupdate(UiList *list, int i) { + UiListView *view = list->obj; + GtkListStore *store = create_list_store(view->var->value, view->model); + gtk_combo_box_set_model(GTK_COMBO_BOX(view->widget), GTK_TREE_MODEL(store)); +} +