ltk

GUI toolkit for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/ltk.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
Log | Files | Refs | README | LICENSE

commit 5a0a7594e75569bc17cc48a0ec0e5975ec51d54d
parent d0faf9b6f4464428cac5de55e56cd1a0a92b45ef
Author: lumidify <nobody@lumidify.org>
Date:   Mon,  6 May 2024 23:33:09 +0200

Add basic combobox; improve external command handling

The combobox is very hacky and doesn't behave properly
in all circumstances.

Diffstat:
MMakefile | 2++
Mconfig.example/ltk.cfg | 30+++++++++++++++++++++++++++++-
Mexamples/ltk/test.c | 12++++++++++--
Asrc/ltk/combobox.c | 596+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/combobox.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/ltk/config.c | 265++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/ltk/config.h | 19++++++++++++++++++-
Msrc/ltk/entry.c | 3++-
Msrc/ltk/event_xlib.c | 1+
Msrc/ltk/eventdefs.h | 1+
Msrc/ltk/ltk.c | 236++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/ltk/ltk.h | 6------
Msrc/ltk/memory.h | 2--
Msrc/ltk/menu.c | 55+++++++++++++++++++++++++++++++++++++------------------
Msrc/ltk/menu.h | 6+++++-
Msrc/ltk/text.h | 2++
Msrc/ltk/text_pango.c | 10++++++++++
Msrc/ltk/txtbuf.c | 10++++++++++
Msrc/ltk/txtbuf.h | 13+++++++++++++
Msrc/ltk/util.c | 188-------------------------------------------------------------------------------
Msrc/ltk/widget.h | 2++
Msrc/ltk/widget_internal.h | 13+++++++++++++
Msrc/ltk/window.c | 17+++++++----------
23 files changed, 1214 insertions(+), 318 deletions(-)

diff --git a/Makefile b/Makefile @@ -59,6 +59,7 @@ OBJ_LTK = \ src/ltk/button.o \ src/ltk/checkbutton.o \ src/ltk/radiobutton.o \ + src/ltk/combobox.o \ src/ltk/graphics_xlib.o \ src/ltk/surface_cache.o \ src/ltk/event_xlib.o \ @@ -98,6 +99,7 @@ HDR_LTK = \ src/ltk/button.h \ src/ltk/checkbutton.h \ src/ltk/radiobutton.h \ + src/ltk/combobox.h \ src/ltk/color.h \ src/ltk/label.h \ src/ltk/rect.h \ diff --git a/config.example/ltk.cfg b/config.example/ltk.cfg @@ -1,7 +1,28 @@ [general] explicit-focus = true all-activatable = true + +# FIXME: document weird parsing for commands (quotes, backslashes) +# FIXME: actually test all of these options... +# Options for commands: +# %f: combined input/output file +# %i: input file +# %o: output file +# If %i is specified but %o is not specified, +# output is read from stdout (and vice versa). +# If %f is specified, %i and %o are not allowed. +# If no files are specified, input is written to +# stdin and output is read from stdout. + +# line-editor is given the contents of a line entry +# and must return the edited text. Newlines are +# stripped from the returning text. line-editor = "st -e vi %f" +# option-chooser is given several options, one on +# each line, and must return one of them. If the +# result contains newlines, only the part before +# the first newline is used. +option-chooser = dmenu mixed-dpi = true fixed-dpi = 96 dpi-scale = 1.0 @@ -45,7 +66,11 @@ fg-disabled = "#292929" # bind edit-text-external ... # bind edit-line-external ... bind-keypress move-next sym tab -bind-keypress move-prev sym tab mods shift +# FIXME: how should this be handled? it's a bit weird because +# shift+tab causes left tab + shift under X11, but that +# requires shift to be given here, so maybe that should be +# abstracted away in the backend? +bind-keypress move-prev sym left-tab mods shift bind-keypress move-next text n bind-keypress move-prev text p bind-keypress move-left sym left @@ -81,6 +106,9 @@ bind-keypress paste-clipboard text v mods ctrl bind-keypress switch-selection-side text o mods alt bind-keypress edit-external text E mods ctrl +[key-binding:combobox] +bind-keypress choose-external text E mods ctrl + # default mapping (just to silence warnings) [key-mapping] language = "English (US)" diff --git a/examples/ltk/test.c b/examples/ltk/test.c @@ -11,6 +11,7 @@ #include <ltk/box.h> #include <ltk/checkbutton.h> #include <ltk/radiobutton.h> +#include <ltk/combobox.h> int quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) { @@ -93,7 +94,14 @@ main(int argc, char *argv[]) { ltk_box_add(box, LTK_CAST_WIDGET(rbtn1), LTK_STICKY_LEFT); ltk_box_add(box, LTK_CAST_WIDGET(rbtn2), LTK_STICKY_LEFT); - ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT); + ltk_combobox *combo = ltk_combobox_create(window); + ltk_combobox_add_option(combo, "Option 1"); + ltk_combobox_add_option(combo, "Option 2"); + ltk_combobox_add_option(combo, "Option 3"); + ltk_combobox_add_option(combo, "Option 4"); + + ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT); + ltk_grid_add(grid, LTK_CAST_WIDGET(combo), 0, 1, 1, 1, LTK_STICKY_LEFT); ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT); ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT); ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT); @@ -102,7 +110,7 @@ main(int argc, char *argv[]) { ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM); ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid)); ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID); - ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID); + ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_MENUENTRY_SIGNAL_PRESSED, &quit, LTK_ARG_VOID); ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5)); ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID); ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_WIDGET_SIGNAL_CHANGE_STATE, &printstate, LTK_ARG_VOID); diff --git a/src/ltk/combobox.c b/src/ltk/combobox.c @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2024 lumidify <nobody@lumidify.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> + +#include "config.h" +#include "combobox.h" +#include "color.h" +#include "graphics.h" +#include "ltk.h" +#include "memory.h" +#include "rect.h" +#include "text.h" +#include "util.h" +#include "widget.h" +#include "menu.h" +#include "widget_internal.h" + +#define MAX_COMBOBOX_BORDER_WIDTH 10000 +#define MAX_COMBOBOX_PADDING 50000 +#define MAX_COMBOBOX_ARROW_SIZE 50000 + +static void ltk_combobox_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); +static int ltk_combobox_release(ltk_widget *self); +static void ltk_combobox_destroy(ltk_widget *self, int shallow); +static void ltk_combobox_recalc_ideal_size(ltk_widget *self); +static int ltk_combobox_remove_child(ltk_widget *self, ltk_widget *widget); +static ltk_widget *ltk_combobox_get_child(ltk_widget *self); +static ltk_widget *ltk_combobox_nearest_child(ltk_widget *self, ltk_rect rect); +static int ltk_combobox_key_press(ltk_widget *self, ltk_key_event *event); +static int choose_external(ltk_widget *self, ltk_key_event *event); +static void ltk_combobox_cmd_return(ltk_widget *self, char *text, size_t len); + +static struct ltk_widget_vtable vtable = { + .key_press = &ltk_combobox_key_press, + .key_release = NULL, + .mouse_press = NULL, + .mouse_release = NULL, + .release = &ltk_combobox_release, + .motion_notify = NULL, + .mouse_leave = NULL, + .mouse_enter = NULL, + .change_state = NULL, + .get_child_at_pos = NULL, + .cmd_return = &ltk_combobox_cmd_return, + .resize = NULL, + .hide = NULL, + .draw = &ltk_combobox_draw, + .destroy = &ltk_combobox_destroy, + .child_size_change = NULL, + .remove_child = &ltk_combobox_remove_child, + .first_child = &ltk_combobox_get_child, + .last_child = &ltk_combobox_get_child, + .nearest_child = &ltk_combobox_nearest_child, + .recalc_ideal_size = &ltk_combobox_recalc_ideal_size, + .type = LTK_WIDGET_COMBOBOX, + .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS, + .invalid_signal = LTK_COMBOBOX_SIGNAL_INVALID, +}; + +static struct { + ltk_color *border; + ltk_color *border_pressed; + ltk_color *border_hover; + ltk_color *border_active; + ltk_color *border_disabled; + ltk_color *fill; + ltk_color *fill_pressed; + ltk_color *fill_hover; + ltk_color *fill_active; + ltk_color *fill_disabled; + ltk_color *text; + ltk_color *text_pressed; + ltk_color *text_hover; + ltk_color *text_active; + ltk_color *text_disabled; + + char *font; + ltk_size arrow_size; + ltk_size border_width; + ltk_size pad; + ltk_size font_size; + int compress_borders; +} theme; + +static ltk_theme_parseinfo parseinfo[] = { + {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0}, + {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0}, + {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#738194"}, 0, 0, 0}, + {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0}, + {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0}, + {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0}, + {"text", THEME_COLOR, {.color = &theme.text}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"text-hover", THEME_COLOR, {.color = &theme.text_hover}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"text-active", THEME_COLOR, {.color = &theme.text_active}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"text-disabled", THEME_COLOR, {.color = &theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0}, + {"text-pressed", THEME_COLOR, {.color = &theme.text_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, + + {"arrow-size", THEME_SIZE, {.size = &theme.arrow_size}, {.size = {.val = 250, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_ARROW_SIZE, 0}, + {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_BORDER_WIDTH, 0}, + {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_PADDING, 0}, + {"compress-borders", THEME_BOOL, {.b = &theme.compress_borders}, {.b = 0}, 0, MAX_COMBOBOX_PADDING, 0}, + {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0}, + {"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0}, +}; + +void +ltk_combobox_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) { + *p = parseinfo; + *len = LENGTH(parseinfo); +} + +static ltk_keybinding_cb cb_map[] = { + {"choose-external", &choose_external}, +}; + +static ltk_array(keypress) *keypresses = NULL; + +void +ltk_combobox_get_keybinding_parseinfo( + ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret, + ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret, + ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret +) { + *press_cbs_ret = cb_map; + *press_len_ret = LENGTH(cb_map); + *release_cbs_ret = NULL; + *release_len_ret = 0; + if (!keypresses) + keypresses = ltk_array_create(keypress, 1); + *presses_ret = keypresses; + *releases_ret = NULL; +} + +void +ltk_combobox_cleanup(void) { + ltk_keypress_bindings_destroy(keypresses); + keypresses = NULL; +} + +/* FIXME: a lot more theme settings */ +static void +ltk_combobox_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) { + ltk_combobox *combobox = LTK_CAST_COMBOBOX(self); + ltk_rect lrect = self->lrect; + ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h}); + if (clip_final.w <= 0 || clip_final.h <= 0) + return; + + int arrow_size = ltk_size_to_pixel(theme.arrow_size, self->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, self->last_dpi); + int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); + ltk_color *border = NULL, *fill = NULL, *text = NULL; + if (self->state & LTK_DISABLED) { + border = theme.border_disabled; + fill = theme.fill_disabled; + text = theme.text_disabled; + } else if (self->state & LTK_PRESSED) { + border = theme.border_pressed; + fill = theme.fill_pressed; + text = theme.text_pressed; + } else if (self->state & LTK_HOVER) { + border = theme.border_hover; + fill = theme.fill_hover; + text = theme.text_hover; + } else if (self->state & LTK_ACTIVE) { + border = theme.border_active; + fill = theme.fill_active; + text = theme.text_active; + } else { + border = theme.border; + fill = theme.fill; + text = theme.text; + } + ltk_rect draw_rect = {x, y, lrect.w, lrect.h}; + ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h}; + ltk_surface_fill_rect(draw_surf, fill, draw_clip); + if (bw > 0) { + ltk_surface_draw_border_clipped( + draw_surf, border, draw_rect, bw, LTK_BORDER_ALL, draw_clip + ); + } + int text_w, text_h; + ltk_text_line_get_size(combobox->tl, &text_w, &text_h); + int text_x = x + pad; + int text_y = y + (lrect.h - text_h) / 2; + ltk_text_line_draw_clipped(combobox->tl, draw_surf, text, text_x, text_y, draw_clip); + + ltk_point arrow_points[] = { + {x + lrect.w - pad - bw - arrow_size, y + lrect.h / 2 - arrow_size / 2}, + {x + lrect.w - pad - bw, y + lrect.h / 2 - arrow_size / 2}, + {x + lrect.w - pad - bw - arrow_size / 2, y + lrect.h / 2 + arrow_size / 2} + }; + ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), draw_clip); + self->dirty = 0; +} + +/* FIXME: this is kind of ugly because it uses a lot of internal knowledge about menus */ +static void +popup_dropdown(ltk_combobox *combobox) { + if (!combobox->dropdown || ltk_menu_get_num_entries(combobox->dropdown) == 0) + return; + ltk_rect combo_rect = LTK_CAST_WIDGET(combobox)->lrect; + ltk_point combo_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(combobox), 0, 0); + + int win_w = LTK_CAST_WIDGET(combobox)->window->rect.w; + int win_h = LTK_CAST_WIDGET(combobox)->window->rect.h; + ltk_menu *dropdown = combobox->dropdown; + ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(dropdown)); + int ideal_w = dropdown->widget.ideal_w; + int ideal_h = dropdown->widget.ideal_h; + int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h; + int combo_bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(combobox)->last_dpi); + + int space_top = combo_global.y; + int space_bottom = win_h - (combo_global.y + combo_rect.h); + int y_top = combo_global.y - ideal_h; + int y_bottom = combo_global.y + combo_rect.h; + if (theme.compress_borders) { + y_top += combo_bw; + y_bottom -= combo_bw; + } + if (space_top > space_bottom) { + y_final = y_top; + if (y_final < 0) { + y_final = 0; + h_final = combo_rect.y; + } + } else { + y_final = y_bottom; + if (space_bottom < ideal_h) + h_final = space_bottom; + } + /* FIXME: maybe threshold so there's always at least a part of + the menu contents shown (instead of maybe just a few pixels) */ + /* pathological case where window is way too small */ + if (h_final <= 0) { + y_final = 0; + h_final = win_h; + } + x_final = combo_global.x; + if (x_final + ideal_w > win_w) + x_final = win_w - ideal_w; + if (x_final < 0) { + x_final = 0; + w_final = win_w; + } + + /* reset everything just in case */ + dropdown->x_scroll_offset = dropdown->y_scroll_offset = 0; + dropdown->scroll_top_hover = dropdown->scroll_bottom_hover = 0; + dropdown->scroll_left_hover = dropdown->scroll_right_hover = 0; + dropdown->widget.lrect.x = x_final; + dropdown->widget.lrect.y = y_final; + dropdown->widget.lrect.w = w_final; + dropdown->widget.lrect.h = h_final; + dropdown->widget.crect = LTK_CAST_WIDGET(dropdown)->lrect; + dropdown->widget.dirty = 1; + dropdown->widget.hidden = 0; + dropdown->popup_submenus = 0; + dropdown->unpopup_submenus_on_hide = 1; + ltk_widget_resize(LTK_CAST_WIDGET(dropdown)); + ltk_window_register_popup(LTK_CAST_WIDGET(combobox)->window, LTK_CAST_WIDGET(dropdown)); + ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(dropdown)->window, LTK_CAST_WIDGET(dropdown)); +} + +static void +unpopup_dropdown(ltk_combobox *combobox) { + if (combobox->dropdown && !LTK_CAST_WIDGET(combobox->dropdown)->hidden) { + ltk_widget_hide(LTK_CAST_WIDGET(combobox->dropdown)); + } +} + +/* FIXME: set ideal width to ideal width of submenu */ +/* FIXME: disable button when no options */ + +static int +ltk_combobox_release(ltk_widget *self) { + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (!combo->dropdown) + return 0; + if (combo->dropdown->widget.hidden) + popup_dropdown(combo); + else + unpopup_dropdown(combo); + return 1; +} + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +static void +recalc_ideal_size(ltk_combobox *combobox) { + int text_w, text_h; + ltk_text_line_get_size(combobox->tl, &text_w, &text_h); + int arrow_size = ltk_size_to_pixel(theme.arrow_size, LTK_CAST_WIDGET(combobox)->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(combobox)->last_dpi); + combobox->widget.ideal_w = text_w + pad * 3 + arrow_size; + combobox->widget.ideal_h = MAX(text_h, arrow_size) + pad * 2; +} + +static void +ltk_combobox_recalc_ideal_size(ltk_widget *self) { + ltk_combobox *combobox = LTK_CAST_COMBOBOX(self); + int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi); + ltk_text_line_set_font_size(combobox->tl, font_size); + recalc_ideal_size(combobox); +} + +static void +combobox_set_active(ltk_combobox *combo, size_t idx, const char *text) { + combo->cur_active = idx; + ltk_text_line_set_const_text(combo->tl, text); + recalc_ideal_size(combo); + if (combo->widget.parent && combo->widget.parent->vtable->child_size_change) { + combo->widget.parent->vtable->child_size_change(combo->widget.parent, LTK_CAST_WIDGET(combo)); + } + ltk_widget_emit_signal(LTK_CAST_WIDGET(combo), LTK_COMBOBOX_SIGNAL_CHANGED, LTK_EMPTY_ARGLIST); +} + +static int +handle_entry_pressed(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) { + (void)args; + ltk_menuentry *e = LTK_CAST_MENUENTRY(self); + ltk_combobox *combo = LTK_CAST_COMBOBOX(LTK_CAST_ARG_WIDGET(data)); + if (!combo->dropdown) /* shouldn't be possible */ + return 1; + size_t idx = ltk_menu_get_entry_index(combo->dropdown, e); + if (idx == SIZE_MAX) /* shouldn't be possible */ + return 1; + combobox_set_active(combo, idx, ltk_menuentry_get_text(e)); + return 1; +} + +static void +ltk_combobox_cmd_return(ltk_widget *self, char *text, size_t len) { + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (!combo->dropdown) + return; + /* need to copy since it's not nul-terminated */ + char *textcopy = ltk_strndup(text, len); + char *nl = strchr(textcopy, '\n'); + /* only take text until first newline into account */ + if (nl) + *nl = '\0'; + for (size_t i = 0; i < ltk_menu_get_num_entries(combo->dropdown); i++) { + if (!strcmp(textcopy, ltk_menuentry_get_text(ltk_menu_get_entry(combo->dropdown, i)))) { + combobox_set_active(combo, i, textcopy); + break; + } + } + ltk_free(textcopy); +} + +static int +choose_external(ltk_widget *self, ltk_key_event *event) { + (void)event; + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (!combo->dropdown || ltk_menu_get_num_entries(combo->dropdown) == 0) + return 0; + ltk_general_config *config = ltk_config_get_general(); + /* FIXME: allow arguments to key mappings - this would allow to have different key mappings + for different editors instead of just one command */ + if (!config->option_chooser) { + ltk_warn("Unable to run external option choosing command: option chooser not configured."); + } else { + /* FIXME: somehow show that there was an error if this returns 1? */ + /* FIXME: change interface to not require length of cmd */ + txtbuf *tmpbuf = txtbuf_new(); + for (size_t i = 0; i < ltk_menu_get_num_entries(combo->dropdown); i++) { + txtbuf_append(tmpbuf, ltk_menuentry_get_text(ltk_menu_get_entry(combo->dropdown, i))); + txtbuf_append(tmpbuf, "\n"); + } + ltk_call_cmd(self, config->option_chooser, txtbuf_get_text(tmpbuf), txtbuf_len(tmpbuf)); + txtbuf_destroy(tmpbuf); + } + return 0; +} + +static int +ltk_combobox_key_press(ltk_widget *self, ltk_key_event *event) { + ltk_keypress_binding b; + for (size_t i = 0; i < ltk_array_len(keypresses); i++) { + b = ltk_array_get(keypresses, i).b; + if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) || + (b.mods == (event->modmask & ~LTK_MOD_SHIFT) && + ((b.text && event->mapped && !strcmp(b.text, event->mapped)) || + (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) { + ltk_array_get(keypresses, i).cb.func(self, event); + self->dirty = 1; + ltk_window_invalidate_widget_rect(self->window, self); + return 1; + } + } + return 0; +} + +const char * +ltk_combobox_get_text(ltk_combobox *combo) { + if (!combo->dropdown) + return NULL; + ltk_menuentry *e = ltk_menu_get_entry(combo->dropdown, combo->cur_active); + if (!e) + return NULL; + return ltk_menuentry_get_text(e); +} + +size_t +ltk_combobox_get_index(ltk_combobox *combo) { + return combo->cur_active; +} + +/* FIXME: this is really hacky - it was added to remove some weird effects when moving + around with keyboard shortcuts */ +/* FIXME: movement is still weird, for instance when pressing left on the dropdown, + focus moves to the combobox, not to the widget to the left - maybe there needs to be + another widget flag so the combobox is activatable but isn't taken into account when + moving back up from the child to the parent */ +/* FIXME: maybe just have a dedicated dropdown instead of reusing a menu in order to fix + these weirdnesses? */ +static int +handle_dropdown_change_state(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) { + (void)args; + ltk_menu *menu = LTK_CAST_MENU(self); + ltk_combobox *combo = LTK_CAST_COMBOBOX(LTK_CAST_ARG_WIDGET(data)); + if (menu != combo->dropdown) /* should never happen */ + return 0; + if (!(menu->widget.state & LTK_ACTIVE) && !menu->widget.hidden) + ltk_widget_hide(self); + return 0; +} + +int +ltk_combobox_insert_option(ltk_combobox *combobox, const char *option, size_t idx) { + unpopup_dropdown(combobox); /* just to avoid weird effects */ + if (!combobox->dropdown) { + combobox->dropdown = ltk_submenu_create(LTK_CAST_WIDGET(combobox)->window); + LTK_CAST_WIDGET(combobox->dropdown)->parent = LTK_CAST_WIDGET(combobox); + ltk_widget_register_signal_handler( + LTK_CAST_WIDGET(combobox->dropdown), LTK_WIDGET_SIGNAL_CHANGE_STATE, + &handle_dropdown_change_state, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox)) + ); + } + ltk_menuentry *e = ltk_menuentry_create(LTK_CAST_WIDGET(combobox)->window, option); + if (ltk_menu_insert_entry(combobox->dropdown, e, idx)) { + ltk_widget_destroy(LTK_CAST_WIDGET(e), 0); + return 1; + } + size_t num = ltk_menu_get_num_entries(combobox->dropdown); + if (num == 1) { + combobox_set_active(combobox, 0, option); + } else if (idx <= combobox->cur_active && combobox->cur_active < num) { + combobox->cur_active++; + } + ltk_widget_register_signal_handler( + LTK_CAST_WIDGET(e), LTK_MENUENTRY_SIGNAL_PRESSED, + &handle_entry_pressed, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox)) + ); + return 0; +} + +int +ltk_combobox_add_option(ltk_combobox *combobox, const char *option) { + /* it's easier to just completely ban options with newlines instead of + dealing with weird cases where the external option-chooser splits + options at newlines */ + /* FIXME: should any other chars be banned? */ + if (strchr(option, '\n')) + return 1; + unpopup_dropdown(combobox); /* just to avoid weird effects */ + if (!combobox->dropdown) { + combobox->dropdown = ltk_submenu_create(LTK_CAST_WIDGET(combobox)->window); + LTK_CAST_WIDGET(combobox->dropdown)->parent = LTK_CAST_WIDGET(combobox); + ltk_widget_register_signal_handler( + LTK_CAST_WIDGET(combobox->dropdown), LTK_WIDGET_SIGNAL_CHANGE_STATE, + &handle_dropdown_change_state, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox)) + ); + } + ltk_menuentry *e = ltk_menuentry_create(LTK_CAST_WIDGET(combobox)->window, option); + /* this should never fail */ + ltk_menu_add_entry(combobox->dropdown, e); + size_t num = ltk_menu_get_num_entries(combobox->dropdown); + if (num == 1) { + combobox_set_active(combobox, 0, option); + } + ltk_widget_register_signal_handler( + LTK_CAST_WIDGET(e), LTK_MENUENTRY_SIGNAL_PRESSED, + &handle_entry_pressed, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox)) + ); + return 0; +} + +int +ltk_combobox_remove_option_index(ltk_combobox *combobox, size_t idx) { + if (!combobox->dropdown) + return 1; + unpopup_dropdown(combobox); /* just to avoid weird effects */ + ltk_menuentry *e = ltk_menu_remove_entry_index(combobox->dropdown, idx); + if (!e) return 1; + ltk_widget_destroy(LTK_CAST_WIDGET(e), 0); + if (idx == combobox->cur_active) { + size_t num = ltk_menu_get_num_entries(combobox->dropdown); + if (num == 0) { + combobox_set_active(combobox, SIZE_MAX, ""); + } else { + e = ltk_menu_get_entry(combobox->dropdown, combobox->cur_active); + if (!e) ltk_fatal("Unable to get menu entry. This should not happen."); + combobox_set_active(combobox, idx >= num ? num - 1 : idx, ltk_menuentry_get_text(e)); + } + } + return 0; +} + +void +ltk_combobox_remove_all_options(ltk_combobox *combobox) { + if (!combobox->dropdown) + return; + unpopup_dropdown(combobox); /* just to avoid weird effects */ + ltk_menu_remove_all_entries(combobox->dropdown); + combobox_set_active(combobox, SIZE_MAX, ""); +} + +/* NOTE: This should never be called since the dropdown is managed + completely by the combobox, but it's here just in case. */ +static int +ltk_combobox_remove_child(ltk_widget *self, ltk_widget *widget) { + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (widget != LTK_CAST_WIDGET(combo->dropdown)) + return 1; + widget->parent = NULL; + combo->dropdown = NULL; + return 0; +} + +static ltk_widget * +ltk_combobox_get_child(ltk_widget *self) { + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (combo->dropdown && !combo->dropdown->widget.hidden) + return LTK_CAST_WIDGET(combo->dropdown); + return NULL; +} + +static ltk_widget * +ltk_combobox_nearest_child(ltk_widget *self, ltk_rect rect) { + (void)rect; + return ltk_combobox_get_child(self); +} + +ltk_combobox * +ltk_combobox_create(ltk_window *window) { + ltk_combobox *combobox = ltk_malloc(sizeof(ltk_combobox)); + ltk_fill_widget_defaults(LTK_CAST_WIDGET(combobox), window, &vtable, 0, 0); + combobox->dropdown = NULL; + combobox->cur_active = SIZE_MAX; + + /* FIXME: only create once text has been added */ + combobox->tl = ltk_text_line_create_const_text_default( + theme.font, ltk_size_to_pixel(theme.font_size, combobox->widget.last_dpi), "", -1 + ); + recalc_ideal_size(combobox); + combobox->widget.dirty = 1; + + return combobox; +} + +static void +ltk_combobox_destroy(ltk_widget *self, int shallow) { + (void)shallow; + ltk_combobox *combo = LTK_CAST_COMBOBOX(self); + if (!combo) { + ltk_warn("Tried to destroy NULL combobox.\n"); + return; + } + ltk_text_line_destroy(combo->tl); + if (combo->dropdown) { + LTK_CAST_WIDGET(combo->dropdown)->parent = NULL; + ltk_widget_destroy(LTK_CAST_WIDGET(combo->dropdown), 0); + } + ltk_free(combo); +} diff --git a/src/ltk/combobox.h b/src/ltk/combobox.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 lumidify <nobody@lumidify.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef LTK_COMBOBOX_H +#define LTK_COMBOBOX_H + +#include "text.h" +#include "widget.h" +#include "window.h" +#include "menu.h" + +#define LTK_COMBOBOX_SIGNAL_CHANGED -1 +#define LTK_COMBOBOX_SIGNAL_INVALID -2 + +typedef struct { + ltk_widget widget; + ltk_text_line *tl; + ltk_menu *dropdown; + size_t cur_active; +} ltk_combobox; + +ltk_combobox *ltk_combobox_create(ltk_window *window); +int ltk_combobox_insert_option(ltk_combobox *combobox, const char *option, size_t idx); +int ltk_combobox_add_option(ltk_combobox *combobox, const char *option); +int ltk_combobox_remove_option_index(ltk_combobox *combobox, size_t idx); +void ltk_combobox_remove_all_options(ltk_combobox *combobox); +const char *ltk_combobox_get_text(ltk_combobox *combo); +size_t ltk_combobox_get_index(ltk_combobox *combo); + +#endif /* LTK_COMBOBOX_H */ diff --git a/src/ltk/config.c b/src/ltk/config.c @@ -35,8 +35,11 @@ static ltk_general_config general_config; static ltk_language_mapping *mappings = NULL; static size_t mappings_alloc = 0, mappings_len = 0; +static ltk_array(cmd) *ltk_parse_cmd(const char *cmdtext, size_t len); + static ltk_theme_parseinfo general_parseinfo[] = { - {"line-editor", THEME_STRING, {.str = &general_config.line_editor}, {.str = NULL}, 0, 0, 0}, + {"line-editor", THEME_CMD, {.cmd = &general_config.line_editor}, {.cmd = NULL}, 0, 0, 0}, + {"option-chooser", THEME_CMD, {.cmd = &general_config.option_chooser}, {.cmd = NULL}, 0, 0, 0}, {"dpi-scale", THEME_DOUBLE, {.d = &general_config.dpi_scale}, {.d = 1.0}, 10, 10000, 0}, {"explicit-focus", THEME_BOOL, {.b = &general_config.explicit_focus}, {.b = 0}, 0, 0, 0}, {"all-activatable", THEME_BOOL, {.b = &general_config.all_activatable}, {.b = 0}, 0, 0, 0}, @@ -64,6 +67,7 @@ static struct { ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret ); } keybinding_handlers[] = { + {"combobox", &ltk_combobox_get_keybinding_parseinfo}, {"entry", &ltk_entry_get_keybinding_parseinfo}, {"window", &ltk_window_get_keybinding_parseinfo}, }; @@ -86,6 +90,7 @@ static struct theme_handlerinfo { {"theme:submenuentry", &ltk_submenuentry_get_theme_parseinfo, "theme:window", 0}, {"theme:checkbutton", &ltk_checkbutton_get_theme_parseinfo, "theme:window", 0}, {"theme:radiobutton", &ltk_radiobutton_get_theme_parseinfo, "theme:window", 0}, + {"theme:combobox", &ltk_combobox_get_theme_parseinfo, "theme:window", 0}, }; GEN_SORT_SEARCH_HELPERS(themehandler, struct theme_handlerinfo, name) @@ -101,6 +106,39 @@ sort_themehandlers(void) { } } +LTK_ARRAY_INIT_FUNC_DECL_STATIC(cmdpiece, struct ltk_cmd_piece) +LTK_ARRAY_INIT_IMPL_STATIC(cmdpiece, struct ltk_cmd_piece) +LTK_ARRAY_INIT_FUNC_DECL_STATIC(cmd, ltk_array(cmdpiece) *) +LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_array(cmdpiece) *) + +static void +cmd_piece_free_helper(struct ltk_cmd_piece p) { + if (p.text) + ltk_free(p.text); +} + +static void +cmd_free_helper(ltk_array(cmdpiece) *arr) { + ltk_array_destroy_deep(cmdpiece, arr, &cmd_piece_free_helper); +} + +static ltk_array(cmd) * +copy_cmd(ltk_array(cmd) *cmd) { + ltk_array(cmd) *cmdcopy = ltk_array_create(cmd, ltk_array_len(cmd)); + for (size_t i = 0; i < ltk_array_len(cmd); i++) { + ltk_array(cmdpiece) *piece = ltk_array_get(cmd, i); + ltk_array(cmdpiece) *piececopy = ltk_array_create(cmdpiece, ltk_array_len(piece)); + for (size_t j = 0; j < ltk_array_len(piece); j++) { + struct ltk_cmd_piece p = {NULL, ltk_array_get(piece, j).type}; + if (ltk_array_get(piece, j).text) + p.text = ltk_strdup(ltk_array_get(piece, j).text); + ltk_array_append(cmdpiece, piececopy, p); + } + ltk_array_append(cmd, cmdcopy, piececopy); + } + return cmdcopy; +} + /* FIXME: handle '#' or no '#' in color specification */ static int handle_theme_setting(ltk_renderdata *renderdata, ltk_theme_parseinfo *entry, const char *value) { @@ -170,6 +208,11 @@ handle_theme_setting(ltk_renderdata *renderdata, ltk_theme_parseinfo *entry, con return 1; entry->initialized = 1; break; + case THEME_CMD: + if (!(*(entry->ptr.cmd) = ltk_parse_cmd(value, strlen(value)))) + return 1; + entry->initialized = 1; + break; case THEME_BOOL: if (strcmp(value, "true") == 0) { *(entry->ptr.b) = 1; @@ -266,11 +309,25 @@ fill_single_theme_defaults(ltk_renderdata *renderdata, struct theme_handlerinfo if (ep) { if (!(*(e->ptr.color) = ltk_color_copy(renderdata, *(ep->ptr.color)))) return 1; + } else if (!e->defaultval.color) { + return 1; /* colors must always be initialized */ } else if (!(*(e->ptr.color) = ltk_color_create(renderdata, e->defaultval.color))) { return 1; } e->initialized = 1; break; + case THEME_CMD: + if (ep) { + /* There is no reason to ever use this, but whatever */ + if (!(*(e->ptr.cmd) = copy_cmd(*(ep->ptr.cmd)))) + return 1; + } else if (!e->defaultval.cmd) { + *(e->ptr.cmd) = NULL; + } else if (!(*(e->ptr.cmd) = ltk_parse_cmd(e->defaultval.cmd, strlen(e->defaultval.cmd)))) { + return 1; + } + e->initialized = 1; + break; case THEME_BOOL: *(e->ptr.b) = ep ? *(ep->ptr.b) : e->defaultval.b; e->initialized = 1; @@ -316,6 +373,10 @@ uninitialize_theme(ltk_renderdata *renderdata) { ltk_color_destroy(renderdata, *(e->ptr.color)); e->initialized = 0; break; + case THEME_CMD: + ltk_array_destroy_deep(cmd, *(e->ptr.cmd), &cmd_free_helper); + e->initialized = 0; + break; case THEME_SIZE: case THEME_INT: case THEME_UINT: @@ -878,7 +939,7 @@ ltk_config_get_language_mapping(size_t idx) { return &mappings[idx]; } -int +static int str_array_prefix(const char *str, const char *ar, size_t len) { size_t slen = strlen(str); if (len < slen) @@ -1086,7 +1147,7 @@ ltk_config_parsefile(ltk_renderdata *renderdata, const char *filename, char **er } /* FIXME: update this */ -const char *default_config = "[general]\n" +static const char *default_config = "[general]\n" "explicit-focus = true\n" "all-activatable = true\n" "[key-binding:window]\n" @@ -1179,6 +1240,7 @@ static struct keysym_mapping { {"kp-up", LTK_KEY_KP_UP}, {"left", LTK_KEY_LEFT}, + {"left-tab", LTK_KEY_LEFT_TAB}, {"linefeed", LTK_KEY_LINEFEED}, {"menu", LTK_KEY_MENU}, {"mode-switch", LTK_KEY_MODE_SWITCH}, @@ -1218,3 +1280,200 @@ parse_keysym(char *keysym_str, size_t len, ltk_keysym *sym) { *sym = km->keysym; return 0; } + +/* FIXME: this is really ugly */ +/* FIXME: this handles double-quote, but the config parser already uses that, so + it's kind of weird because it's parsed twice (also backslashes are parsed twice). */ +static ltk_array(cmd) * +ltk_parse_cmd(const char *cmdtext, size_t len) { + int bs = 0; + int in_sqstr = 0; + int in_dqstr = 0; + int in_ws = 1; + int inout_used = 0, input_used = 0, output_used = 0; + char c; + size_t cur_start = 0; + int offset = 0; + ltk_array(cmdpiece) *cur_arg = ltk_array_create(cmdpiece, 1); + ltk_array(cmd) *cmd = ltk_array_create(cmd, 4); + char *cmdcopy = ltk_strndup(cmdtext, len); + for (size_t i = 0; i < len; i++) { + c = cmdcopy[i]; + if (c == '\\') { + if (bs) { + offset++; + bs = 0; + } else { + bs = 1; + } + } else if (isspace(c)) { + if (!in_sqstr && !in_dqstr) { + if (bs) { + if (in_ws) { + in_ws = 0; + cur_start = i; + offset = 0; + } else { + offset++; + } + bs = 0; + } else if (!in_ws) { + /* FIXME: shouldn't this be < instead of <=? */ + if (cur_start <= i - offset) { + struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, i - cur_start - offset), LTK_CMD_TEXT}; + ltk_array_append(cmdpiece, cur_arg, p); + } + /* FIXME: cmd is named horribly */ + ltk_array_append(cmd, cmd, cur_arg); + cur_arg = ltk_array_create(cmdpiece, 1); + in_ws = 1; + offset = 0; + } + /* FIXME: parsing weird here - bs just ignored */ + } else if (bs) { + bs = 0; + } + } else if (c == '%') { + if (bs) { + if (in_ws) { + cur_start = i; + offset = 0; + } else { + offset++; + } + bs = 0; + } else if (!in_sqstr && i < len - 1 && (cmdcopy[i + 1] == 'f' || cmdcopy[i + 1] == 'i' || cmdcopy[i + 1] == 'o')) { + if (!in_ws && cur_start < i - offset) { + struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, i - cur_start - offset), LTK_CMD_TEXT}; + ltk_array_append(cmdpiece, cur_arg, p); + } + struct ltk_cmd_piece p = {NULL, LTK_CMD_INOUT_FILE}; + switch (cmdcopy[i + 1]) { + case 'f': + p.type = LTK_CMD_INOUT_FILE; + if (input_used || output_used) + goto error; + inout_used = 1; + break; + case 'i': + p.type = LTK_CMD_INPUT_FILE; + if (inout_used) + goto error; + input_used = 1; + break; + case 'o': + p.type = LTK_CMD_OUTPUT_FILE; + if (inout_used) + goto error; + output_used = 1; + break; + default: + ltk_fatal("Impossible."); + } + ltk_array_append(cmdpiece, cur_arg, p); + i++; + cur_start = i + 1; + offset = 0; + } else if (in_ws) { + cur_start = i; + offset = 0; + } + in_ws = 0; + } else if (c == '"') { + if (in_sqstr) { + bs = 0; + } else if (bs) { + if (in_ws) { + cur_start = i; + offset = 0; + } else { + offset++; + } + bs = 0; + } else if (in_dqstr) { + offset++; + in_dqstr = 0; + continue; + } else { + in_dqstr = 1; + if (in_ws) { + cur_start = i + 1; + offset = 0; + } else { + offset++; + continue; + } + } + in_ws = 0; + } else if (c == '\'') { + if (in_dqstr) { + bs = 0; + } else if (bs) { + if (in_ws) { + cur_start = i; + offset = 0; + } else { + offset++; + } + bs = 0; + } else if (in_sqstr) { + offset++; + in_sqstr = 0; + continue; + } else { + in_sqstr = 1; + if (in_ws) { + cur_start = i + 1; + offset = 0; + } else { + offset++; + continue; + } + } + in_ws = 0; + } else if (bs) { + if (!in_sqstr && !in_dqstr) { + if (in_ws) { + cur_start = i; + offset = 0; + } else { + offset++; + } + } + bs = 0; + in_ws = 0; + } else { + if (in_ws) { + cur_start = i; + offset = 0; + } + in_ws = 0; + } + cmdcopy[i - offset] = cmdcopy[i]; + } + /* FIXME: proper error messages with errstr */ + if (in_sqstr || in_dqstr) { + /*ltk_warn("Unterminated string in command\n");*/ + goto error; + } + if (!in_ws) { + if (cur_start <= len - offset) { + struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, len - cur_start - offset), LTK_CMD_TEXT}; + ltk_array_append(cmdpiece, cur_arg, p); + } + ltk_array_append(cmd, cmd, cur_arg); + cur_arg = NULL; + } + if (cmd->len == 0) { + /*ltk_warn("Empty command\n");*/ + goto error; + } + ltk_free(cmdcopy); + return cmd; +error: + ltk_free(cmdcopy); + if (cur_arg) + ltk_array_destroy_deep(cmdpiece, cur_arg, &cmd_piece_free_helper); + ltk_array_destroy_deep(cmd, cmd, &cmd_free_helper); + return NULL; +} diff --git a/src/ltk/config.h b/src/ltk/config.h @@ -55,8 +55,22 @@ typedef struct { size_t mappings_alloc, mappings_len; } ltk_language_mapping; +struct ltk_cmd_piece { + char *text; + enum { + LTK_CMD_TEXT, + LTK_CMD_INPUT_FILE, + LTK_CMD_OUTPUT_FILE, + LTK_CMD_INOUT_FILE, + } type; +}; + +LTK_ARRAY_INIT_STRUCT_DECL(cmdpiece, struct ltk_cmd_piece) +LTK_ARRAY_INIT_STRUCT_DECL(cmd, ltk_array(cmdpiece) *) + typedef struct { - char *line_editor; + ltk_array(cmd) *line_editor; + ltk_array(cmd) *option_chooser; double dpi_scale; double fixed_dpi; int mixed_dpi; @@ -90,6 +104,7 @@ typedef enum { THEME_BORDERSIDES, THEME_SIZE, THEME_DOUBLE, + THEME_CMD, } ltk_theme_datatype; typedef struct { @@ -106,6 +121,7 @@ typedef struct { ltk_border_sides *border; ltk_size *size; double *d; + ltk_array(cmd) **cmd; } ptr; /* Note: The default color is also given as a string because it has to be allocated first (it is only a @@ -120,6 +136,7 @@ typedef struct { ltk_border_sides border; ltk_size size; double d; + char *cmd; } defaultval; /* FIXME: min/max doesn't make too much sense for sizes since they can use different units, but that shouldn't matter for now because diff --git a/src/ltk/entry.c b/src/ltk/entry.c @@ -39,6 +39,7 @@ #include "util.h" #include "widget.h" #include "config.h" +#include "widget_internal.h" #define MAX_ENTRY_BORDER_WIDTH 10000 #define MAX_ENTRY_PADDING 50000 @@ -577,7 +578,7 @@ edit_external(ltk_widget *self, ltk_key_event *event) { } else { /* FIXME: somehow show that there was an error if this returns 1? */ /* FIXME: change interface to not require length of cmd */ - ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, strlen(config->line_editor), entry->text, entry->len); + ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, entry->text, entry->len); } return 0; } diff --git a/src/ltk/event_xlib.c b/src/ltk/event_xlib.c @@ -622,6 +622,7 @@ static struct keysym_mapping { {XK_space, LTK_KEY_SPACE}, {XK_Sys_Req, LTK_KEY_SYS_REQ}, {XK_Tab, LTK_KEY_TAB}, + {XK_ISO_Left_Tab, LTK_KEY_LEFT_TAB}, {XK_Up, LTK_KEY_UP}, {XK_Undo, LTK_KEY_UNDO}, }; diff --git a/src/ltk/eventdefs.h b/src/ltk/eventdefs.h @@ -140,6 +140,7 @@ typedef enum { LTK_KEY_SPACE, LTK_KEY_SYS_REQ, LTK_KEY_TAB, + LTK_KEY_LEFT_TAB, LTK_KEY_UP, LTK_KEY_UNDO } ltk_keysym; diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c @@ -45,8 +45,9 @@ #include "widget_internal.h" typedef struct { - char *tmpfile; ltk_widget *caller; + char *infile; + char *outfile; int pid; } ltk_cmdinfo; @@ -54,8 +55,8 @@ LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *) LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *) LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *) LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *) -LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo) -LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo) +LTK_ARRAY_INIT_DECL_STATIC(cmdinfo, ltk_cmdinfo) +LTK_ARRAY_INIT_IMPL_STATIC(cmdinfo, ltk_cmdinfo) static struct { ltk_renderdata *renderdata; @@ -66,9 +67,8 @@ static struct { /* PID of external command called e.g. by text widget to edit text. ON exit, cmd_caller->vtable->cmd_return is called with the text the external command wrote to a file. */ - /*IMPORTANT: this needs to be checked whenever a widget is destroyed! - FIXME: allow option to instead return output of command */ - ltk_array(cmd) *cmds; + /*FIXME: this needs to be checked whenever a widget is destroyed!*/ + ltk_array(cmdinfo) *cmds; size_t cur_kbd; } shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0}; @@ -97,59 +97,12 @@ typedef struct { knows if I'll need them again sometime... */ static ltk_widget_funcs widget_funcs[] = { { - .name = "box", - .cleanup = NULL, - }, - { - .name = "button", - .cleanup = NULL, - }, - { .name = "entry", .cleanup = &ltk_entry_cleanup, }, { - .name = "grid", - .cleanup = NULL, - }, - { - .name = "label", - .cleanup = NULL, - }, - { - /* FIXME: this is actually image_widget */ - .name = "image", - .cleanup = NULL, - }, - { - .name = "menu", - .cleanup = NULL, - }, - { - .name = "menuentry", - .cleanup = NULL, - }, - { - .name = "submenu", - .cleanup = NULL, - }, - { - .name = "submenuentry", - .cleanup = NULL, - /* - This "widget" is only needed to have separate styles for regular - menu entries and submenu entries. "submenu" is just an alias for - "menu" in most cases - it's just needed when creating a menu to - decide if it's a submenu or not. - FIXME: is that even necessary? Why can't it just decide if it's - a submenu based on whether it has a parent or not? - -> I guess right-click menus are also just submenus, so they - need to set it explicitly, but wasn't there another reason? - */ - }, - { - .name = "scrollbar", - .cleanup = NULL, + .name = "combobox", + .cleanup = &ltk_combobox_cleanup, }, { /* Handler for window theme. */ @@ -202,7 +155,7 @@ ltk_init(void) { ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4); shared_data.windows = ltk_array_create(window, 1); shared_data.rwindows = ltk_array_create(rwindow, 1); - shared_data.cmds = ltk_array_create(cmd, 1); + shared_data.cmds = ltk_array_create(cmdinfo, 1); return 0; /* FIXME: or maybe 1? */ } @@ -237,28 +190,37 @@ ltk_mainloop_step(int limit_framerate) { int pid = -1; int wstatus = 0; /* FIXME: kill all children on exit? */ + /* -> at least unlink any files? */ if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) { ltk_cmdinfo *info; /* FIXME: should commands be split into read/write and block write commands during external editing? */ for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) { info = &(ltk_array_get(shared_data.cmds, i)); if (info->pid == pid) { + /* FIXME: actually NULL this when widgets are destroyed */ if (!info->caller) { ltk_warn("Widget disappeared while text was being edited in external program\n"); /* FIXME: call overwritten cmd_return! */ } else if (info->caller->vtable->cmd_return) { size_t file_len = 0; char *errstr = NULL; - char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr); + char *filename = info->outfile ? info->outfile : info->infile; + char *contents = ltk_read_file(filename, &file_len, &errstr); if (!contents) { - ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr); + ltk_warn("Unable to read file '%s' written by external command: %s\n", filename, errstr); } else { info->caller->vtable->cmd_return(info->caller, contents, file_len); ltk_free0(contents); } } - ltk_free0(info->tmpfile); - ltk_array_delete(cmd, shared_data.cmds, i, 1); + /* FIXME: error checking */ + unlink(info->infile); + ltk_free(info->infile); + if (info->outfile) { + unlink(info->outfile); + ltk_free(info->outfile); + } + ltk_array_delete(cmdinfo, shared_data.cmds, i, 1); break; } } @@ -341,9 +303,11 @@ ltk_deinit(void) { if (shared_data.cmds) { for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) { /* FIXME: maybe kill child processes? */ - ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile); + ltk_free((ltk_array_get(shared_data.cmds, i)).infile); + if (ltk_array_get(shared_data.cmds, i).outfile) + ltk_free((ltk_array_get(shared_data.cmds, i)).outfile); } - ltk_array_destroy(cmd, shared_data.cmds); + ltk_array_destroy(cmdinfo, shared_data.cmds); } shared_data.cmds = NULL; if (shared_data.windows) { @@ -462,38 +426,140 @@ ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg da return id; } +LTK_ARRAY_INIT_DECL_STATIC(str, char *) +LTK_ARRAY_INIT_IMPL_STATIC(str, char *) + +static void +str_free_helper(char *elem) { + ltk_free(elem); +} + int -ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) { +ltk_call_cmd(ltk_widget *caller, ltk_array(cmd) *cmd, const char *text, size_t textlen) { + /* FIXME: maybe support stdin/stdout without temporary files by just piping directly */ /* FIXME: support environment variable $TMPDIR */ - ltk_cmdinfo info = {NULL, NULL, -1}; - info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX"); - int fd = mkstemp(info.tmpfile); - if (fd == -1) { - ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd); - ltk_free0(info.tmpfile); - return 1; + ltk_cmdinfo info = { + .caller = NULL, .infile = NULL, .outfile = NULL, .pid = -1 + }; + ltk_array(str) *cmdstr = ltk_array_create(str, 4); + txtbuf *tmpbuf = txtbuf_new(); + int needs_stdin = 1; + int needs_stdout = 1; + + int infd = -1, outfd = -1; + + info.infile = ltk_strdup("/tmp/ltk.XXXXXX"); + infd = mkstemp(info.infile); + if (infd == -1) { + ltk_warn_errno("Unable to create temporary input file while trying to run command."); + ltk_free(info.infile); + info.infile = NULL; /* so it isn't unlinked below */ + goto error; } - close(fd); /* FIXME: give file descriptor directly to modified version of ltk_write_file */ char *errstr = NULL; - if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) { - ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr); - unlink(info.tmpfile); - ltk_free0(info.tmpfile); - return 1; + if (ltk_write_file(info.infile, text, textlen, &errstr)) { + ltk_warn("Unable to write to temporary input file '%s' while trying to run command.", info.infile, errstr); + goto error; } - int pid = -1; - if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) { - /* FIXME: errno */ - ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd); - unlink(info.tmpfile); - ltk_free0(info.tmpfile); - return 1; + + for (size_t i = 0; i < ltk_array_len(cmd); i++) { + ltk_array(cmdpiece) *pa = ltk_array_get(cmd, i); + for (size_t j = 0; j < ltk_array_len(pa); j++) { + struct ltk_cmd_piece p = ltk_array_get(pa, j); + switch (p.type) { + case LTK_CMD_TEXT: + txtbuf_append(tmpbuf, p.text); + break; + case LTK_CMD_INOUT_FILE: + needs_stdout = 0; + /* fall through */ + case LTK_CMD_INPUT_FILE: + needs_stdin = 0; + txtbuf_append(tmpbuf, info.infile); + break; + case LTK_CMD_OUTPUT_FILE: + needs_stdout = 0; + if (!info.outfile) { + info.outfile = ltk_strdup("/tmp/ltk.XXXXXX"); + outfd = mkstemp(info.outfile); + if (outfd == -1) { + ltk_warn_errno("Unable to create temporary output file while trying to run command."); + ltk_free(info.outfile); + info.outfile = NULL; /* so it isn't unlinked below */ + goto error; + } + } + txtbuf_append(tmpbuf, info.outfile); + break; + default: + ltk_warn("Invalid command piece type. This should not happen."); + goto error; + } + } + ltk_array_append(str, cmdstr, txtbuf_get_textcopy(tmpbuf)); + txtbuf_clear(tmpbuf); + } + /* if no output file was specified, we still need to create it for stdout */ + if (needs_stdout) { + info.outfile = ltk_strdup("/tmp/ltk.XXXXXX"); + outfd = mkstemp(info.outfile); + if (outfd == -1) { + ltk_warn_errno("Unable to create temporary output file while trying to run command."); + ltk_free(info.outfile); + info.outfile = NULL; /* so it isn't unlinked below */ + goto error; + } + } + ltk_array_append(str, cmdstr, NULL); /* necessary for execve */ + txtbuf_destroy(tmpbuf); + tmpbuf = NULL; + + int fret = -1; + if ((fret = fork()) < 0) { + ltk_warn("Unable to fork\n"); + goto error; + } else if (fret == 0) { + if (needs_stdin) { + if (dup2(infd, fileno(stdin)) == -1) + ltk_fatal("Unable to set up stdin in child process."); + } + if (needs_stdout) { + int fd = outfd == -1 ? infd : outfd; + if (dup2(fd, fileno(stdout)) == -1) + ltk_fatal("Unable to set up stdout in child process."); + } + if (execvp(cmdstr->buf[0], cmdstr->buf) == -1) + ltk_fatal("Unable to exec external command."); } - info.pid = pid; + ltk_array_destroy_deep(str, cmdstr, &str_free_helper); + + info.pid = fret; info.caller = caller; - ltk_array_append(cmd, shared_data.cmds, info); + ltk_array_append(cmdinfo, shared_data.cmds, info); + + if (infd != -1) + close(infd); /* FIXME: error checking also on close */ + if (outfd != -1) + close(outfd); return 0; +error: + if (infd != -1) + close(infd); /* FIXME: error checking also on close and unlink */ + if (outfd != -1) + close(outfd); + if (tmpbuf) + txtbuf_destroy(tmpbuf); + if (info.infile) { + unlink(info.infile); + ltk_free(info.infile); + } + if (info.outfile) { + unlink(info.outfile); + ltk_free(info.outfile); + } + ltk_array_destroy_deep(str, cmdstr, &str_free_helper); + return 1; } static void diff --git a/src/ltk/ltk.h b/src/ltk/ltk.h @@ -43,12 +43,6 @@ int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_ar ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h); void ltk_window_destroy(ltk_widget *self, int shallow); -/* FIXME: allow piping text instead of writing to temporary file */ -/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those --> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored --> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */ -int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen); - /* convenience function to use the default text context */ ltk_text_line *ltk_text_line_create_default(const char *font, int font_size, char *text, int take_over_text, int width); ltk_text_line *ltk_text_line_create_const_text_default(const char *font, int font_size, const char *text, int width); diff --git a/src/ltk/memory.h b/src/ltk/memory.h @@ -17,8 +17,6 @@ #ifndef LTK_MEMORY_H #define LTK_MEMORY_H -/* FIXME: Move ltk_warn, etc. to util.* */ - #include <stdlib.h> #if MEMDEBUG == 1 diff --git a/src/ltk/menu.c b/src/ltk/menu.c @@ -104,7 +104,6 @@ static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event); static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event); static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event); static void shrink_entries(ltk_menu *menu); -static size_t get_entry(ltk_menu *menu, ltk_menuentry *entry); static void ltk_menu_destroy(ltk_widget *self, int shallow); static ltk_menu *ltk_menu_create_base(ltk_window *window, int is_submenu); @@ -321,6 +320,11 @@ ltk_menuentry_get_child(ltk_widget *self) { return NULL; } +const char * +ltk_menuentry_get_text(ltk_menuentry *entry) { + return ltk_text_line_get_text(entry->text_line); +} + static void ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) { /* FIXME: figure out how hidden should work */ @@ -731,20 +735,19 @@ popup_active_menu(ltk_menuentry *e) { ltk_rect menu_rect = e->widget.lrect; ltk_point entry_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e), 0, 0); ltk_point menu_global; - if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) { - ltk_menu *menu = LTK_CAST_MENU(e->widget.parent); + if (LTK_CAST_WIDGET(e)->parent && LTK_CAST_WIDGET(e)->parent->vtable->type == LTK_WIDGET_MENU) { + ltk_menu *menu = LTK_CAST_MENU(LTK_CAST_WIDGET(e)->parent); in_submenu = menu->is_submenu; was_opened_left = menu->was_opened_left; menu_rect = menu->widget.lrect; - menu_global = ltk_widget_pos_to_global(e->widget.parent, 0, 0); + menu_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e)->parent, 0, 0); } else { - menu_global = ltk_widget_pos_to_global(&e->widget, 0, 0); + menu_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e), 0, 0); } int win_w = e->widget.window->rect.w; int win_h = e->widget.window->rect.h; ltk_menu *submenu = e->submenu; ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(submenu)); - ltk_widget_resize(LTK_CAST_WIDGET(submenu)); int ideal_w = submenu->widget.ideal_w; int ideal_h = submenu->widget.ideal_h; int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h; @@ -842,14 +845,14 @@ popup_active_menu(ltk_menuentry *e) { submenu->widget.lrect.y = y_final; submenu->widget.lrect.w = w_final; submenu->widget.lrect.h = h_final; - submenu->widget.crect = submenu->widget.lrect; + submenu->widget.crect = LTK_CAST_WIDGET(submenu)->lrect; submenu->widget.dirty = 1; submenu->widget.hidden = 0; submenu->popup_submenus = 0; submenu->unpopup_submenus_on_hide = 1; - ltk_menu_resize(&submenu->widget); - ltk_window_register_popup(e->widget.window, (ltk_widget *)submenu); - ltk_window_invalidate_widget_rect(submenu->widget.window, &submenu->widget); + ltk_widget_resize(LTK_CAST_WIDGET(submenu)); + ltk_window_register_popup(LTK_CAST_WIDGET(e)->window, LTK_CAST_WIDGET(submenu)); + ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(submenu)->window, LTK_CAST_WIDGET(submenu)); } static void @@ -1126,15 +1129,16 @@ shrink_entries(ltk_menu *menu) { } } -int +ltk_menuentry * ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) { if (idx >= menu->num_entries) - return 1; /* invalid index */ + return NULL; /* invalid index */ menu->entries[idx]->widget.parent = NULL; /* I don't think this is needed because the entry isn't shown anywhere. Its size will be recalculated once it is added to a menu again. */ /* ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[idx]); */ + ltk_menuentry *ret = menu->entries[idx]; memmove( menu->entries + idx, menu->entries + idx + 1, @@ -1143,11 +1147,11 @@ ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) { menu->num_entries--; shrink_entries(menu); recalc_ideal_menu_size_with_notification(LTK_CAST_WIDGET(menu), NULL); - return 0; + return ret; } -static size_t -get_entry(ltk_menu *menu, ltk_menuentry *entry) { +size_t +ltk_menu_get_entry_index(ltk_menu *menu, ltk_menuentry *entry) { for (size_t i = 0; i < menu->num_entries; i++) { if (menu->entries[i] == entry) return i; @@ -1155,12 +1159,27 @@ get_entry(ltk_menu *menu, ltk_menuentry *entry) { return SIZE_MAX; } +size_t +ltk_menu_get_num_entries(ltk_menu *menu) { + return menu->num_entries; +} + +ltk_menuentry * +ltk_menu_get_entry(ltk_menu *menu, size_t idx) { + if (idx >= menu->num_entries) + return NULL; + return menu->entries[idx]; +} + int ltk_menu_remove_entry(ltk_menu *menu, ltk_menuentry *entry) { - size_t idx = get_entry(menu, entry); + size_t idx = ltk_menu_get_entry_index(menu, entry); if (idx >= menu->num_entries) return 1; - return ltk_menu_remove_entry_index(menu, idx); + ltk_menuentry *ret = ltk_menu_remove_entry_index(menu, idx); + if (!ret) /* shouldn't be possible */ + return 1; + return 0; } static int @@ -1212,7 +1231,7 @@ ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect) { is already at bottom of respective menu - the top-level menu will give the first submenu in the current active hierarchy as child widget again, and nearest_child on that submenu will (probably) give the bottom widget again, so nothing changes except that all submenus except - for the first and second one disappeare */ + for the first and second one disappear */ static ltk_widget * ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget) { ltk_menu *menu = LTK_CAST_MENU(self); diff --git a/src/ltk/menu.h b/src/ltk/menu.h @@ -73,10 +73,14 @@ ltk_menu *ltk_submenu_create(ltk_window *window); ltk_menuentry *ltk_menuentry_create(ltk_window *window, const char *text); int ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu); int ltk_menuentry_detach_submenu(ltk_menuentry *e); +const char *ltk_menuentry_get_text(ltk_menuentry *entry); int ltk_menu_insert_entry(ltk_menu *menu, ltk_menuentry *entry, size_t idx); int ltk_menu_add_entry(ltk_menu *menu, ltk_menuentry *entry); -int ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx); +ltk_menuentry *ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx); int ltk_menu_remove_entry(ltk_menu *menu, ltk_menuentry *entry); void ltk_menu_remove_all_entries(ltk_menu *menu); +size_t ltk_menu_get_num_entries(ltk_menu *menu); +ltk_menuentry *ltk_menu_get_entry(ltk_menu *menu, size_t idx); +size_t ltk_menu_get_entry_index(ltk_menu *menu, ltk_menuentry *entry); #endif /* LTK_MENU_H */ diff --git a/src/ltk/text.h b/src/ltk/text.h @@ -40,6 +40,8 @@ void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h); void ltk_text_line_destroy(ltk_text_line *tl); /* FIXME: length of text */ void ltk_text_line_set_text(ltk_text_line *line, char *text, int take_over_text); +void ltk_text_line_set_const_text(ltk_text_line *line, const char *text); +const char *ltk_text_line_get_text(ltk_text_line *line); /* Draw the entire line to a surface. */ /* FIXME: Some widgets rely on this to not fail when negative coordinates are given or diff --git a/src/ltk/text_pango.c b/src/ltk/text_pango.c @@ -102,6 +102,16 @@ ltk_text_line_set_text(ltk_text_line *tl, char *text, int take_over_text) { } void +ltk_text_line_set_const_text(ltk_text_line *tl, const char *text) { + ltk_text_line_set_text(tl, ltk_strdup(text), 1); +} + +const char * +ltk_text_line_get_text(ltk_text_line *tl) { + return tl->text; +} + +void ltk_text_line_set_font_size(ltk_text_line *tl, int font_size) { if (font_size == tl->font_size) return; diff --git a/src/ltk/txtbuf.c b/src/ltk/txtbuf.c @@ -135,6 +135,16 @@ txtbuf_get_textcopy(txtbuf *buf) { return buf->text ? ltk_strndup(buf->text, buf->len) : ltk_strdup(""); } +const char * +txtbuf_get_text(txtbuf *buf) { + return buf->text; +} + +size_t +txtbuf_len(txtbuf *buf) { + return buf->len; +} + /* FIXME: proper "normalize" function to add nul-termination if needed */ int txtbuf_cmp(txtbuf *buf1, txtbuf *buf2) { diff --git a/src/ltk/txtbuf.h b/src/ltk/txtbuf.h @@ -113,6 +113,19 @@ txtbuf *txtbuf_dup(txtbuf *src); char *txtbuf_get_textcopy(txtbuf *buf); /* + * Get text stored in 'buf'. + * The returned text belongs to the txtbuf and must not be changed. + * The returned text may be invalidated as soon as any other + * functions are called on the txtbuf. + */ +const char *txtbuf_get_text(txtbuf *buf); + +/* + * Get the length of the text stored in 'buf'. + */ +size_t txtbuf_len(txtbuf *buf); + +/* * Clear the text, but do not reduce the internal capacity * (for efficiency if it will be filled up again anyways). */ diff --git a/src/ltk/util.c b/src/ltk/util.c @@ -85,194 +85,6 @@ errorclose: return 1; } -/* FIXME: maybe have a few standard array types defined somewhere else */ -LTK_ARRAY_INIT_DECL_STATIC(cmd, char *) -LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *) - -static void -free_helper(char *ptr) { - ltk_free(ptr); -} - -/* FIXME: this is really ugly */ -/* FIXME: parse command only once in beginning instead of each time it is run? */ -/* FIXME: this handles double-quote, but the config parser already uses that, so - it's kind of weird because it's parsed twice (also backslashes are parsed twice). */ -int -ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) { - int bs = 0; - int in_sqstr = 0; - int in_dqstr = 0; - int in_ws = 1; - char c; - size_t cur_start = 0; - int offset = 0; - txtbuf *cur_arg = txtbuf_new(); - ltk_array(cmd) *cmd = ltk_array_create(cmd, 4); - char *cmdcopy = ltk_strndup(cmdtext, len); - for (size_t i = 0; i < len; i++) { - c = cmdcopy[i]; - if (c == '\\') { - if (bs) { - offset++; - bs = 0; - } else { - bs = 1; - } - } else if (isspace(c)) { - if (!in_sqstr && !in_dqstr) { - if (bs) { - if (in_ws) { - in_ws = 0; - cur_start = i; - offset = 0; - } else { - offset++; - } - bs = 0; - } else if (!in_ws) { - /* FIXME: shouldn't this be < instead of <=? */ - if (cur_start <= i - offset) - txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset); - /* FIXME: cmd is named horribly */ - ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg)); - txtbuf_clear(cur_arg); - in_ws = 1; - offset = 0; - } - /* FIXME: parsing weird here - bs just ignored */ - } else if (bs) { - bs = 0; - } - } else if (c == '%') { - if (bs) { - if (in_ws) { - cur_start = i; - offset = 0; - } else { - offset++; - } - bs = 0; - } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') { - if (!in_ws && cur_start < i - offset) - txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset); - txtbuf_append(cur_arg, filename); - i++; - cur_start = i + 1; - offset = 0; - } else if (in_ws) { - cur_start = i; - offset = 0; - } - in_ws = 0; - } else if (c == '"') { - if (in_sqstr) { - bs = 0; - } else if (bs) { - if (in_ws) { - cur_start = i; - offset = 0; - } else { - offset++; - } - bs = 0; - } else if (in_dqstr) { - offset++; - in_dqstr = 0; - continue; - } else { - in_dqstr = 1; - if (in_ws) { - cur_start = i + 1; - offset = 0; - } else { - offset++; - continue; - } - } - in_ws = 0; - } else if (c == '\'') { - if (in_dqstr) { - bs = 0; - } else if (bs) { - if (in_ws) { - cur_start = i; - offset = 0; - } else { - offset++; - } - bs = 0; - } else if (in_sqstr) { - offset++; - in_sqstr = 0; - continue; - } else { - in_sqstr = 1; - if (in_ws) { - cur_start = i + 1; - offset = 0; - } else { - offset++; - continue; - } - } - in_ws = 0; - } else if (bs) { - if (!in_sqstr && !in_dqstr) { - if (in_ws) { - cur_start = i; - offset = 0; - } else { - offset++; - } - } - bs = 0; - in_ws = 0; - } else { - if (in_ws) { - cur_start = i; - offset = 0; - } - in_ws = 0; - } - cmdcopy[i - offset] = cmdcopy[i]; - } - if (in_sqstr || in_dqstr) { - ltk_warn("Unterminated string in command\n"); - goto error; - } - if (!in_ws) { - if (cur_start <= len - offset) - txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset); - ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg)); - } - if (cmd->len == 0) { - ltk_warn("Empty command\n"); - goto error; - } - ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */ - int fret = -1; - if ((fret = fork()) < 0) { - ltk_warn("Unable to fork\n"); - goto error; - } else if (fret == 0) { - if (execvp(cmd->buf[0], cmd->buf) == -1) { - /* FIXME: what to do on error here? */ - exit(1); - } - } else { - ltk_free(cmdcopy); - txtbuf_destroy(cur_arg); - ltk_array_destroy_deep(cmd, cmd, &free_helper); - return fret; - } -error: - ltk_free(cmdcopy); - txtbuf_destroy(cur_arg); - ltk_array_destroy_deep(cmd, cmd, &free_helper); - return -1; -} - /* If `needed` is larger than `*alloc_size`, resize `*str` to `max(needed, *alloc_size * 2)`. Aborts program on error. */ void diff --git a/src/ltk/widget.h b/src/ltk/widget.h @@ -47,6 +47,7 @@ typedef enum { LTK_WIDGET_SCROLLBAR, LTK_WIDGET_CHECKBUTTON, LTK_WIDGET_RADIOBUTTON, + LTK_WIDGET_COMBOBOX, LTK_NUM_WIDGETS, } ltk_widget_type; @@ -188,6 +189,7 @@ typedef struct { #define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w)) #define LTK_CAST_CHECKBUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_CHECKBUTTON), (ltk_checkbutton *)(w)) #define LTK_CAST_RADIOBUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_RADIOBUTTON), (ltk_radiobutton *)(w)) +#define LTK_CAST_COMBOBOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_COMBOBOX), (ltk_combobox *)(w)) /* FIXME: a bit weird because window never gets some of these signals */ #define LTK_WIDGET_SIGNAL_KEY_PRESS 1 diff --git a/src/ltk/widget_internal.h b/src/ltk/widget_internal.h @@ -35,6 +35,14 @@ void ltk_submenu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len); void ltk_submenuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len); void ltk_scrollbar_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len); +void ltk_combobox_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len); +void ltk_combobox_cleanup(void); +void ltk_combobox_get_keybinding_parseinfo( + ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret, + ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret, + ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret +); + void ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len); void ltk_entry_cleanup(void); void ltk_entry_get_keybinding_parseinfo( @@ -53,4 +61,9 @@ void ltk_window_get_keybinding_parseinfo( ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret ); +/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those +-> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored +-> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */ +int ltk_call_cmd(ltk_widget *caller, ltk_array(cmd) *cmd, const char *text, size_t textlen); + #endif /* LTK_WIDGET_INTERNAL_H */ diff --git a/src/ltk/window.c b/src/ltk/window.c @@ -193,19 +193,16 @@ ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) { if (!keypresses) return 1; ltk_keypress_binding *b = NULL; + /* FIXME: move into separate function and share between window, entry, etc. */ for (size_t i = 0; i < ltk_array_len(keypresses); i++) { b = &ltk_array_get(keypresses, i).b; - if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) { + if ((!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) continue; - } else if (b->text) { - if (event->mapped && !strcmp(b->text, event->mapped)) - handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event); - } else if (b->rawtext) { - if (event->text && !strcmp(b->text, event->text)) - handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event); - } else if (b->sym != LTK_KEY_NONE) { - if (event->sym == b->sym) - handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event); + if ((b->mods == event->modmask && b->sym != LTK_KEY_NONE && b->sym == event->sym) || + (b->mods == (event->modmask & ~LTK_MOD_SHIFT) && + ((b->text && event->mapped && !strcmp(b->text, event->mapped)) || + (b->rawtext && event->text && !strcmp(b->rawtext, event->text))))) { + handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event); } } return 1;