diff -r 000000000000 -r 2483f517c562 ui/gtk/text.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/gtk/text.c Sun Jan 21 16:30:18 2024 +0100 @@ -0,0 +1,691 @@ +/* + * 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 +#include +#include + +#include "text.h" +#include "container.h" + + +#include "../common/types.h" + +static void selection_handler( + GtkTextBuffer *buf, + GtkTextIter *location, + GtkTextMark *mark, + UiTextArea *textview) +{ + const char *mname = gtk_text_mark_get_name(mark); + if(mname) { + GtkTextIter begin; + GtkTextIter end; + int sel = gtk_text_buffer_get_selection_bounds (buf, &begin, &end); + if(sel != textview->last_selection_state) { + if(sel) { + ui_set_group(textview->ctx, UI_GROUP_SELECTION); + } else { + ui_unset_group(textview->ctx, UI_GROUP_SELECTION); + } + } + textview->last_selection_state = sel; + } +} + +UIWIDGET ui_textarea_var(UiObject *obj, UiVar *var) { + GtkWidget *text_area = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_area), GTK_WRAP_WORD_CHAR); + g_signal_connect( + text_area, + "realize", + G_CALLBACK(ui_textarea_realize_event), + NULL); + + UiTextArea *uitext = malloc(sizeof(UiTextArea)); + uitext->ctx = obj->ctx; + uitext->var = var; + uitext->last_selection_state = 0; + + g_signal_connect( + text_area, + "destroy", + G_CALLBACK(ui_textarea_destroy), + uitext); + + GtkWidget *scroll_area = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy( + GTK_SCROLLED_WINDOW(scroll_area), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); // GTK_POLICY_ALWAYS + gtk_container_add(GTK_CONTAINER(scroll_area), text_area); + + // font and padding + PangoFontDescription *font; + font = pango_font_description_from_string("Monospace"); + gtk_widget_modify_font(text_area, font); + pango_font_description_free(font); + + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_area), 2); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text_area), 2); + + // add + UiContainer *ct = uic_get_current_container(obj); + ct->add(ct, scroll_area, TRUE); + + // bind value + UiText *value = var->value; + if(value) { + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_area)); + + if(value->value.ptr) { + gtk_text_buffer_set_text(buf, value->value.ptr, -1); + value->value.free(value->value.ptr); + } + + value->get = ui_textarea_get; + value->set = ui_textarea_set; + value->getsubstr = ui_textarea_getsubstr; + value->insert = ui_textarea_insert; + value->setposition = ui_textarea_setposition; + value->position = ui_textarea_position; + value->selection = ui_textarea_selection; + value->length = ui_textarea_length; + value->remove = ui_textarea_remove; + value->value.ptr = NULL; + value->value.free = NULL; + value->obj = buf; + if(!value->undomgr) { + value->undomgr = ui_create_undomgr(); + } + + g_signal_connect( + buf, + "changed", + G_CALLBACK(ui_textbuf_changed), + uitext); + + // register undo manager + g_signal_connect( + buf, + "insert-text", + G_CALLBACK(ui_textbuf_insert), + var); + g_signal_connect( + buf, + "delete-range", + G_CALLBACK(ui_textbuf_delete), + var); + g_signal_connect( + buf, + "mark-set", + G_CALLBACK(selection_handler), + uitext); + } + + return scroll_area; +} + +void ui_textarea_destroy(GtkWidget *object, UiTextArea *textarea) { + ui_destroy_boundvar(textarea->ctx, textarea->var); + free(textarea); +} + +UIWIDGET ui_textarea(UiObject *obj, UiText *value) { + UiVar *var = malloc(sizeof(UiVar)); + var->value = value; + var->type = UI_VAR_SPECIAL; + var->from = NULL; + var->from_ctx = NULL; + return ui_textarea_var(obj, var); +} + +UIWIDGET ui_textarea_nv(UiObject *obj, char *varname) { + UiVar *var = uic_create_var(obj->ctx, varname, UI_VAR_TEXT); + if(var) { + return ui_textarea_var(obj, var); + } else { + // TODO: error + } + return NULL; +} + +UIWIDGET ui_textarea_gettextwidget(UIWIDGET textarea) { + return gtk_bin_get_child(GTK_BIN(textarea)); +} + +char* ui_textarea_get(UiText *text) { + if(text->value.ptr) { + text->value.free(text->value.ptr); + } + GtkTextBuffer *buf = text->obj; + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_bounds(buf, &start, &end); + char *str = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + text->value.ptr = g_strdup(str); + text->value.free = (ui_freefunc)g_free; + return str; +} + +void ui_textarea_set(UiText *text, char *str) { + gtk_text_buffer_set_text((GtkTextBuffer*)text->obj, str, -1); + if(text->value.ptr) { + text->value.free(text->value.ptr); + } + text->value.ptr = NULL; + text->value.free = NULL; +} + +char* ui_textarea_getsubstr(UiText *text, int begin, int end) { + if(text->value.ptr) { + text->value.free(text->value.ptr); + } + GtkTextBuffer *buf = text->obj; + GtkTextIter ib; + GtkTextIter ie; + gtk_text_buffer_get_iter_at_offset(text->obj, &ib, begin); + gtk_text_buffer_get_iter_at_offset(text->obj, &ie, end); + char *str = gtk_text_buffer_get_text(buf, &ib, &ie, FALSE); + text->value.ptr = g_strdup(str); + text->value.free = (ui_freefunc)g_free; + return str; +} + +void ui_textarea_insert(UiText *text, int pos, char *str) { + GtkTextIter offset; + gtk_text_buffer_get_iter_at_offset(text->obj, &offset, pos); + gtk_text_buffer_insert(text->obj, &offset, str, -1); + if(text->value.ptr) { + text->value.free(text->value.ptr); + } + text->value.ptr = NULL; + text->value.free = NULL; +} + +void ui_textarea_setposition(UiText *text, int pos) { + GtkTextIter iter; + gtk_text_buffer_get_iter_at_offset(text->obj, &iter, pos); + gtk_text_buffer_place_cursor(text->obj, &iter); +} + +int ui_textarea_position(UiText *text) { + GtkTextIter begin; + GtkTextIter end; + gtk_text_buffer_get_selection_bounds(text->obj, &begin, &end); + text->pos = gtk_text_iter_get_offset(&begin); + return text->pos; +} + +void ui_textarea_selection(UiText *text, int *begin, int *end) { + GtkTextIter b; + GtkTextIter e; + gtk_text_buffer_get_selection_bounds(text->obj, &b, &e); + *begin = gtk_text_iter_get_offset(&b); + *end = gtk_text_iter_get_offset(&e); +} + +int ui_textarea_length(UiText *text) { + GtkTextBuffer *buf = text->obj; + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_bounds(buf, &start, &end); + return gtk_text_iter_get_offset(&end); +} + +void ui_textarea_remove(UiText *text, int begin, int end) { + GtkTextBuffer *buf = text->obj; + GtkTextIter ib; + GtkTextIter ie; + gtk_text_buffer_get_iter_at_offset(buf, &ib, begin); + gtk_text_buffer_get_iter_at_offset(buf, &ie, end); + gtk_text_buffer_delete(buf, &ib, &ie); +} + +void ui_textarea_realize_event(GtkWidget *widget, gpointer data) { + gtk_widget_grab_focus(widget); +} + + + +void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea) { + UiText *value = textarea->var->value; + if(value->observers) { + UiEvent e; + e.obj = textarea->ctx->obj; + e.window = e.obj->window; + e.document = textarea->ctx->document; + e.eventdata = value; + e.intval = 0; + ui_notify_evt(value->observers, &e); + } +} + +// undo manager functions + +void ui_textbuf_insert( + GtkTextBuffer *textbuffer, + GtkTextIter *location, + char *text, + int length, + void *data) +{ + UiVar *var = data; + UiText *value = var->value; + if(!value->undomgr) { + value->undomgr = ui_create_undomgr(); + } + UiUndoMgr *mgr = value->undomgr; + if(!mgr->event) { + return; + } + + if(mgr->cur) { + UiTextBufOp *elm = mgr->cur->next; + if(elm) { + mgr->cur->next = NULL; + mgr->end = mgr->cur; + while(elm) { + elm->prev = NULL; + UiTextBufOp *next = elm->next; + ui_free_textbuf_op(elm); + elm = next; + } + } + + UiTextBufOp *last_op = mgr->cur; + if( + last_op->type == UI_TEXTBUF_INSERT && + ui_check_insertstr(last_op->text, last_op->len, text, length) == 0) + { + // append text to last op + int ln = last_op->len; + char *newtext = malloc(ln + length + 1); + memcpy(newtext, last_op->text, ln); + memcpy(newtext+ln, text, length); + newtext[ln+length] = '\0'; + + last_op->text = newtext; + last_op->len = ln + length; + last_op->end += length; + + return; + } + } + + char *dpstr = malloc(length + 1); + memcpy(dpstr, text, length); + dpstr[length] = 0; + + UiTextBufOp *op = malloc(sizeof(UiTextBufOp)); + op->prev = NULL; + op->next = NULL; + op->type = UI_TEXTBUF_INSERT; + op->start = gtk_text_iter_get_offset(location); + op->end = op->start+length; + op->len = length; + op->text = dpstr; + + cx_linked_list_add( + (void**)&mgr->begin, + (void**)&mgr->end, + offsetof(UiTextBufOp, prev), + offsetof(UiTextBufOp, next), + op); + + mgr->cur = op; +} + +void ui_textbuf_delete( + GtkTextBuffer *textbuffer, + GtkTextIter *start, + GtkTextIter *end, + void *data) +{ + UiVar *var = data; + UiText *value = var->value; + if(!value->undomgr) { + value->undomgr = ui_create_undomgr(); + } + UiUndoMgr *mgr = value->undomgr; + if(!mgr->event) { + return; + } + + if(mgr->cur) { + UiTextBufOp *elm = mgr->cur->next; + if(elm) { + mgr->cur->next = NULL; + mgr->end = mgr->cur; + while(elm) { + elm->prev = NULL; + UiTextBufOp *next = elm->next; + ui_free_textbuf_op(elm); + elm = next; + } + } + } + + char *text = gtk_text_buffer_get_text(value->obj, start, end, FALSE); + + UiTextBufOp *op = malloc(sizeof(UiTextBufOp)); + op->prev = NULL; + op->next = NULL; + op->type = UI_TEXTBUF_DELETE; + op->start = gtk_text_iter_get_offset(start); + op->end = gtk_text_iter_get_offset(end); + op->len = op->end - op->start; + + char *dpstr = malloc(op->len + 1); + memcpy(dpstr, text, op->len); + dpstr[op->len] = 0; + op->text = dpstr; + + cx_linked_list_add( + (void**)&mgr->begin, + (void**)&mgr->end, + offsetof(UiTextBufOp, prev), + offsetof(UiTextBufOp, next), + op); + + mgr->cur = op; +} + +UiUndoMgr* ui_create_undomgr() { + UiUndoMgr *mgr = malloc(sizeof(UiUndoMgr)); + mgr->begin = NULL; + mgr->end = NULL; + mgr->cur = NULL; + mgr->length = 0; + mgr->event = 1; + return mgr; +} + +void ui_destroy_undomgr(UiUndoMgr *mgr) { + UiTextBufOp *op = mgr->begin; + while(op) { + UiTextBufOp *nextOp = op->next; + if(op->text) { + free(op->text); + } + free(op); + op = nextOp; + } + free(mgr); +} + +void ui_free_textbuf_op(UiTextBufOp *op) { + if(op->text) { + free(op->text); + } + free(op); +} + +int ui_check_insertstr(char *oldstr, int oldlen, char *newstr, int newlen) { + // return 1 if oldstr + newstr are one word + + int has_space = 0; + for(int i=0;i 32) { + return 1; + } + } + + return 0; +} + +void ui_text_undo(UiText *value) { + UiUndoMgr *mgr = value->undomgr; + + if(mgr->cur) { + UiTextBufOp *op = mgr->cur; + mgr->event = 0; + switch(op->type) { + case UI_TEXTBUF_INSERT: { + GtkTextIter begin; + GtkTextIter end; + gtk_text_buffer_get_iter_at_offset(value->obj, &begin, op->start); + gtk_text_buffer_get_iter_at_offset(value->obj, &end, op->end); + gtk_text_buffer_delete(value->obj, &begin, &end); + break; + } + case UI_TEXTBUF_DELETE: { + GtkTextIter begin; + GtkTextIter end; + gtk_text_buffer_get_iter_at_offset(value->obj, &begin, op->start); + gtk_text_buffer_get_iter_at_offset(value->obj, &end, op->end); + gtk_text_buffer_insert(value->obj, &begin, op->text, op->len); + break; + } + } + mgr->event = 1; + mgr->cur = mgr->cur->prev; + } +} + +void ui_text_redo(UiText *value) { + UiUndoMgr *mgr = value->undomgr; + + UiTextBufOp *elm = NULL; + if(mgr->cur) { + if(mgr->cur->next) { + elm = mgr->cur->next; + } + } else if(mgr->begin) { + elm = mgr->begin; + } + + if(elm) { + UiTextBufOp *op = elm; + mgr->event = 0; + switch(op->type) { + case UI_TEXTBUF_INSERT: { + GtkTextIter begin; + GtkTextIter end; + gtk_text_buffer_get_iter_at_offset(value->obj, &begin, op->start); + gtk_text_buffer_get_iter_at_offset(value->obj, &end, op->end); + gtk_text_buffer_insert(value->obj, &begin, op->text, op->len); + break; + } + case UI_TEXTBUF_DELETE: { + GtkTextIter begin; + GtkTextIter end; + gtk_text_buffer_get_iter_at_offset(value->obj, &begin, op->start); + gtk_text_buffer_get_iter_at_offset(value->obj, &end, op->end); + gtk_text_buffer_delete(value->obj, &begin, &end); + break; + } + } + mgr->event = 1; + mgr->cur = elm; + } +} + + +static UIWIDGET create_textfield_var(UiObject *obj, int width, UiBool frameless, UiBool password, UiVar *var) { + GtkWidget *textfield = gtk_entry_new(); + + UiTextField *uitext = malloc(sizeof(UiTextField)); + uitext->ctx = obj->ctx; + uitext->var = var; + + g_signal_connect( + textfield, + "destroy", + G_CALLBACK(ui_textfield_destroy), + uitext); + + if(width > 0) { + gtk_entry_set_width_chars(GTK_ENTRY(textfield), width); + } + if(frameless) { + // TODO: gtk2legacy workaroud + gtk_entry_set_has_frame(GTK_ENTRY(textfield), FALSE); + } + if(password) { + gtk_entry_set_visibility(GTK_ENTRY(textfield), FALSE); + } + + UiContainer *ct = uic_get_current_container(obj); + ct->add(ct, textfield, FALSE); + + if(var) { + UiString *value = var->value; + if(value->value.ptr) { + gtk_entry_set_text(GTK_ENTRY(textfield), value->value.ptr); + value->value.free(value->value.ptr); + value->value.ptr = NULL; + value->value.free = NULL; + } + + value->get = ui_textfield_get; + value->set = ui_textfield_set; + value->value.ptr = NULL; + value->value.free = NULL; + value->obj = GTK_ENTRY(textfield); + + g_signal_connect( + textfield, + "changed", + G_CALLBACK(ui_textfield_changed), + uitext); + } + + 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; +} + +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); +} + +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) { + UiString *value = textfield->var->value; + if(value->observers) { + UiEvent e; + e.obj = textfield->ctx->obj; + e.window = e.obj->window; + e.document = textfield->ctx->document; + e.eventdata = value; + e.intval = 0; + ui_notify_evt(value->observers, &e); + } +} + +UIWIDGET ui_textfield(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(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(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) { + str->value.free(str->value.ptr); + } + str->value.ptr = g_strdup(gtk_entry_get_text(str->obj)); + str->value.free = (ui_freefunc)g_free; + return str->value.ptr; +} + +void ui_textfield_set(UiString *str, char *value) { + gtk_entry_set_text(str->obj, value); + if(str->value.ptr) { + str->value.free(str->value.ptr); + str->value.ptr = NULL; + str->value.free = NULL; + } +}