--- a/ui/gtk/text.c Mon Feb 12 21:13:23 2024 +0100 +++ b/ui/gtk/text.c Sun Jun 09 15:43:08 2024 +0200 @@ -33,6 +33,10 @@ #include "text.h" #include "container.h" +#include <cx/printf.h> + +#include <gdk/gdkkeysyms.h> + #include "../common/types.h" @@ -534,9 +538,14 @@ } -static UIWIDGET create_textfield_var(UiObject *obj, int width, UiBool frameless, UiBool password, UiVar *var) { + + +static UIWIDGET create_textfield(UiObject *obj, UiBool frameless, UiBool password, UiTextFieldArgs args) { GtkWidget *textfield = gtk_entry_new(); + UiObject* current = uic_current_obj(obj); + UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.value, args.varname, UI_VAR_STRING); + UiTextField *uitext = malloc(sizeof(UiTextField)); uitext->ctx = obj->ctx; uitext->var = var; @@ -547,8 +556,8 @@ G_CALLBACK(ui_textfield_destroy), uitext); - if(width > 0) { - gtk_entry_set_width_chars(GTK_ENTRY(textfield), width); + if(args.width > 0) { + gtk_entry_set_width_chars(GTK_ENTRY(textfield), args.width); } if(frameless) { // TODO: gtk2legacy workaroud @@ -586,40 +595,25 @@ return textfield; } -static UIWIDGET create_textfield_nv(UiObject *obj, int width, UiBool frameless, UiBool password, char *varname) { - UiVar *var = uic_create_var(obj->ctx, varname, UI_VAR_STRING); - if(var) { - return create_textfield_var(obj, width, frameless, password, var); - } else { - // TODO: error - } - return NULL; +UIWIDGET ui_textfield_create(UiObject *obj, UiTextFieldArgs args) { + return create_textfield(obj, FALSE, FALSE, args); } -static UIWIDGET create_textfield(UiObject *obj, int width, UiBool frameless, UiBool password, UiString *value) { - UiVar *var = NULL; - if(value) { - var = malloc(sizeof(UiVar)); - var->value = value; - var->type = UI_VAR_SPECIAL; - var->from = NULL; - var->from_ctx = NULL; - } - return create_textfield_var(obj, width, frameless, password, var); +UIWIDGET ui_frameless_textfield_create(UiObject* obj, UiTextFieldArgs args) { + return create_textfield(obj, TRUE, FALSE, args); } +UIWIDGET ui_passwordfield_create(UiObject* obj, UiTextFieldArgs args) { + return create_textfield(obj, FALSE, TRUE, args); +} + + void ui_textfield_destroy(GtkWidget *object, UiTextField *textfield) { - if(textfield->var) { - UiText *text = textfield->var->value; - if(text->undomgr) { - ui_destroy_undomgr(text->undomgr); - } - ui_destroy_boundvar(textfield->ctx, textfield->var); - } free(textfield); } void ui_textfield_changed(GtkEditable *editable, UiTextField *textfield) { + // changed event is only registered, if the textfield->var != NULL UiString *value = textfield->var->value; if(value->observers) { UiEvent e; @@ -632,45 +626,6 @@ } } -UIWIDGET ui_textfield_deprecated(UiObject *obj, UiString *value) { - return create_textfield(obj, 0, FALSE, FALSE, value); -} - -UIWIDGET ui_textfield_nv(UiObject *obj, char *varname) { - return create_textfield_nv(obj, 0, FALSE, FALSE, varname); -} - -UIWIDGET ui_textfield_w(UiObject *obj, int width, UiString *value) { - return create_textfield(obj, width, FALSE, FALSE, value); -} - -UIWIDGET ui_textfield_wnv(UiObject *obj, int width, char *varname) { - return create_textfield_nv(obj, width, FALSE, FALSE, varname); -} - -UIWIDGET ui_frameless_textfield_deprecated(UiObject *obj, UiString *value) { - return create_textfield(obj, 0, TRUE, FALSE, value); -} - -UIWIDGET ui_frameless_textfield_nv(UiObject *obj, char *varname) { - return create_textfield_nv(obj, 0, TRUE, FALSE, varname); -} - -UIWIDGET ui_passwordfield_deprecated(UiObject *obj, UiString *value) { - return create_textfield(obj, 0, FALSE, TRUE, value); -} - -UIWIDGET ui_passwordfield_nv(UiObject *obj, char *varname) { - return create_textfield_nv(obj, 0, FALSE, TRUE, varname); -} - -UIWIDGET ui_passwordfield_w(UiObject *obj, int width, UiString *value) { - return create_textfield(obj, width, FALSE, TRUE, value); -} - -UIWIDGET ui_passwordfield_wnv(UiObject *obj, int width, char *varname) { - return create_textfield_nv(obj, width, FALSE, TRUE, varname); -} char* ui_textfield_get(UiString *str) { if(str->value.ptr) { @@ -689,3 +644,283 @@ str->value.free = NULL; } } + +// ----------------------- path textfield ----------------------- + +// TODO: move to common +static UiPathElm* default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data) { + cxstring *pathelms; + size_t nelm = cx_strsplit_a(cxDefaultAllocator, cx_strn(full_path, len), CX_STR("/"), 4096, &pathelms); + + if (nelm == 0) { + *ret_nelm = 0; + return NULL; + } + + UiPathElm* elms = (UiPathElm*)calloc(nelm, sizeof(UiPathElm)); + size_t n = nelm; + int j = 0; + for (int i = 0; i < nelm; i++) { + cxstring c = pathelms[i]; + if (c.length == 0) { + if (i == 0) { + c.length = 1; + } + else { + n--; + continue; + } + } + + cxmutstr m = cx_strdup(c); + elms[j].name = m.ptr; + elms[j].name_len = m.length; + + size_t elm_path_len = c.ptr + c.length - full_path; + cxmutstr elm_path = cx_strdup(cx_strn(full_path, elm_path_len)); + elms[j].path = elm_path.ptr; + elms[j].path_len = elm_path.length; + + j++; + } + *ret_nelm = n; + + return elms; +} + +static gboolean path_textfield_btn_pressed(GtkWidget *widget, GdkEventButton *event, UiPathTextField *pathtf) { + gtk_box_pack_start(GTK_BOX(pathtf->hbox), pathtf->entry, TRUE, TRUE, 0); + gtk_container_remove(GTK_CONTAINER(pathtf->hbox), pathtf->buttonbox); + + gtk_widget_show(pathtf->entry); + gtk_widget_grab_focus(pathtf->entry); + + return TRUE; +} + +static void ui_pathelm_destroy(UiPathElm *elms, size_t nelm) { + for(int i=0;i<nelm;i++) { + free(elms[i].name); + free(elms[i].path); + } + free(elms); +} + +static void ui_path_textfield_destroy(GtkWidget *object, UiPathTextField *pathtf) { + free(pathtf->current_path); + g_object_unref(pathtf->entry); + free(pathtf); +} + +static void ui_path_textfield_activate(GtkWidget *entry, UiPathTextField *pathtf) { + const gchar *text = gtk_entry_get_text(GTK_ENTRY(pathtf->entry)); + if(strlen(text) == 0) { + return; + } + + UiObject *obj = pathtf->obj; + + if(ui_pathtextfield_update(pathtf, text)) { + return; + } + + if(pathtf->onactivate) { + UiEvent evt; + evt.obj = obj; + evt.window = obj->window; + evt.document = obj->ctx->document; + evt.eventdata = (char*)text; + evt.intval = -1; + pathtf->onactivate(&evt, pathtf->onactivatedata); + } +} + +static gboolean ui_path_textfield_key_press(GtkWidget *self, GdkEventKey *event, UiPathTextField *pathtf) { + if (event->keyval == GDK_KEY_Escape) { + // reset GtkEntry value + gtk_entry_set_text(GTK_ENTRY(self), pathtf->current_path); + const gchar *text = gtk_entry_get_text(GTK_ENTRY(self)); + ui_pathtextfield_update(pathtf, text); + return TRUE; + } + return FALSE; +} + +static GtkWidget* create_path_button_box() { + GtkWidget *bb = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL); + gtk_button_box_set_layout(GTK_BUTTON_BOX(bb), GTK_BUTTONBOX_EXPAND); // linked style + gtk_box_set_homogeneous(GTK_BOX(bb), FALSE); + gtk_box_set_spacing(GTK_BOX(bb), 0); + return bb; +} + +UIWIDGET ui_path_textfield_create(UiObject* obj, UiPathTextFieldArgs args) { + UiObject* current = uic_current_obj(obj); + + UiPathTextField *pathtf = malloc(sizeof(UiPathTextField)); + memset(pathtf, 0, sizeof(UiPathTextField)); + pathtf->obj = obj; + pathtf->getpathelm = args.getpathelm; + pathtf->getpathelmdata = args.getpathelmdata; + pathtf->onactivate = args.onactivate; + pathtf->onactivatedata = args.onactivatedata; + pathtf->ondragcomplete = args.ondragcomplete; + pathtf->ondragcompletedata = args.ondragcompletedata; + pathtf->ondragstart = args.ondragstart; + pathtf->ondragstartdata = args.ondragstartdata; + pathtf->ondrop = args.ondrop; + pathtf->ondropdata = args.ondropsdata; + + if(!pathtf->getpathelm) { + pathtf->getpathelm = default_pathelm_func; + pathtf->getpathelmdata = NULL; + } + + // top level container for the path textfield is a GtkEventBox + // the event box is needed to handle background button presses + GtkWidget *eventbox = gtk_event_box_new(); + g_signal_connect( + eventbox, + "button-press-event", + G_CALLBACK(path_textfield_btn_pressed), + pathtf); + g_signal_connect( + eventbox, + "destroy", + G_CALLBACK(ui_path_textfield_destroy), + pathtf); + + UI_APPLY_LAYOUT1(current, args); + current->container->add(current->container, eventbox, FALSE); + + // hbox as parent for the GtkEntry and GtkButtonBox + GtkWidget *hbox = ui_gtk_hbox_new(0); + pathtf->hbox = hbox; + gtk_container_add(GTK_CONTAINER(eventbox), hbox); + gtk_widget_set_name(hbox, "path-textfield-box"); + + // create GtkEntry, that is also visible by default (with input yet) + pathtf->entry = gtk_entry_new(); + g_object_ref(G_OBJECT(pathtf->entry)); + gtk_box_pack_start(GTK_BOX(hbox), pathtf->entry, TRUE, TRUE, 0); + + g_signal_connect( + pathtf->entry, + "activate", + G_CALLBACK(ui_path_textfield_activate), + pathtf); + g_signal_connect( + pathtf->entry, + "key-press-event", + G_CALLBACK(ui_path_textfield_key_press), + pathtf); + + UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.value, args.varname, UI_VAR_STRING); + if (var) { + UiString* value = (UiString*)var->value; + value->obj = pathtf; + value->get = ui_path_textfield_get; + value->set = ui_path_textfield_set; + + if(value->value.ptr) { + char *str = strdup(value->value.ptr); + ui_string_set(value, str); + free(str); + } + } + + return hbox; +} + +void ui_path_button_clicked(GtkWidget *widget, UiEventData *event) { + UiPathElm *elm = event->customdata; + cxmutstr path = cx_strdup(cx_strn(elm->path, elm->path_len)); + UiEvent evt; + evt.obj = event->obj; + evt.window = evt.obj->window; + evt.document = evt.obj->ctx->document; + evt.eventdata = elm->path; + evt.intval = event->value; + event->callback(&evt, event->userdata); + free(path.ptr); +} + +int ui_pathtextfield_update(UiPathTextField* pathtf, const char *full_path) { + size_t full_path_len = strlen(full_path); + + size_t nelm = 0; + UiPathElm* path_elm = pathtf->getpathelm(full_path, full_path_len, &nelm, pathtf->getpathelmdata); + if (!path_elm) { + return 1; + } + + free(pathtf->current_path); + pathtf->current_path = strdup(full_path); + + ui_pathelm_destroy(pathtf->current_pathelms, pathtf->current_nelm); + pathtf->current_pathelms = path_elm; + pathtf->current_nelm = nelm; + + GtkWidget *buttonbox = create_path_button_box(); + pathtf->buttonbox = buttonbox; + + // switch from entry to buttonbox + gtk_container_remove(GTK_CONTAINER(pathtf->hbox), pathtf->entry); + gtk_box_pack_start(GTK_BOX(pathtf->hbox), buttonbox, FALSE, FALSE, 0); + + for (int i=0;i<nelm;i++) { + UiPathElm *elm = &path_elm[i]; + + cxmutstr name = cx_strdup(cx_strn(elm->name, elm->name_len)); + GtkWidget *button = gtk_button_new_with_label(name.ptr); + free(name.ptr); + + if(pathtf->onactivate) { + UiEventData *eventdata = malloc(sizeof(UiEventData)); + eventdata->callback = pathtf->onactivate; + eventdata->userdata = pathtf->onactivatedata; + eventdata->obj = pathtf->obj; + eventdata->customdata = elm; + eventdata->value = i; + + g_signal_connect( + button, + "clicked", + G_CALLBACK(ui_path_button_clicked), + eventdata); + + g_signal_connect( + button, + "destroy", + G_CALLBACK(ui_destroy_userdata), + eventdata); + } + + gtk_box_pack_start(GTK_BOX(buttonbox), button, FALSE, FALSE, 0); + } + + gtk_widget_show_all(buttonbox); + + return 0; +} + +char* ui_path_textfield_get(UiString *str) { + if(str->value.ptr) { + str->value.free(str->value.ptr); + } + UiPathTextField *tf = str->obj; + str->value.ptr = g_strdup(gtk_entry_get_text(GTK_ENTRY(tf->entry))); + str->value.free = (ui_freefunc)g_free; + return str->value.ptr; +} + +void ui_path_textfield_set(UiString *str, const char *value) { + UiPathTextField *tf = str->obj; + gtk_entry_set_text(GTK_ENTRY(tf->entry), value); + ui_pathtextfield_update(tf, value); + if(str->value.ptr) { + str->value.free(str->value.ptr); + str->value.ptr = NULL; + str->value.free = NULL; + } +}