aboutsummaryrefslogtreecommitdiffstats
path: root/src/jalv_gtk.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/jalv_gtk.c')
-rw-r--r--src/jalv_gtk.c1248
1 files changed, 1248 insertions, 0 deletions
diff --git a/src/jalv_gtk.c b/src/jalv_gtk.c
new file mode 100644
index 0000000..a27a8df
--- /dev/null
+++ b/src/jalv_gtk.c
@@ -0,0 +1,1248 @@
+/*
+ Copyright 2007-2017 David Robillard <http://drobilla.net>
+
+ 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.
+
+ THIS 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 <math.h>
+
+#include <gtk/gtk.h>
+
+#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
+#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h"
+
+#include "jalv_internal.h"
+
+#if GTK_MAJOR_VERSION == 3
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#elif __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+#endif
+
+static GtkCheckMenuItem* active_preset_item = NULL;
+static bool updating = false;
+
+/** Widget for a control. */
+typedef struct {
+ GtkSpinButton* spin;
+ GtkWidget* control;
+} Controller;
+
+static float
+get_float(const LilvNode* node, float fallback)
+{
+ if (lilv_node_is_float(node) || lilv_node_is_int(node)) {
+ return lilv_node_as_float(node);
+ }
+
+ return fallback;
+}
+
+static GtkWidget*
+new_box(gboolean horizontal, gint spacing)
+{
+ #if GTK_MAJOR_VERSION == 3
+ return gtk_box_new(
+ horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL,
+ spacing);
+ #else
+ return (horizontal
+ ? gtk_hbox_new(FALSE, spacing)
+ : gtk_vbox_new(FALSE, spacing));
+ #endif
+}
+
+static GtkWidget*
+new_hscale(gdouble min, gdouble max, gdouble step)
+{
+ #if GTK_MAJOR_VERSION == 3
+ return gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, step);
+ #else
+ return gtk_hscale_new_with_range(min, max, step);
+ #endif
+}
+
+static void
+size_request(GtkWidget* widget, GtkRequisition* req)
+{
+ #if GTK_MAJOR_VERSION == 3
+ gtk_widget_get_preferred_size(widget, NULL, req);
+ #else
+ gtk_widget_size_request(widget, req);
+ #endif
+}
+
+static void
+on_window_destroy(ZIX_UNUSED GtkWidget* widget, ZIX_UNUSED gpointer data)
+{
+ gtk_main_quit();
+}
+
+int
+jalv_init(int* argc, char*** argv, JalvOptions* opts)
+{
+ GOptionEntry entries[] = {
+ { "uuid", 'u', 0, G_OPTION_ARG_STRING, &opts->uuid,
+ "UUID for Jack session restoration", "UUID" },
+ { "load", 'l', 0, G_OPTION_ARG_STRING, &opts->load,
+ "Load state from save directory", "DIR" },
+ { "preset", 'p', 0, G_OPTION_ARG_STRING, &opts->preset,
+ "Load state from preset", "URI" },
+ { "dump", 'd', 0, G_OPTION_ARG_NONE, &opts->dump,
+ "Dump plugin <=> UI communication", NULL },
+ { "trace", 't', 0, G_OPTION_ARG_NONE, &opts->trace,
+ "Print trace messages from plugin", NULL },
+ { "show-hidden", 's', 0, G_OPTION_ARG_NONE, &opts->show_hidden,
+ "Show controls for ports with notOnGUI property on generic UI", NULL },
+ { "no-menu", 'n', 0, G_OPTION_ARG_NONE, &opts->no_menu,
+ "Do not show Jalv menu on window", NULL },
+ { "generic-ui", 'g', 0, G_OPTION_ARG_NONE, &opts->generic_ui,
+ "Use Jalv generic UI and not the plugin UI", NULL},
+ { "buffer-size", 'b', 0, G_OPTION_ARG_INT, &opts->buffer_size,
+ "Buffer size for plugin <=> UI communication", "SIZE"},
+ { "update-frequency", 'r', 0, G_OPTION_ARG_DOUBLE, &opts->update_rate,
+ "UI update frequency", NULL},
+ { "control", 'c', 0, G_OPTION_ARG_STRING_ARRAY, &opts->controls,
+ "Set control value (e.g. \"vol=1.4\")", NULL},
+ { "print-controls", 'p', 0, G_OPTION_ARG_NONE, &opts->print_controls,
+ "Print control output changes to stdout", NULL},
+ { "jack-name", 'n', 0, G_OPTION_ARG_STRING, &opts->name,
+ "JACK client name", NULL},
+ { "exact-jack-name", 'x', 0, G_OPTION_ARG_NONE, &opts->name_exact,
+ "Exact JACK client name (exit if taken)", NULL },
+ { 0, 0, 0, G_OPTION_ARG_NONE, 0, 0, 0 } };
+ GError* error = NULL;
+ const int err = gtk_init_with_args(
+ argc, argv,
+ "PLUGIN_URI - Run an LV2 plugin as a Jack application",
+ entries, NULL, &error);
+
+ if (!err) {
+ fprintf(stderr, "%s\n", error->message);
+ }
+
+ return !err;
+}
+
+const char*
+jalv_native_ui_type(void)
+{
+#if GTK_MAJOR_VERSION == 2
+ return "http://lv2plug.in/ns/extensions/ui#GtkUI";
+#elif GTK_MAJOR_VERSION == 3
+ return "http://lv2plug.in/ns/extensions/ui#Gtk3UI";
+#else
+ return NULL;
+#endif
+}
+
+static void
+on_save_activate(ZIX_UNUSED GtkWidget* widget, void* ptr)
+{
+ Jalv* jalv = (Jalv*)ptr;
+ GtkWidget* dialog = gtk_file_chooser_dialog_new(
+ "Save State",
+ (GtkWindow*)jalv->window,
+ GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ "_Save", GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ char* base = g_build_filename(path, "/", NULL);
+ jalv_save(jalv, base);
+ g_free(path);
+ g_free(base);
+ }
+
+ gtk_widget_destroy(dialog);
+}
+
+static void
+on_quit_activate(ZIX_UNUSED GtkWidget* widget, gpointer data)
+{
+ GtkWidget* window = (GtkWidget*)data;
+ gtk_widget_destroy(window);
+}
+
+typedef struct {
+ Jalv* jalv;
+ LilvNode* preset;
+} PresetRecord;
+
+static char*
+symbolify(const char* in)
+{
+ const size_t len = strlen(in);
+ char* out = (char*)calloc(len + 1, 1);
+ for (size_t i = 0; i < len; ++i) {
+ if (g_ascii_isalnum(in[i])) {
+ out[i] = in[i];
+ } else {
+ out[i] = '_';
+ }
+ }
+ return out;
+}
+
+static void
+set_window_title(Jalv* jalv)
+{
+ LilvNode* name = lilv_plugin_get_name(jalv->plugin);
+ const char* plugin = lilv_node_as_string(name);
+ if (jalv->preset) {
+ const char* preset_label = lilv_state_get_label(jalv->preset);
+ char* title = g_strdup_printf("%s - %s", plugin, preset_label);
+ gtk_window_set_title(GTK_WINDOW(jalv->window), title);
+ free(title);
+ } else {
+ gtk_window_set_title(GTK_WINDOW(jalv->window), plugin);
+ }
+ lilv_node_free(name);
+}
+
+static void
+on_preset_activate(GtkWidget* widget, gpointer data)
+{
+ if (GTK_CHECK_MENU_ITEM(widget) != active_preset_item) {
+ PresetRecord* record = (PresetRecord*)data;
+ jalv_apply_preset(record->jalv, record->preset);
+ if (active_preset_item) {
+ gtk_check_menu_item_set_active(active_preset_item, FALSE);
+ }
+
+ active_preset_item = GTK_CHECK_MENU_ITEM(widget);
+ gtk_check_menu_item_set_active(active_preset_item, TRUE);
+ set_window_title(record->jalv);
+ }
+}
+
+static void
+on_preset_destroy(gpointer data, ZIX_UNUSED GClosure* closure)
+{
+ PresetRecord* record = (PresetRecord*)data;
+ lilv_node_free(record->preset);
+ free(record);
+}
+
+typedef struct {
+ GtkMenuItem* item;
+ char* label;
+ GtkMenu* menu;
+ GSequence* banks;
+} PresetMenu;
+
+static PresetMenu*
+pset_menu_new(const char* label)
+{
+ PresetMenu* menu = (PresetMenu*)malloc(sizeof(PresetMenu));
+ menu->label = g_strdup(label);
+ menu->item = GTK_MENU_ITEM(gtk_menu_item_new_with_label(menu->label));
+ menu->menu = GTK_MENU(gtk_menu_new());
+ menu->banks = NULL;
+ return menu;
+}
+
+static void
+pset_menu_free(PresetMenu* menu)
+{
+ if (menu->banks) {
+ for (GSequenceIter* i = g_sequence_get_begin_iter(menu->banks);
+ !g_sequence_iter_is_end(i);
+ i = g_sequence_iter_next(i)) {
+ PresetMenu* bank_menu = (PresetMenu*)g_sequence_get(i);
+ pset_menu_free(bank_menu);
+ }
+ g_sequence_free(menu->banks);
+ }
+
+ free(menu->label);
+ free(menu);
+}
+
+static gint
+menu_cmp(gconstpointer a, gconstpointer b, ZIX_UNUSED gpointer data)
+{
+ return strcmp(((const PresetMenu*)a)->label, ((const PresetMenu*)b)->label);
+}
+
+static PresetMenu*
+get_bank_menu(Jalv* jalv, PresetMenu* menu, const LilvNode* bank)
+{
+ LilvNode* label = lilv_world_get(
+ jalv->world, bank, jalv->nodes.rdfs_label, NULL);
+
+ const char* uri = lilv_node_as_string(bank);
+ const char* str = label ? lilv_node_as_string(label) : uri;
+ PresetMenu key = { NULL, (char*)str, NULL, NULL };
+ GSequenceIter* i = g_sequence_lookup(menu->banks, &key, menu_cmp, NULL);
+ if (!i) {
+ PresetMenu* bank_menu = pset_menu_new(str);
+ gtk_menu_item_set_submenu(bank_menu->item, GTK_WIDGET(bank_menu->menu));
+ g_sequence_insert_sorted(menu->banks, bank_menu, menu_cmp, NULL);
+ return bank_menu;
+ }
+ return (PresetMenu*)g_sequence_get(i);
+}
+
+static int
+add_preset_to_menu(Jalv* jalv,
+ const LilvNode* node,
+ const LilvNode* title,
+ void* data)
+{
+ PresetMenu* menu = (PresetMenu*)data;
+ const char* label = lilv_node_as_string(title);
+ GtkWidget* item = gtk_check_menu_item_new_with_label(label);
+ gtk_check_menu_item_set_draw_as_radio(GTK_CHECK_MENU_ITEM(item), TRUE);
+ if (jalv->preset &&
+ lilv_node_equals(lilv_state_get_uri(jalv->preset), node)) {
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
+ active_preset_item = GTK_CHECK_MENU_ITEM(item);
+ }
+
+ LilvNode* bank = lilv_world_get(
+ jalv->world, node, jalv->nodes.pset_bank, NULL);
+
+ if (bank) {
+ PresetMenu* bank_menu = get_bank_menu(jalv, menu, bank);
+ gtk_menu_shell_append(GTK_MENU_SHELL(bank_menu->menu), item);
+ } else {
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu->menu), item);
+ }
+
+ PresetRecord* record = (PresetRecord*)malloc(sizeof(PresetRecord));
+ record->jalv = jalv;
+ record->preset = lilv_node_duplicate(node);
+
+ g_signal_connect_data(G_OBJECT(item), "activate",
+ G_CALLBACK(on_preset_activate),
+ record, on_preset_destroy,
+ (GConnectFlags)0);
+
+ return 0;
+}
+
+static void
+finish_menu(PresetMenu* menu)
+{
+ for (GSequenceIter* i = g_sequence_get_begin_iter(menu->banks);
+ !g_sequence_iter_is_end(i);
+ i = g_sequence_iter_next(i)) {
+ PresetMenu* bank_menu = (PresetMenu*)g_sequence_get(i);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu->menu),
+ GTK_WIDGET(bank_menu->item));
+ }
+ g_sequence_free(menu->banks);
+}
+
+static void
+rebuild_preset_menu(Jalv* jalv, GtkContainer* pset_menu)
+{
+ // Clear current menu
+ active_preset_item = NULL;
+ for (GList* items = g_list_nth(gtk_container_get_children(pset_menu), 3);
+ items;
+ items = items->next) {
+ gtk_container_remove(pset_menu, GTK_WIDGET(items->data));
+ }
+
+ // Load presets and build new menu
+ PresetMenu menu = {
+ NULL, NULL, GTK_MENU(pset_menu),
+ g_sequence_new((GDestroyNotify)pset_menu_free)
+ };
+ jalv_load_presets(jalv, add_preset_to_menu, &menu);
+ finish_menu(&menu);
+ gtk_widget_show_all(GTK_WIDGET(pset_menu));
+}
+
+static void
+on_save_preset_activate(GtkWidget* widget, void* ptr)
+{
+ Jalv* jalv = (Jalv*)ptr;
+
+ GtkWidget* dialog = gtk_file_chooser_dialog_new(
+ "Save Preset",
+ (GtkWindow*)jalv->window,
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ "_Cancel", GTK_RESPONSE_REJECT,
+ "_Save", GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ char* dot_lv2 = g_build_filename(g_get_home_dir(), ".lv2", NULL);
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), dot_lv2);
+ free(dot_lv2);
+
+ GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ GtkBox* box = GTK_BOX(new_box(true, 8));
+ GtkWidget* uri_label = gtk_label_new("URI (Optional):");
+ GtkWidget* uri_entry = gtk_entry_new();
+ GtkWidget* add_prefix = gtk_check_button_new_with_mnemonic(
+ "_Prefix plugin name");
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(add_prefix), TRUE);
+ gtk_box_pack_start(box, uri_label, FALSE, TRUE, 2);
+ gtk_box_pack_start(box, uri_entry, TRUE, TRUE, 2);
+ gtk_box_pack_start(GTK_BOX(content), GTK_WIDGET(box), FALSE, FALSE, 6);
+ gtk_box_pack_start(GTK_BOX(content), add_prefix, FALSE, FALSE, 6);
+
+ gtk_widget_show_all(GTK_WIDGET(dialog));
+ gtk_entry_set_activates_default(GTK_ENTRY(uri_entry), TRUE);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ LilvNode* plug_name = lilv_plugin_get_name(jalv->plugin);
+ const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ const char* uri = gtk_entry_get_text(GTK_ENTRY(uri_entry));
+ const char* prefix = "";
+ const char* sep = "";
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(add_prefix))) {
+ prefix = lilv_node_as_string(plug_name);
+ sep = "_";
+ }
+
+ char* dirname = g_path_get_dirname(path);
+ char* basename = g_path_get_basename(path);
+ char* sym = symbolify(basename);
+ char* sprefix = symbolify(prefix);
+ char* bundle = g_strjoin(NULL, sprefix, sep, sym, ".preset.lv2/", NULL);
+ char* file = g_strjoin(NULL, sym, ".ttl", NULL);
+ char* dir = g_build_filename(dirname, bundle, NULL);
+
+ jalv_save_preset(jalv, dir, (strlen(uri) ? uri : NULL), basename, file);
+
+ // Reload bundle into the world
+ LilvNode* ldir = lilv_new_file_uri(jalv->world, NULL, dir);
+ lilv_world_unload_bundle(jalv->world, ldir);
+ lilv_world_load_bundle(jalv->world, ldir);
+ lilv_node_free(ldir);
+
+ // Rebuild preset menu and update window title
+ rebuild_preset_menu(jalv, GTK_CONTAINER(gtk_widget_get_parent(widget)));
+ set_window_title(jalv);
+
+ g_free(dir);
+ g_free(file);
+ g_free(bundle);
+ free(sprefix);
+ free(sym);
+ g_free(basename);
+ g_free(dirname);
+ lilv_node_free(plug_name);
+ }
+
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+static void
+on_delete_preset_activate(GtkWidget* widget, void* ptr)
+{
+ Jalv* jalv = (Jalv*)ptr;
+ if (!jalv->preset) {
+ return;
+ }
+
+ GtkWidget* dialog = gtk_dialog_new_with_buttons(
+ "Delete Preset?",
+ (GtkWindow*)jalv->window,
+ (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
+ "_Cancel", GTK_RESPONSE_REJECT,
+ "_OK", GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ char* msg = g_strdup_printf("Delete preset \"%s\" from the file system?",
+ lilv_state_get_label(jalv->preset));
+
+ GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ GtkWidget* text = gtk_label_new(msg);
+ gtk_box_pack_start(GTK_BOX(content), text, TRUE, TRUE, 4);
+
+ gtk_widget_show_all(dialog);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ jalv_delete_current_preset(jalv);
+ rebuild_preset_menu(jalv, GTK_CONTAINER(gtk_widget_get_parent(widget)));
+ }
+
+ lilv_state_free(jalv->preset);
+ jalv->preset = NULL;
+ set_window_title(jalv);
+
+ g_free(msg);
+ gtk_widget_destroy(text);
+ gtk_widget_destroy(dialog);
+}
+
+static void
+set_control(const ControlID* control,
+ uint32_t size,
+ LV2_URID type,
+ const void* body)
+{
+ if (!updating) {
+ jalv_set_control(control, size, type, body);
+ }
+}
+
+static bool
+differ_enough(float a, float b)
+{
+ return fabsf(a - b) >= FLT_EPSILON;
+}
+
+static void
+set_float_control(const ControlID* control, float value)
+{
+ if (control->value_type == control->jalv->forge.Int) {
+ const int32_t ival = lrint(value);
+ set_control(control, sizeof(ival), control->jalv->forge.Int, &ival);
+ } else if (control->value_type == control->jalv->forge.Long) {
+ const int64_t lval = lrint(value);
+ set_control(control, sizeof(lval), control->jalv->forge.Long, &lval);
+ } else if (control->value_type == control->jalv->forge.Float) {
+ set_control(control, sizeof(value), control->jalv->forge.Float, &value);
+ } else if (control->value_type == control->jalv->forge.Double) {
+ const double dval = value;
+ set_control(control, sizeof(dval), control->jalv->forge.Double, &dval);
+ } else if (control->value_type == control->jalv->forge.Bool) {
+ const int32_t ival = value;
+ set_control(control, sizeof(ival), control->jalv->forge.Bool, &ival);
+ }
+
+ Controller* controller = (Controller*)control->widget;
+ if (controller && controller->spin &&
+ differ_enough(gtk_spin_button_get_value(controller->spin), value)) {
+ gtk_spin_button_set_value(controller->spin, value);
+ }
+}
+
+static double
+get_atom_double(Jalv* jalv,
+ ZIX_UNUSED uint32_t size,
+ LV2_URID type,
+ const void* body)
+{
+ if (type == jalv->forge.Int || type == jalv->forge.Bool) {
+ return *(const int32_t*)body;
+ } else if (type == jalv->forge.Long) {
+ return *(const int64_t*)body;
+ } else if (type == jalv->forge.Float) {
+ return *(const float*)body;
+ } else if (type == jalv->forge.Double) {
+ return *(const double*)body;
+ }
+ return NAN;
+}
+
+static void
+control_changed(Jalv* jalv,
+ Controller* controller,
+ uint32_t size,
+ LV2_URID type,
+ const void* body)
+{
+ GtkWidget* widget = controller->control;
+ const double fvalue = get_atom_double(jalv, size, type, body);
+
+ if (!isnan(fvalue)) {
+ if (GTK_IS_COMBO_BOX(widget)) {
+ GtkTreeModel* model = gtk_combo_box_get_model(GTK_COMBO_BOX(widget));
+ GValue value = { 0, { { 0 } } };
+ GtkTreeIter i;
+ bool valid = gtk_tree_model_get_iter_first(model, &i);
+ while (valid) {
+ gtk_tree_model_get_value(model, &i, 0, &value);
+ const double v = g_value_get_float(&value);
+ g_value_unset(&value);
+ if (fabs(v - fvalue) < FLT_EPSILON) {
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(widget), &i);
+ return;
+ }
+ valid = gtk_tree_model_iter_next(model, &i);
+ }
+ } else if (GTK_IS_TOGGLE_BUTTON(widget)) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),
+ fvalue > 0.0f);
+ } else if (GTK_IS_RANGE(widget)) {
+ gtk_range_set_value(GTK_RANGE(widget), fvalue);
+ } else {
+ fprintf(stderr, "Unknown widget type for value\n");
+ }
+
+ if (controller->spin) {
+ // Update spinner for numeric control
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(controller->spin),
+ fvalue);
+ }
+ } else if (GTK_IS_ENTRY(widget) && type == jalv->urids.atom_String) {
+ gtk_entry_set_text(GTK_ENTRY(widget), (const char*)body);
+ } else if (GTK_IS_FILE_CHOOSER(widget) && type == jalv->urids.atom_Path) {
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(widget), (const char*)body);
+ } else {
+ fprintf(stderr, "Unknown widget type for value\n");
+ }
+}
+
+static int
+patch_set_get(Jalv* jalv,
+ const LV2_Atom_Object* obj,
+ const LV2_Atom_URID** property,
+ const LV2_Atom** value)
+{
+ lv2_atom_object_get(obj,
+ jalv->urids.patch_property, (const LV2_Atom*)property,
+ jalv->urids.patch_value, value,
+ 0);
+ if (!*property) {
+ fprintf(stderr, "patch:Set message with no property\n");
+ return 1;
+ } else if ((*property)->atom.type != jalv->forge.URID) {
+ fprintf(stderr, "patch:Set property is not a URID\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+patch_put_get(Jalv* jalv,
+ const LV2_Atom_Object* obj,
+ const LV2_Atom_Object** body)
+{
+ lv2_atom_object_get(obj,
+ jalv->urids.patch_body, (const LV2_Atom*)body,
+ 0);
+ if (!*body) {
+ fprintf(stderr, "patch:Put message with no body\n");
+ return 1;
+ } else if (!lv2_atom_forge_is_object_type(&jalv->forge, (*body)->atom.type)) {
+ fprintf(stderr, "patch:Put body is not an object\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static void
+property_changed(Jalv* jalv, LV2_URID key, const LV2_Atom* value)
+{
+ ControlID* control = get_property_control(&jalv->controls, key);
+ if (control) {
+ control_changed(jalv,
+ (Controller*)control->widget,
+ value->size,
+ value->type,
+ value + 1);
+ }
+}
+
+void
+jalv_ui_port_event(Jalv* jalv,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t protocol,
+ const void* buffer)
+{
+ if (jalv->ui_instance) {
+ suil_instance_port_event(jalv->ui_instance, port_index,
+ buffer_size, protocol, buffer);
+ return;
+ } else if (protocol == 0 && (Controller*)jalv->ports[port_index].widget) {
+ control_changed(jalv,
+ (Controller*)jalv->ports[port_index].widget,
+ buffer_size,
+ jalv->forge.Float,
+ buffer);
+ return;
+ } else if (protocol == 0) {
+ return; // No widget (probably notOnGUI)
+ } else if (protocol != jalv->urids.atom_eventTransfer) {
+ fprintf(stderr, "Unknown port event protocol\n");
+ return;
+ }
+
+ const LV2_Atom* atom = (const LV2_Atom*)buffer;
+ if (lv2_atom_forge_is_object_type(&jalv->forge, atom->type)) {
+ updating = true;
+ const LV2_Atom_Object* obj = (const LV2_Atom_Object*)buffer;
+ if (obj->body.otype == jalv->urids.patch_Set) {
+ const LV2_Atom_URID* property = NULL;
+ const LV2_Atom* value = NULL;
+ if (!patch_set_get(jalv, obj, &property, &value)) {
+ property_changed(jalv, property->body, value);
+ }
+ } else if (obj->body.otype == jalv->urids.patch_Put) {
+ const LV2_Atom_Object* body = NULL;
+ if (!patch_put_get(jalv, obj, &body)) {
+ LV2_ATOM_OBJECT_FOREACH(body, prop) {
+ property_changed(jalv, prop->key, &prop->value);
+ }
+ }
+ } else {
+ printf("Unknown object type?\n");
+ }
+ updating = false;
+ }
+}
+
+static gboolean
+scale_changed(GtkRange* range, gpointer data)
+{
+ set_float_control((const ControlID*)data, gtk_range_get_value(range));
+ return FALSE;
+}
+
+static gboolean
+spin_changed(GtkSpinButton* spin, gpointer data)
+{
+ const ControlID* control = (const ControlID*)data;
+ Controller* controller = (Controller*)control->widget;
+ GtkRange* range = GTK_RANGE(controller->control);
+ const double value = gtk_spin_button_get_value(spin);
+ if (differ_enough(gtk_range_get_value(range), value)) {
+ gtk_range_set_value(range, value);
+ }
+ return FALSE;
+}
+
+static gboolean
+log_scale_changed(GtkRange* range, gpointer data)
+{
+ set_float_control((const ControlID*)data, expf(gtk_range_get_value(range)));
+ return FALSE;
+}
+
+static gboolean
+log_spin_changed(GtkSpinButton* spin, gpointer data)
+{
+ const ControlID* control = (const ControlID*)data;
+ Controller* controller = (Controller*)control->widget;
+ GtkRange* range = GTK_RANGE(controller->control);
+ const double value = gtk_spin_button_get_value(spin);
+ if (differ_enough(gtk_range_get_value(range), logf(value))) {
+ gtk_range_set_value(range, logf(value));
+ }
+ return FALSE;
+}
+
+static void
+combo_changed(GtkComboBox* box, gpointer data)
+{
+ const ControlID* control = (const ControlID*)data;
+
+ GtkTreeIter iter;
+ if (gtk_combo_box_get_active_iter(box, &iter)) {
+ GtkTreeModel* model = gtk_combo_box_get_model(box);
+ GValue value = { 0, { { 0 } } };
+
+ gtk_tree_model_get_value(model, &iter, 0, &value);
+ const double v = g_value_get_float(&value);
+ g_value_unset(&value);
+
+ set_float_control(control, v);
+ }
+}
+
+static gboolean
+toggle_changed(GtkToggleButton* button, gpointer data)
+{
+ set_float_control((const ControlID*)data,
+ gtk_toggle_button_get_active(button) ? 1.0f : 0.0f);
+ return FALSE;
+}
+
+static void
+string_changed(GtkEntry* widget, gpointer data)
+{
+ ControlID* control = (ControlID*)data;
+ const char* string = gtk_entry_get_text(widget);
+
+ set_control(control, strlen(string) + 1, control->jalv->forge.String, string);
+}
+
+static void
+file_changed(GtkFileChooserButton* widget,
+ gpointer data)
+{
+ ControlID* control = (ControlID*)data;
+ Jalv* jalv = control->jalv;
+ const char* filename = gtk_file_chooser_get_filename(
+ GTK_FILE_CHOOSER(widget));
+
+ set_control(control, strlen(filename), jalv->forge.Path, filename);
+}
+
+static Controller*
+new_controller(GtkSpinButton* spin, GtkWidget* control)
+{
+ Controller* controller = (Controller*)malloc(sizeof(Controller));
+ controller->spin = spin;
+ controller->control = control;
+ return controller;
+}
+
+static Controller*
+make_combo(ControlID* record, float value)
+{
+ GtkListStore* list_store = gtk_list_store_new(
+ 2, G_TYPE_FLOAT, G_TYPE_STRING);
+ int active = -1;
+ for (size_t i = 0; i < record->n_points; ++i) {
+ const ScalePoint* point = &record->points[i];
+ GtkTreeIter iter;
+ gtk_list_store_append(list_store, &iter);
+ gtk_list_store_set(list_store, &iter,
+ 0, point->value,
+ 1, point->label,
+ -1);
+ if (fabs(value - point->value) < FLT_EPSILON) {
+ active = i;
+ }
+ }
+
+ GtkWidget* combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list_store));
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), active);
+ g_object_unref(list_store);
+
+ gtk_widget_set_sensitive(combo, record->is_writable);
+
+ GtkCellRenderer* cell = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), cell, "text", 1, NULL);
+
+ if (record->is_writable) {
+ g_signal_connect(G_OBJECT(combo), "changed",
+ G_CALLBACK(combo_changed), record);
+ }
+
+ return new_controller(NULL, combo);
+}
+
+static Controller*
+make_log_slider(ControlID* record, float value)
+{
+ const float min = get_float(record->min, 0.0f);
+ const float max = get_float(record->max, 1.0f);
+ const float lmin = logf(min);
+ const float lmax = logf(max);
+ const float ldft = logf(value);
+ GtkWidget* scale = new_hscale(lmin, lmax, 0.001);
+ GtkWidget* spin = gtk_spin_button_new_with_range(min, max, 0.000001);
+
+ gtk_widget_set_sensitive(scale, record->is_writable);
+ gtk_widget_set_sensitive(spin, record->is_writable);
+
+ gtk_scale_set_draw_value(GTK_SCALE(scale), FALSE);
+ gtk_range_set_value(GTK_RANGE(scale), ldft);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), value);
+
+ if (record->is_writable) {
+ g_signal_connect(G_OBJECT(scale), "value-changed",
+ G_CALLBACK(log_scale_changed), record);
+ g_signal_connect(G_OBJECT(spin), "value-changed",
+ G_CALLBACK(log_spin_changed), record);
+ }
+
+ return new_controller(GTK_SPIN_BUTTON(spin), scale);
+}
+
+static Controller*
+make_slider(ControlID* record, float value)
+{
+ const float min = get_float(record->min, 0.0f);
+ const float max = get_float(record->max, 1.0f);
+ const double step = record->is_integer ? 1.0 : ((max - min) / 100.0);
+ GtkWidget* scale = new_hscale(min, max, step);
+ GtkWidget* spin = gtk_spin_button_new_with_range(min, max, step);
+
+ gtk_widget_set_sensitive(scale, record->is_writable);
+ gtk_widget_set_sensitive(spin, record->is_writable);
+
+ if (record->is_integer) {
+ gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 0);
+ } else {
+ gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 7);
+ }
+
+ gtk_scale_set_draw_value(GTK_SCALE(scale), FALSE);
+ gtk_range_set_value(GTK_RANGE(scale), value);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), value);
+ if (record->points) {
+ for (size_t i = 0; i < record->n_points; ++i) {
+ const ScalePoint* point = &record->points[i];
+
+ gchar* str = g_markup_printf_escaped(
+ "<span font_size=\"small\">%s</span>", point->label);
+ gtk_scale_add_mark(
+ GTK_SCALE(scale), point->value, GTK_POS_TOP, str);
+ }
+ }
+
+ if (record->is_writable) {
+ g_signal_connect(G_OBJECT(scale), "value-changed",
+ G_CALLBACK(scale_changed), record);
+ g_signal_connect(G_OBJECT(spin), "value-changed",
+ G_CALLBACK(spin_changed), record);
+ }
+
+ return new_controller(GTK_SPIN_BUTTON(spin), scale);
+}
+
+static Controller*
+make_toggle(ControlID* record, float value)
+{
+ GtkWidget* check = gtk_check_button_new();
+
+ gtk_widget_set_sensitive(check, record->is_writable);
+
+ if (value) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), TRUE);
+ }
+
+ if (record->is_writable) {
+ g_signal_connect(G_OBJECT(check), "toggled",
+ G_CALLBACK(toggle_changed), record);
+ }
+
+ return new_controller(NULL, check);
+}
+
+static Controller*
+make_entry(ControlID* control)
+{
+ GtkWidget* entry = gtk_entry_new();
+
+ gtk_widget_set_sensitive(entry, control->is_writable);
+ if (control->is_writable) {
+ g_signal_connect(G_OBJECT(entry), "activate",
+ G_CALLBACK(string_changed), control);
+ }
+
+ return new_controller(NULL, entry);
+}
+
+static Controller*
+make_file_chooser(ControlID* record)
+{
+ GtkWidget* button = gtk_file_chooser_button_new(
+ "Open File", GTK_FILE_CHOOSER_ACTION_OPEN);
+
+ gtk_widget_set_sensitive(button, record->is_writable);
+
+ if (record->is_writable) {
+ g_signal_connect(G_OBJECT(button), "file-set",
+ G_CALLBACK(file_changed), record);
+ }
+
+ return new_controller(NULL, button);
+}
+
+static Controller*
+make_controller(ControlID* control, float value)
+{
+ Controller* controller = NULL;
+
+ if (control->is_toggle) {
+ controller = make_toggle(control, value);
+ } else if (control->is_enumeration) {
+ controller = make_combo(control, value);
+ } else if (control->is_logarithmic) {
+ controller = make_log_slider(control, value);
+ } else {
+ controller = make_slider(control, value);
+ }
+
+ return controller;
+}
+
+static GtkWidget*
+new_label(const char* text, bool title, float xalign, float yalign)
+{
+ GtkWidget* label = gtk_label_new(NULL);
+ const char* fmt = title ? "<span font_weight=\"bold\">%s</span>" : "%s:";
+ gchar* str = g_markup_printf_escaped(fmt, text);
+ gtk_label_set_markup(GTK_LABEL(label), str);
+ g_free(str);
+ gtk_misc_set_alignment(GTK_MISC(label), xalign, yalign);
+ return label;
+}
+
+static void
+add_control_row(GtkWidget* table,
+ int row,
+ const char* name,
+ Controller* controller)
+{
+ GtkWidget* label = new_label(name, false, 1.0, 0.5);
+ gtk_table_attach(GTK_TABLE(table),
+ label,
+ 0, 1, row, row + 1,
+ GTK_FILL, (GtkAttachOptions)(GTK_FILL|GTK_EXPAND), 8, 1);
+ int control_left_attach = 1;
+ if (controller->spin) {
+ control_left_attach = 2;
+ gtk_table_attach(GTK_TABLE(table), GTK_WIDGET(controller->spin),
+ 1, 2, row, row + 1,
+ GTK_FILL, GTK_FILL, 2, 1);
+ }
+ gtk_table_attach(GTK_TABLE(table), controller->control,
+ control_left_attach, 3, row, row + 1,
+ (GtkAttachOptions)(GTK_FILL|GTK_EXPAND), GTK_FILL, 2, 1);
+}
+
+static int
+control_group_cmp(const void* p1, const void* p2, ZIX_UNUSED void* data)
+{
+ const ControlID* control1 = *(const ControlID*const*)p1;
+ const ControlID* control2 = *(const ControlID*const*)p2;
+
+ const int cmp = (control1->group && control2->group)
+ ? strcmp(lilv_node_as_string(control1->group),
+ lilv_node_as_string(control2->group))
+ : ((intptr_t)control1->group - (intptr_t)control2->group);
+
+ return cmp;
+}
+
+static GtkWidget*
+build_control_widget(Jalv* jalv, GtkWidget* window)
+{
+ GtkWidget* port_table = gtk_table_new(jalv->num_ports, 3, false);
+
+ /* Make an array of controls sorted by group */
+ GArray* controls = g_array_new(FALSE, TRUE, sizeof(ControlID*));
+ for (unsigned i = 0; i < jalv->controls.n_controls; ++i) {
+ g_array_append_vals(controls, &jalv->controls.controls[i], 1);
+ }
+ g_array_sort_with_data(controls, control_group_cmp, jalv);
+
+ /* Add controls in group order */
+ LilvNode* last_group = NULL;
+ int n_rows = 0;
+ for (size_t i = 0; i < controls->len; ++i) {
+ ControlID* record = g_array_index(controls, ControlID*, i);
+ Controller* controller = NULL;
+ LilvNode* group = record->group;
+
+ /* Check group and add new heading if necessary */
+ if (group && !lilv_node_equals(group, last_group)) {
+ LilvNode* group_name = lilv_world_get(
+ jalv->world, group, jalv->nodes.lv2_name, NULL);
+ GtkWidget* group_label = new_label(
+ lilv_node_as_string(group_name), true, 0.0f, 1.0f);
+ gtk_table_attach(GTK_TABLE(port_table), group_label,
+ 0, 2, n_rows, n_rows + 1,
+ GTK_FILL, GTK_FILL, 0, 6);
+ ++n_rows;
+ }
+ last_group = group;
+
+ /* Make control widget */
+ if (record->value_type == jalv->forge.String) {
+ controller = make_entry(record);
+ } else if (record->value_type == jalv->forge.Path) {
+ controller = make_file_chooser(record);
+ } else {
+ const float val = get_float(record->def, 0.0f);
+ controller = make_controller(record, val);
+ }
+
+ record->widget = controller;
+ if (record->type == PORT) {
+ jalv->ports[record->index].widget = controller;
+ }
+ if (controller) {
+ /* Add row to table for this controller */
+ add_control_row(
+ port_table, n_rows++,
+ (record->label
+ ? lilv_node_as_string(record->label)
+ : lilv_node_as_uri(record->node)),
+ controller);
+
+ /* Set tooltip text from comment, if available */
+ LilvNode* comment = lilv_world_get(
+ jalv->world, record->node, jalv->nodes.rdfs_comment, NULL);
+ if (comment) {
+ gtk_widget_set_tooltip_text(controller->control,
+ lilv_node_as_string(comment));
+ }
+ lilv_node_free(comment);
+ }
+ }
+
+ if (n_rows > 0) {
+ gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
+ GtkWidget* alignment = gtk_alignment_new(0.5, 0.0, 1.0, 0.0);
+ gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), 0, 0, 8, 8);
+ gtk_container_add(GTK_CONTAINER(alignment), port_table);
+ return alignment;
+ } else {
+ gtk_widget_destroy(port_table);
+ GtkWidget* button = gtk_button_new_with_label("Close");
+ g_signal_connect_swapped(button, "clicked",
+ G_CALLBACK(gtk_widget_destroy), window);
+ gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
+ return button;
+ }
+}
+
+static void
+build_menu(Jalv* jalv, GtkWidget* window, GtkWidget* vbox)
+{
+ GtkWidget* menu_bar = gtk_menu_bar_new();
+ GtkWidget* file = gtk_menu_item_new_with_mnemonic("_File");
+ GtkWidget* file_menu = gtk_menu_new();
+
+ GtkAccelGroup* ag = gtk_accel_group_new();
+ gtk_window_add_accel_group(GTK_WINDOW(window), ag);
+
+ GtkWidget* save = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, ag);
+ GtkWidget* quit = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, ag);
+
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(file), file_menu);
+ gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save);
+ gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu_bar), file);
+
+ GtkWidget* pset_item = gtk_menu_item_new_with_mnemonic("_Presets");
+ GtkWidget* pset_menu = gtk_menu_new();
+ GtkWidget* save_preset = gtk_menu_item_new_with_mnemonic(
+ "_Save Preset...");
+ GtkWidget* delete_preset = gtk_menu_item_new_with_mnemonic(
+ "_Delete Current Preset...");
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(pset_item), pset_menu);
+ gtk_menu_shell_append(GTK_MENU_SHELL(pset_menu), save_preset);
+ gtk_menu_shell_append(GTK_MENU_SHELL(pset_menu), delete_preset);
+ gtk_menu_shell_append(GTK_MENU_SHELL(pset_menu),
+ gtk_separator_menu_item_new());
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu_bar), pset_item);
+
+ PresetMenu menu = {
+ NULL, NULL, GTK_MENU(pset_menu),
+ g_sequence_new((GDestroyNotify)pset_menu_free)
+ };
+ jalv_load_presets(jalv, add_preset_to_menu, &menu);
+ finish_menu(&menu);
+
+ g_signal_connect(G_OBJECT(quit), "activate",
+ G_CALLBACK(on_quit_activate), window);
+
+ g_signal_connect(G_OBJECT(save), "activate",
+ G_CALLBACK(on_save_activate), jalv);
+
+ g_signal_connect(G_OBJECT(save_preset), "activate",
+ G_CALLBACK(on_save_preset_activate), jalv);
+
+ g_signal_connect(G_OBJECT(delete_preset), "activate",
+ G_CALLBACK(on_delete_preset_activate), jalv);
+
+ gtk_box_pack_start(GTK_BOX(vbox), menu_bar, FALSE, FALSE, 0);
+}
+
+bool
+jalv_discover_ui(ZIX_UNUSED Jalv* jalv)
+{
+ return TRUE;
+}
+
+int
+jalv_open_ui(Jalv* jalv)
+{
+ GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ jalv->window = window;
+
+ g_signal_connect(window, "destroy",
+ G_CALLBACK(on_window_destroy), jalv);
+
+ set_window_title(jalv);
+
+ GtkWidget* vbox = new_box(false, 0);
+ gtk_window_set_role(GTK_WINDOW(window), "plugin_ui");
+ gtk_container_add(GTK_CONTAINER(window), vbox);
+
+ if (!jalv->opts.no_menu) {
+ build_menu(jalv, window, vbox);
+ }
+
+ /* Create/show alignment to contain UI (whether custom or generic) */
+ GtkWidget* alignment = gtk_alignment_new(0.5, 0.5, 1.0, 1.0);
+ gtk_box_pack_start(GTK_BOX(vbox), alignment, TRUE, TRUE, 0);
+ gtk_widget_show(alignment);
+
+ /* Attempt to instantiate custom UI if necessary */
+ if (jalv->ui && !jalv->opts.generic_ui) {
+ jalv_ui_instantiate(jalv, jalv_native_ui_type(), alignment);
+ }
+
+ if (jalv->ui_instance) {
+ GtkWidget* widget = (GtkWidget*)suil_instance_get_widget(
+ jalv->ui_instance);
+
+ gtk_container_add(GTK_CONTAINER(alignment), widget);
+ gtk_window_set_resizable(GTK_WINDOW(window), jalv_ui_is_resizable(jalv));
+ gtk_widget_show_all(vbox);
+ gtk_widget_grab_focus(widget);
+ } else {
+ GtkWidget* controls = build_control_widget(jalv, window);
+ GtkWidget* scroll_win = gtk_scrolled_window_new(NULL, NULL);
+ gtk_scrolled_window_add_with_viewport(
+ GTK_SCROLLED_WINDOW(scroll_win), controls);
+ gtk_scrolled_window_set_policy(
+ GTK_SCROLLED_WINDOW(scroll_win),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_container_add(GTK_CONTAINER(alignment), scroll_win);
+ gtk_widget_show_all(vbox);
+
+ GtkRequisition controls_size, box_size;
+ size_request(GTK_WIDGET(controls), &controls_size);
+ size_request(GTK_WIDGET(vbox), &box_size);
+
+ gtk_window_set_default_size(
+ GTK_WINDOW(window),
+ MAX(MAX(box_size.width, controls_size.width) + 24, 640),
+ box_size.height + controls_size.height);
+ }
+
+ jalv_init_ui(jalv);
+
+ g_timeout_add(1000 / jalv->ui_update_hz, (GSourceFunc)jalv_update, jalv);
+
+ gtk_window_present(GTK_WINDOW(window));
+
+ gtk_main();
+ suil_instance_free(jalv->ui_instance);
+ jalv->ui_instance = NULL;
+ zix_sem_post(&jalv->done);
+ return 0;
+}
+
+int
+jalv_close_ui(ZIX_UNUSED Jalv* jalv)
+{
+ gtk_main_quit();
+ return 0;
+}
+
+#if GTK_MAJOR_VERSION == 3
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#elif __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)
+# pragma GCC diagnostic pop
+#endif
+#endif
+