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:
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 = <k_combobox_key_press,
+ .key_release = NULL,
+ .mouse_press = NULL,
+ .mouse_release = NULL,
+ .release = <k_combobox_release,
+ .motion_notify = NULL,
+ .mouse_leave = NULL,
+ .mouse_enter = NULL,
+ .change_state = NULL,
+ .get_child_at_pos = NULL,
+ .cmd_return = <k_combobox_cmd_return,
+ .resize = NULL,
+ .hide = NULL,
+ .draw = <k_combobox_draw,
+ .destroy = <k_combobox_destroy,
+ .child_size_change = NULL,
+ .remove_child = <k_combobox_remove_child,
+ .first_child = <k_combobox_get_child,
+ .last_child = <k_combobox_get_child,
+ .nearest_child = <k_combobox_nearest_child,
+ .recalc_ideal_size = <k_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", <k_combobox_get_keybinding_parseinfo},
{"entry", <k_entry_get_keybinding_parseinfo},
{"window", <k_window_get_keybinding_parseinfo},
};
@@ -86,6 +90,7 @@ static struct theme_handlerinfo {
{"theme:submenuentry", <k_submenuentry_get_theme_parseinfo, "theme:window", 0},
{"theme:checkbutton", <k_checkbutton_get_theme_parseinfo, "theme:window", 0},
{"theme:radiobutton", <k_radiobutton_get_theme_parseinfo, "theme:window", 0},
+ {"theme:combobox", <k_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 = <k_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 = <k_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 = <k_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;