// public api int osc_edit(const char *hierarchy_descriptor, char type, void *ptr); // descriptor format: [ifsTF]/path/name void osc_edit_sync(int server_fd, int client_fd, unsigned timeout_ms); int osc_edit_load(const char *mask); int osc_edit_save(const char *mask); int osc_edit_reset(const char *mask); #define OSC_EDIT_PORT "2023" // maybe use portname("OSC_EDITOR_V1", 000) ? // --- impl #pragma once #include "oscpack.h" #include "oscsend.h" #include "oscrecv.h" #ifndef OSC_EDIT_INI #define OSC_EDIT_INI "oscedit.ini" #endif typedef struct osc_variant { char *key; // key address char type; // variant type union osc_variant_ { int64_t i; char *s; double f; uintptr_t up; } live[1], offline[1]; bool edited; } osc_variant; map(char*, void*) client_vars; array(char*) client_outgoing; map(char*, osc_variant) server_vars; array(char*) server_outgoing; int osc_edit(const char *hierarchy_descriptor, char type, void *ptr) { *((void**)map_find_or_add(client_vars, (char*)hierarchy_descriptor, ptr)) = ptr; array_push(client_outgoing, stringf("%c%s", type, hierarchy_descriptor)); // @leak return 0; } int osc_edit_load(const char *mask) { ini_t map = ini(OSC_EDIT_INI); if( !map ) return 0; map_clear(server_vars); // @todo: @leak: iterate variant strings and free() them for each_map_ptr_sorted(map, char *, key, char *, val) { if( strmatchi(*key + 2, mask ) ) { // skip initial char_type+dot printf("%s=%s\n", *key, *val); osc_variant variant = {0}; variant.type = 0[*key]; /**/ if( variant.type == 'i' ) variant.live[0].i = atoi(*val); else if( variant.type == 'f' ) variant.live[0].f = atof(*val); else if( variant.type == 's' ) variant.live[0].s = STRDUP(*val); else if( strchr("bTF",variant.type) ) variant.live[0].i = (*val)[3] == 't'; variant.key = STRDUP(*key + 2); variant.offline[0] = variant.live[0]; map_insert(server_vars, STRDUP(*key + 2), variant); // @todo: no need to STRDUP() here. we got variant.key already allocated. } } map_free(map); ui_notify("Loaded.", NULL); return 1; } int osc_edit_save(const char *mask) { unlink( OSC_EDIT_INI ); for each_map_ptr(server_vars, char *, title, osc_variant, msg) { if( strmatchi(*title, mask) ) { msg->edited = false; if( msg->type == 's' ) msg->offline[0].s = STRDUP(msg->live[0].s); // @leak else memcpy( &msg->offline[0], &msg->live[0], sizeof(msg->offline[0]) ); if( msg->type == 'i' ) ini_write(OSC_EDIT_INI, "i", msg->key, va("%d", (int)msg->live[0].i )); if( msg->type == 'f' ) ini_write(OSC_EDIT_INI, "f", msg->key, va("%f", (float)msg->live[0].f )); if( msg->type == 's' ) ini_write(OSC_EDIT_INI, "s", msg->key, va("%s", msg->live[0].s )); if( strchr("bTF",msg->type) ) ini_write(OSC_EDIT_INI, "b", msg->key, va("%s", msg->live[0].i ? "true":"false" )); } } ui_notify("Saved.", NULL); return 1; } int osc_edit_reset(const char *mask) { for each_map_ptr(server_vars, char *, title, osc_variant, msg) { if( strmatchi(*title, mask) ) { msg->edited = false; if( msg->type == 's' ) msg->live[0].s = STRDUP(msg->offline[0].s); // @leak else memcpy( &msg->live[0], &msg->offline[0], sizeof(msg->live[0]) ); } } return 1; } void osc_edit_sync(int server_fd, int client_fd, unsigned timeout_ms) { // client logic if( client_fd >= 0 ) { // 1. send outgoing vars (widget requests at server) // 2. recv modified vars (from user tweaking widgets), wait till timeout_ms // received vars are removed from outgoing queue, so it's never submitted again until it gets modified. // [1] for(int i = 0; i < array_count(client_outgoing); ++i) { char *out = 0; /**/ if( client_outgoing[i][0] == 'i' ) out = osc_pack_va(client_outgoing[i]+1, "i", *(int*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); else if( client_outgoing[i][0] == 'f' ) out = osc_pack_va(client_outgoing[i]+1, "f", *(float*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); else if( client_outgoing[i][0] == 's' ) out = osc_pack_va(client_outgoing[i]+1, "s", *(char**)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); else if( client_outgoing[i][0] == 'b' ) out = osc_pack_va(client_outgoing[i]+1, *(bool*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ? "T":"F" ); else if( client_outgoing[i][0] == 'T' ) out = osc_pack_va(client_outgoing[i]+1, "T" ); else if( client_outgoing[i][0] == 'F' ) out = osc_pack_va(client_outgoing[i]+1, "F" ); else if( client_outgoing[i][0] == '\0') out = osc_pack_va(client_outgoing[i]+1, "" ); else ASSERT(0, "unsupported osc_edit command `%c`", client_outgoing[i][0]); if( out ) osc_send(client_fd, out+4, *(int*)out); } for(int i = 0; i < array_count(client_outgoing); ++i) { FREE(client_outgoing[i]); } array_clear(client_outgoing); // [2] sleep_ms(timeout_ms); } // server logic if( server_fd >= 0 ) { // 1. recv vars from client // 2. if they do not exist: instantiate UI controls for them // 3. if they do exist: update values from client // 4. when UI controls are modified, update our local copies and send its values back to client osc_update(server_fd, timeout_ms); // call every frame. reads the udp port and parse all messages found there const osc_message *begin; for( int it = 0, end = osc_list(&begin); it < end; ++it ) { const osc_message *msg = begin + it; osc_variant oscv, zero = {0}; oscv.key = STRDUP(msg->pattern); oscv.type = msg->types[0]; if(oscv.type == 's') oscv.live[0].s = STRDUP(msg->v[0].s), oscv.offline[0].s = STRDUP(msg->v[0].s); else memcpy(&oscv.live[0], &msg->v[0], sizeof(oscv.live[0])), memcpy(&oscv.offline[0], &msg->v[0], sizeof(oscv.live[0])); *map_find_or_add_allocated_key(server_vars, STRDUP(msg->pattern), zero) = oscv; } static int on = 1; if( ui_window("editor", &on) ) { int num_messages = osc_list(&begin); int prev_title_len = 0; char *prev_title = 0; if( !map_count(server_vars) ) { // create section header int choice = ui_label2_toolbar("New", ICON_MD_LOOP); if( choice == 1 ) osc_edit_load("*"); } else for each_map_ptr_sorted(server_vars, char *, title, osc_variant, msg) { char *second_slash = strchr(*title + 1, '/'); int title_len = (int)(second_slash - *title) - 1; // -> /player/title -> player int different_size = title_len != prev_title_len; int new_section = different_size || (prev_title && strncmp(prev_title, *title, title_len+1)); // create section header if( new_section ) { if( prev_title ) ui_separator(); char *caption = va("*%.*s", title_len + 2, *title); int choice = ui_label2_toolbar(caption, prev_title ? "" : va(">%d" ICON_MD_EMAIL " " ICON_MD_UNDO " " ICON_MD_LOOP " " ICON_MD_SD_CARD, num_messages)); // ICON_MD_UNDO " " ICON_MD_UPLOAD " " ICON_MD_DOWNLOAD if( choice == 3 ) osc_edit_reset("*"); if( choice == 2 ) osc_edit_load("*"); if( choice == 1 ) osc_edit_save("*"); prev_title_len = title_len, prev_title = va("%s", *title); } else { prev_title_len = title_len; } ui_label_icon_highlight = !!msg->edited; // vec2 ui_label_icon_clicked_L; // left // vec2 ui_label_icon_clicked_R; // right char *title_copy = va(ICON_MD_UNDO " " "%s", *title + 1 + title_len + 1); // /player/titlexx -> UNDO_ICON titlexx // create gui elements /**/ if( msg->type == '\0') { ui_label(title_copy); } else if( msg->type == 'i' ) { int i = (int)msg->live[0].i; msg->edited |= ui_int(title_copy, &i); msg->live[0].i = i; } else if( msg->type == 'f' ) { float f = msg->live[0].f; msg->edited |= ui_float(title_copy, &f); msg->live[0].f = f; } else if( msg->type == 's' ) { char s[128] = {0}; strncpy(s, msg->live[0].s, sizeof(s)); uint64_t old = hash_str(s); ui_buffer(title_copy, s, sizeof(s)); uint64_t mod = hash_str(s); if(mod != old) msg->edited |= 1, FREE(msg->live[0].s), msg->live[0].s = stringf("%s",s); } else if( strchr("bTF",msg->type) ) { bool b = !!msg->live[0].i; msg->edited |= ui_bool(title_copy, &b); msg->live[0].i = b; } else ASSERT(0, "unsupported osc_edit command `%c`", msg->type); if( ui_label_icon_clicked_L.x > 0 && ui_label_icon_clicked_L.x <= 24 ) { // if clicked on UNDO icon (1st icon) osc_edit_reset(*title); } } ui_window_end(); } ui_demo(0); } }