From 642b63f5a64b1dfda2ecd041fed3a008645d72cc Mon Sep 17 00:00:00 2001 From: Vito Caputo Date: Sun, 3 Oct 2021 18:13:36 -0700 Subject: glimmer: first stab at adding settings support This bumps the rototiller submodule for the new described settings stuff, among other improvements. There are some ugly kludges surrounding widget lifecycle since I destroy setting widgets from their signal callbacks and that seems to anger gtk/glib. I'm unclear on what the right way is here, but leaking for now is relatively harmless. It seems like gtk+ should be holding a reference on the respective widget across signal deliveries so the callbacks can potentialy destroy them with the underlying resource freeing becoming queued. Perhaps there's something like signal propagation up the heirarchy happening and since I'm destroying the settings frame higher up the tree and not the specific widget being signaled things fall over. It's a TODO item to sort out, at least the resources leaked aren't substantial and it's only on interactive configuration. This has also been done in fashion preserving the --module=foo,bar=baz arguments consistent with classic rototiller. You can start glimmer with the same syntax --module= flag, and it will select the specified module and apply the supplied settings in a way reflected in the GUI. There should probably be a new '--go' flag added to enable starting the rendering via commandline. As-is glimmer will always leave you at a settings dialog waiting for a "Go!" click, even if you specified module+settings comprehensively via the CLI. --- src/gtk_fb.c | 4 +- src/main.c | 377 +++++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 319 insertions(+), 62 deletions(-) (limited to 'src') diff --git a/src/gtk_fb.c b/src/gtk_fb.c index 2306db5..4520df7 100644 --- a/src/gtk_fb.c +++ b/src/gtk_fb.c @@ -74,11 +74,11 @@ static int gtk_fb_init(const til_settings_t *settings, void **res_context) assert(settings); assert(res_context); - fullscreen = til_settings_get_value(settings, "fullscreen"); + fullscreen = til_settings_get_value(settings, "fullscreen", NULL); if (!fullscreen) return -EINVAL; - size = til_settings_get_value(settings, "size"); + size = til_settings_get_value(settings, "size", NULL); if (!size && !strcasecmp(fullscreen, "off")) return -EINVAL; diff --git a/src/main.c b/src/main.c index 8caf117..74cb96b 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,7 @@ #include #include +#include /* glimmer is a GTK+-3.0 frontend for rototiller */ @@ -27,16 +28,21 @@ extern til_fb_ops_t gtk_fb_ops; #define DEFAULT_WIDTH 320 #define DEFAULT_HEIGHT 480 -#define BOX_SPACING 4 +#define DEFAULT_MODULE "rtv" +#define BOX_SPACING 1 +#define FRAME_MARGIN 8 +#define LABEL_MARGIN 4 #define NUM_FB_PAGES 3 static struct glimmer_t { - GtkWidget *modules_list; + GtkComboBox *modules_combobox; + GtkWidget *module_box, *module_frame; - til_fb_t *fb; + til_args_t args; til_settings_t *video_settings; - til_settings_t *module_settings; + + til_fb_t *fb; const til_module_t *module; void *module_context; pthread_t thread; @@ -45,12 +51,44 @@ static struct glimmer_t { } glimmer; -static unsigned get_ticks(const struct timeval *start, const struct timeval *now, unsigned offset) +static void glimmer_module_setup(const til_module_t *module, til_settings_t *settings); + + +static unsigned glimmer_get_ticks(const struct timeval *start, const struct timeval *now, unsigned offset) { return (unsigned)((now->tv_sec - start->tv_sec) * 1000 + (now->tv_usec - start->tv_usec) / 1000) + offset; } +static void glimmer_active_module(const til_module_t **res_module, til_settings_t **res_settings) +{ + GtkTreeIter iter; + + if (gtk_combo_box_get_active_iter(glimmer.modules_combobox, &iter)) { + GtkTreeModel *model = gtk_combo_box_get_model(glimmer.modules_combobox); + char *name; + + gtk_tree_model_get(model, &iter, + 0, &name, + 1, res_module, + 2, res_settings, + -1); + + g_free(name); + } +} + + +static void glimmer_active_module_setup(void) +{ + const til_module_t *module; + til_settings_t *settings; + + glimmer_active_module(&module, &settings); + glimmer_module_setup(module, settings); +} + + /* TODO: this should probably move into libtil */ static void * glimmer_thread(void *foo) { @@ -62,7 +100,7 @@ static void * glimmer_thread(void *foo) page = til_fb_page_get(glimmer.fb); gettimeofday(&now, NULL); - ticks = get_ticks(&glimmer.start_tv, &now, glimmer.ticks_offset); + ticks = glimmer_get_ticks(&glimmer.start_tv, &now, glimmer.ticks_offset); til_module_render(glimmer.module, glimmer.module_context, ticks, &page->fragment); til_fb_page_put(glimmer.fb, page); } @@ -71,7 +109,8 @@ static void * glimmer_thread(void *foo) static void glimmer_go(GtkButton *button, gpointer user_data) { - int r; + til_settings_t *settings; + int r; if (glimmer.fb) { pthread_cancel(glimmer.thread); @@ -79,21 +118,10 @@ static void glimmer_go(GtkButton *button, gpointer user_data) til_quiesce(); glimmer.fb = til_fb_free(glimmer.fb); - glimmer.video_settings = til_settings_free(glimmer.video_settings); - glimmer.module_settings = til_settings_free(glimmer.module_settings); + glimmer.module_context = til_module_destroy_context(glimmer.module, glimmer.module_context); } - /* TODO: translate the GTK+ settings panel values into - * glimmer.{fb,module}_settings - */ - - /* For now, construct a simple 640x480 non-fullscreen fb, and - * simply don't do any module setup (those *should* have static builtin - * defaults that at least work on some level. - */ - glimmer.video_settings = til_settings_new("fullscreen=off,size=640x480"); - glimmer.module_settings = til_settings_new("TODO"); - + /* TODO: prolly stop recreating fb on every go, or maybe only if the settings changed */ r = til_fb_new(>k_fb_ops, glimmer.video_settings, NUM_FB_PAGES, &glimmer.fb); if (r < 0) { puts("fb no go!"); @@ -101,10 +129,10 @@ static void glimmer_go(GtkButton *button, gpointer user_data) } gettimeofday(&glimmer.start_tv, NULL); - glimmer.module = til_lookup_module(gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(glimmer.modules_list))); + glimmer_active_module(&glimmer.module, &settings); r = til_module_create_context( glimmer.module, - get_ticks( + glimmer_get_ticks( &glimmer.start_tv, &glimmer.start_tv, glimmer.ticks_offset), @@ -118,13 +146,186 @@ static void glimmer_go(GtkButton *button, gpointer user_data) } -static void activate(GtkApplication *app, gpointer user_data) +static void glimmer_combobox_setting_changed_cb(GtkComboBoxText *combobox, gpointer user_data) { - GtkWidget *window, *vbox, *settings, *button; - const til_module_t **modules; - size_t n_modules; + til_setting_t *setting = user_data; + + /* XXX FIXME FIXME XXX */ + /* XXX FIXME FIXME XXX */ + /* I don't know gtk+ well enough to know what's the non-leaky way to do this: + * glimmer_active_module_setup() will destroy the module frame which encompasses the + * widget this signal emitted from. It appears that there isn't a reference held across + * the signal callbacks so they can safely perform a queued destroy of the originating widget + * within the callback to then become realized at the end of all the signal deliveries and + * callback processing when the final reference gets removed. + * + * for now I'm working around this by simply adding a ref, leaking the memory, until I find + * the Right Way. + */ + g_object_ref(combobox); + /* XXX FIXME FIXME XXX */ + /* XXX FIXME FIXME XXX */ - til_get_modules(&modules, &n_modules); + setting->value = gtk_combo_box_text_get_active_text(combobox); + glimmer_active_module_setup(); +} + +static void glimmer_entry_setting_changed_cb(GtkEntry *entry, gpointer user_data) +{ + til_setting_t *setting = user_data; + + /* XXX FIXME: see above comment for combobox, but oddly I'm only seeing + * errors printed for the combobox case. I'm just assuming the problem exists here as well. + */ + g_object_ref(entry); + + /* FIXME TODO there needs to be some validation of the free-form input against setting->desc->regex, + * though that probably shouldn't happen here. + */ + setting->value = strdup(gtk_entry_get_text(entry)); + glimmer_active_module_setup(); +} + + +/* (re)construct the gui settings pane to reflect *module and *settings */ +static void glimmer_module_setup(const til_module_t *module, til_settings_t *settings) +{ + GtkWidget *vbox, *label; + + if (glimmer.module_frame) + gtk_widget_destroy(glimmer.module_frame); + + glimmer.module_frame = g_object_new( GTK_TYPE_FRAME, + "parent", GTK_CONTAINER(glimmer.module_box), + "label", module->name, + "label-xalign", .01f, + "margin", FRAME_MARGIN, + "visible", TRUE, + NULL); + gtk_box_set_child_packing( GTK_BOX(glimmer.module_box), + GTK_WIDGET(glimmer.module_frame), + TRUE, + FALSE, + BOX_SPACING, + GTK_PACK_START); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, BOX_SPACING); + gtk_container_add(GTK_CONTAINER(glimmer.module_frame), vbox); + + label = g_object_new( GTK_TYPE_LABEL, + "parent", GTK_CONTAINER(vbox), + "label", module->description, + "halign", GTK_ALIGN_START, + "margin", LABEL_MARGIN, + "visible", TRUE, + NULL); + + label = g_object_new( GTK_TYPE_LABEL, + "parent", GTK_CONTAINER(vbox), + "label", module->author, + "halign", GTK_ALIGN_START, + "margin", LABEL_MARGIN, + "visible", TRUE, + NULL); + + if (module->setup) { + GtkWidget *frame, *svbox; + til_setting_t *setting; + const til_setting_desc_t *desc; + + frame = g_object_new( GTK_TYPE_FRAME, + "parent", GTK_CONTAINER(vbox), + "label", "Settings", + "label-xalign", .01f, + "margin", FRAME_MARGIN, + "visible", TRUE, + NULL); + gtk_box_set_child_packing( GTK_BOX(vbox), + GTK_WIDGET(frame), + TRUE, + TRUE, + BOX_SPACING, + GTK_PACK_START); + + svbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, BOX_SPACING); + gtk_container_add(GTK_CONTAINER(frame), svbox); + + til_settings_reset_descs(settings); + while (module->setup(settings, &setting, &desc) > 0) { + GtkWidget *shbox; + + if (!setting) { + til_settings_add_value(settings, desc->key, desc->preferred, NULL); + continue; + } + + shbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, BOX_SPACING); + gtk_container_add(GTK_CONTAINER(svbox), shbox); + gtk_widget_set_halign(GTK_WIDGET(shbox), GTK_ALIGN_START); + + label = g_object_new( GTK_TYPE_LABEL, + "parent", GTK_CONTAINER(shbox), + "label", desc->name, + "halign", GTK_ALIGN_START, + "margin", LABEL_MARGIN, + "visible", TRUE, + NULL); + + if (desc->values) { + GtkWidget *combobox; + + /* combo box */ + combobox = gtk_combo_box_text_new(); + gtk_container_add(GTK_CONTAINER(shbox), combobox); + gtk_widget_set_halign(GTK_WIDGET(combobox), GTK_ALIGN_END); + for (int i = 0; desc->values[i]; i++) { + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combobox), NULL, desc->values[i]); + if (!strcmp(setting->value, desc->values[i])) + gtk_combo_box_set_active(GTK_COMBO_BOX(combobox), i); + } + g_signal_connect(combobox, "changed", G_CALLBACK(glimmer_combobox_setting_changed_cb), setting); + + } else { + GtkWidget *entry; + + /* plain unstructured text input box */ + entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(entry), setting->value); + gtk_container_add(GTK_CONTAINER(shbox), entry); + gtk_widget_set_halign(GTK_WIDGET(entry), GTK_ALIGN_END); + + /* XXX FIXME */ + /* XXX FIXME */ + /* XXX FIXME */ + /* "activate" only occurs on hitting Enter in the GtkEntry. So we'll miss + * edits that are visible but not Entered before hitting Go! We likely need + * to catch more signals... + */ + /* XXX FIXME */ + /* XXX FIXME */ + /* XXX FIXME */ + + g_signal_connect(entry, "activate", G_CALLBACK(glimmer_entry_setting_changed_cb), setting); + } + + if (!setting->desc) + setting->desc = desc; + } + } + + gtk_widget_show_all(glimmer.module_frame); +} + + +static void glimmer_module_changed_cb(GtkComboBox *box, G_GNUC_UNUSED gpointer user_data) +{ + glimmer_active_module_setup(); +} + + +static void glimmer_activate(GtkApplication *app, gpointer user_data) +{ + GtkWidget *window, *vbox, *button; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "glimmer"); @@ -133,54 +334,110 @@ static void activate(GtkApplication *app, gpointer user_data) vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, BOX_SPACING); gtk_container_add(GTK_CONTAINER(window), vbox); - glimmer.modules_list = gtk_combo_box_text_new(); - for (size_t i = 0; i < n_modules; i++) { - gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(glimmer.modules_list), NULL, modules[i]->name); - - /* like rototiller, default to rtv */ - if (!strcmp(modules[i]->name, "rtv")) - gtk_combo_box_set_active(GTK_COMBO_BOX(glimmer.modules_list), i); + { /* construct modules list combobox, associating a name, module, and settings per entry */ + const til_module_t **modules; + size_t n_modules; + const char *module; + GtkComboBox *combobox; + GtkListStore *store; + GtkCellRenderer *text; + + til_get_modules(&modules, &n_modules); + module = til_settings_get_key(glimmer.module_settings, 0, NULL); + + combobox = g_object_new(GTK_TYPE_COMBO_BOX, "visible", TRUE, NULL); + store = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER); + for (size_t i = 0; i < n_modules; i++) { + GtkTreeIter iter; + + gtk_list_store_append(store, &iter); + gtk_list_store_set( store, &iter, + 0, modules[i]->name, + 1, modules[i], + 2, (module && !strcmp(module, modules[i]->name)) ? glimmer.module_settings : til_settings_new(NULL), + -1); + } + + gtk_combo_box_set_model(combobox, GTK_TREE_MODEL(store)); + gtk_combo_box_set_id_column(combobox, 0); + gtk_combo_box_set_active_id(combobox, module ? : DEFAULT_MODULE); + + g_signal_connect(combobox, "changed", G_CALLBACK(glimmer_module_changed_cb), NULL); + + text = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combobox), text, TRUE); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combobox), text, "text", 0); + + glimmer.modules_combobox = combobox; } - gtk_container_add(GTK_CONTAINER(vbox), glimmer.modules_list); - - gtk_box_set_child_packing( - GTK_BOX(vbox), - glimmer.modules_list, - FALSE, - FALSE, - BOX_SPACING * 4, /* FIXME: having the combo box too near the window edge puts the pointer into the scroll-up arrow on click :/ */ - GTK_PACK_START); - - /* TODO: below the combobox, present framebuffer and the selected module's settings */ - settings = gtk_label_new("TODO: fb/module settings here"); - gtk_container_add(GTK_CONTAINER(vbox), settings); - gtk_box_set_child_packing( - GTK_BOX(vbox), - settings, - TRUE, - TRUE, - BOX_SPACING, - GTK_PACK_START); + + gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(glimmer.modules_combobox)); + gtk_box_set_child_packing( GTK_BOX(vbox), + GTK_WIDGET(glimmer.modules_combobox), + FALSE, + FALSE, + BOX_SPACING * 4, + GTK_PACK_START); + + glimmer.module_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, BOX_SPACING); + gtk_container_add(GTK_CONTAINER(vbox), glimmer.module_box); + gtk_box_set_child_packing( GTK_BOX(vbox), + glimmer.module_box, + TRUE, + TRUE, + BOX_SPACING, + GTK_PACK_START); + + glimmer_active_module_setup(); /* button to rototill as configured */ - button = gtk_button_new_with_label("Go!"); - gtk_container_add(GTK_CONTAINER(vbox), button); + button = g_object_new( GTK_TYPE_BUTTON, + "parent", GTK_CONTAINER(vbox), + "label", "Go!", + "visible", TRUE, + NULL); g_signal_connect(button, "clicked", G_CALLBACK(glimmer_go), NULL); gtk_widget_show_all(window); } -int main(int argc, char **argv) +int main(int argc, const char *argv[]) { + int r, status, pruned_argc; + const char **pruned_argv; GtkApplication *app; - int status; til_init(); + + r = til_args_pruned_parse(argc, argv, &glimmer.args, &pruned_argc, &pruned_argv); + if (r < 0) { + fprintf(stderr, "Unable to parse args: %s\n", strerror(-r)); + return EXIT_FAILURE; + } + + glimmer.module_settings = til_settings_new(glimmer.args.module); + /* TODO: glimmer doesn't currently handle video settings, gtk_fb doesn't even + * implement a .setup() method. It would be an interesting exercise to bring + * in support for rototiller's sdl and drm fb backends, but it immediately + * becomes awkward with obvious conflicts like drm wanting to own the display + * implicitly being shared when glimmer's already using gtk if not on distinct + * devices. + * + * But it'd be nice to at least support window sizing/fullscreen + * startup via args w/gtk_fb, so at some point I should add a + * gtk_fb.setup() method for filling in the blanks of what the args omit. + * For now these statically defined comprehensive settings simply skirt + * the issue. + */ + //glimmer.video_settings = til_settings_new(glimmer.args.video); + glimmer.video_settings = til_settings_new("fullscreen=off,size=640x480"); + app = gtk_application_new("com.pengaru.glimmer", G_APPLICATION_FLAGS_NONE); - g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); - status = g_application_run(G_APPLICATION(app), argc, argv); + g_signal_connect(app, "activate", G_CALLBACK(glimmer_activate), NULL); + status = g_application_run(G_APPLICATION(app), pruned_argc, (char **)pruned_argv); g_object_unref(app); + til_shutdown(); return status; -- cgit v1.2.3