/* * nanolight.c * * Copyright © 2019 Thomas White * * This file is part of NanoLight. * * NanoLight is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include #include #include #include #include #include #include #include #define _(x) gettext(x) #include "nanolight.h" #include "command.h" #include "scanout.h" static void show_help(const char *s) { printf(_("Syntax: %s [options]\n\n"), s); printf(_("Theatrical lighting control program.\n\n" " -h, --help Display this help message.\n")); } #define OVERALL_BORDER (20.0) #define OVERALL_SPLIT (0.5) #define FIXTURE_BORDER (5.0) enum attr_class key_attrs[] = { 0, ATT_INTENSITY, /* F1 */ ATT_TILT, /* F2 */ ATT_STROBE, /* F3 */ ATT_CYAN, /* F4 */ ATT_MAGENTA, /* F5 */ ATT_YELLOW, /* F6 */ ATT_GOBO, /* F7 */ ATT_RGOBO, /* F8 */ ATT_PRISM, /* F9 */ ATT_FOCUS, /* F10 */ ATT_ZOOM, /* F11 */ ATT_ZOOM, /* F12 */ }; static double get_attr_val(struct fixture *fix, enum attr_class acls) { int i; for ( i=0; icls->n_attrs; i++ ) { if ( fix->cls->attrs[i].cls == acls ) { int max = 255; if ( fix->cls->attrs[i].props & ATTR_16BIT ) max = 65535; return (double)fix->attr_vals[i] / max; } } return 0.0; } static int fixture_selected(struct nanolight *nl, struct fixture *fix) { int i; for ( i=0; in_sel; i++ ) { if ( &nl->fixtures[nl->selection[i]] == fix ) return 1; } return 0; } static int find_attribute(struct fixture *fix, enum attr_class cls, int *n) { int j; for ( j=0; jcls->n_attrs; j++ ) { if ( fix->cls->attrs[j].cls == cls ) { *n = j; return 1; } } return 0; } static void draw_fixture(cairo_t *cr, PangoContext *pc, PangoFontDescription *fontdesc, struct nanolight *nl, struct fixture *fix) { PangoLayout *layout; const double w = 40.0; const double h = 3.0/2.0*w; char tmp[32]; int n; /* Pan/tilt (underneath rectangle) */ if ( find_attribute(fix, ATT_PAN, &n) ) { double x = w*fix->attr_vals[n] / 65535; cairo_move_to(cr, x, -1.0); cairo_line_to(cr, x, h+1.0); cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); cairo_set_line_width(cr, 1.0); cairo_stroke(cr); } if ( find_attribute(fix, ATT_TILT, &n) ) { double y = h*(1.0 - (double)fix->attr_vals[n] / 65535); cairo_move_to(cr, -1.0, y); cairo_line_to(cr, w+1.0, y); cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); cairo_set_line_width(cr, 1.0); cairo_stroke(cr); } cairo_rectangle(cr, 0.0, 0.0, w, h); if ( fixture_selected(nl, fix) ) { cairo_set_source_rgba(cr, 0.3, 0.3, 0.9, 0.9); } else { cairo_set_source_rgba(cr, 0.3, 0.3, 0.3, 0.9); } cairo_fill_preserve(cr); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_set_line_width(cr, 1.0); cairo_stroke(cr); /* Label */ layout = pango_layout_new(pc); pango_layout_set_text(layout, fix->label, -1); pango_layout_set_width(layout, (w*PANGO_SCALE)-4.0); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); pango_layout_set_font_description(layout, fontdesc); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_move_to(cr, 0.0, 2.0); pango_cairo_show_layout(cr, layout); g_object_unref(layout); /* Intensity */ snprintf(tmp, 32, "%.0f %%", get_attr_val(fix, ATT_INTENSITY)*100.0); layout = pango_layout_new(pc); pango_layout_set_text(layout, tmp, -1); pango_layout_set_width(layout, (w*PANGO_SCALE)-4.0); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); pango_layout_set_font_description(layout, fontdesc); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_move_to(cr, 0.0, 15.0); pango_cairo_show_layout(cr, layout); g_object_unref(layout); } static const char *attr_text(enum attr_class cls) { switch ( cls ) { case ATT_INTENSITY : return "Intensity"; case ATT_PAN : return "(pan)"; case ATT_TILT : return "Pan/tilt"; case ATT_STROBE : return "Strobe"; case ATT_CYAN : return "Cyan"; case ATT_MAGENTA : return "Magenta"; case ATT_YELLOW : return "Yellow"; case ATT_RGOBO : return "RGobo"; case ATT_GOBO : return "Gobo"; case ATT_PRISM : return "Prism"; case ATT_FOCUS : return "Focus"; case ATT_ZOOM : return "Zoom"; } return "(unknown)"; } static gboolean draw_sig(GtkWidget *widget, cairo_t *cr, struct nanolight *nl) { int w, h; int ch; int i; PangoContext *pc; PangoFontDescription *fontdesc; double x, y; PangoRectangle cursor; w = gtk_widget_get_allocated_width(widget); h = gtk_widget_get_allocated_height(widget); pc = gtk_widget_get_pango_context(widget); /* Overall background */ cairo_set_source_rgb(cr, 0.0, 0.0, 0.2); cairo_paint(cr); /* Separator between fixture and cue areas */ cairo_move_to(cr, w*OVERALL_SPLIT, OVERALL_BORDER); cairo_line_to(cr, w*OVERALL_SPLIT, h - OVERALL_BORDER); cairo_set_line_width(cr, 3.0); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_stroke(cr); /* Fixtures */ cairo_save(cr); cairo_translate(cr, OVERALL_BORDER, OVERALL_BORDER); x = FIXTURE_BORDER; y = FIXTURE_BORDER; fontdesc = pango_font_description_from_string("Comfortaa Bold 8"); for ( i=0; in_fixtures; i++ ) { cairo_save(cr); cairo_translate(cr, x, y); cairo_scale(cr, nl->fixture_width/40.0, nl->fixture_width/40.0); draw_fixture(cr, pc, fontdesc, nl, &nl->fixtures[i]); cairo_restore(cr); x += nl->fixture_width + FIXTURE_BORDER*2; if ( x + nl->fixture_width + FIXTURE_BORDER*2 > w*OVERALL_SPLIT ) { x = FIXTURE_BORDER; y += nl->fixture_width*3.0/2.0 + FIXTURE_BORDER*2; } } cairo_restore(cr); /* Command line */ pango_layout_set_text(nl->layout, nl->cmdline, -1); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_save(cr); cairo_translate(cr, OVERALL_BORDER, h - OVERALL_BORDER - 20.0); cairo_move_to(cr, 0.0, 0.0); pango_cairo_show_layout(cr, nl->layout); pango_layout_get_cursor_pos(nl->layout, nl->cursor_idx, &cursor, NULL); x = pango_units_to_double(cursor.x); y = pango_units_to_double(cursor.y); ch = pango_units_to_double(cursor.height); cairo_move_to(cr, x, y); cairo_line_to(cr, x, y+ch); cairo_set_line_width(cr, 3.0); cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_set_source_rgb(cr, 0.8, 0.4, 0.4); cairo_stroke(cr); cairo_restore(cr); /* Selected attribute indicator */ if ( nl->n_sel > 0 ) { pango_layout_set_text(nl->sa_layout, attr_text(nl->sel_attr), -1); cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); pango_layout_set_width(nl->sa_layout, pango_units_from_double(200.0)); cairo_move_to(cr, w - 200.0 - OVERALL_BORDER, h-OVERALL_BORDER-20.0); pango_cairo_show_layout(cr, nl->sa_layout); } return FALSE; } static struct fixture *create_fixture(struct nanolight *nl, struct fixture_class *cls, const char *label, int universe, int base_addr) { struct fixture *fix; int i; if ( nl->n_fixtures == nl->max_fixtures ) { struct fixture *fixtures_new; fixtures_new = realloc(nl->fixtures, (64+nl->max_fixtures)*sizeof(struct fixture)); if ( fixtures_new == NULL ) return NULL; nl->fixtures = fixtures_new; nl->max_fixtures += 64; } fix = &nl->fixtures[nl->n_fixtures++]; fix->label = strdup(label); fix->universe = universe; fix->base_addr = base_addr; fix->cls = cls; fix->attr_vals = calloc(cls->n_attrs, sizeof(int)); fix->attr_vals_start = calloc(cls->n_attrs, sizeof(int)); if ( (fix->attr_vals == NULL) || (fix->attr_vals_start == NULL) ) { nl->n_fixtures--; return NULL; } for ( i=0; in_attrs; i++ ) { fix->attr_vals[i] = cls->attrs[i].home; } return fix; } static void redraw(struct nanolight *nl) { gint w, h; w = gtk_widget_get_allocated_width(GTK_WIDGET(nl->da)); h = gtk_widget_get_allocated_height(GTK_WIDGET(nl->da)); gtk_widget_queue_draw_area(GTK_WIDGET(nl->da), 0, 0, w, h); } static void execute_command(struct nanolight *nl) { if ( command_run(nl->cmdline, nl) == 0 ) { nl->cmdline[0] = '\0'; nl->cursor_idx = 0; } redraw(nl); } static gboolean im_commit_sig(GtkIMContext *im, gchar *str, struct nanolight *nl) { size_t cmd_len = strlen(nl->cmdline); if ( cmd_len+strlen(str) > 1023 ) return FALSE; strcat(nl->cmdline, str); nl->cursor_idx += strlen(str); redraw(nl); return FALSE; } static size_t delete_char(char *str) { char *last; size_t len; if ( str[0] == '\0' ) return 0; last = g_utf8_find_prev_char(str, str+strlen(str)); len = strlen(last); last[0] = '\0'; return len; } static void cap_value(struct fixture *fix, int n, signed int *v) { if ( *v < 0 ) *v = 0; if ( fix->cls->attrs[n].props & ATTR_16BIT ) { if ( *v > 65535 ) *v = 65535; } else { if ( *v > 255 ) *v = 255; } } static void set_start_attrs(struct nanolight *nl, enum attr_class cls) { int i; for ( i=0; in_sel; i++ ) { int n; struct fixture *fix = &nl->fixtures[nl->selection[i]]; if ( find_attribute(fix, cls, &n) ) { fix->attr_vals_start[n] = fix->attr_vals[n]; } } /* If altering tilt, also change pan */ if ( cls == ATT_TILT ) { set_start_attrs(nl, ATT_PAN); } } static gboolean button_press_sig(GtkWidget *da, GdkEventButton *event, struct nanolight *nl) { #if 0 GdkSeat *seat; GdkWindow *win = gtk_widget_get_window(nl->da); seat = gdk_display_get_default_seat(gdk_display_get_default()); nl->pointer = gdk_seat_get_pointer(seat); #endif set_start_attrs(nl, nl->sel_attr); nl->x_orig = event->x; nl->y_orig = event->y; nl->dragging = 1; return FALSE; } static gboolean button_release_sig(GtkWidget *da, GdkEventButton *event, struct nanolight *nl) { nl->dragging = 0; return FALSE; } static double maybe_fine(struct fixture *fix, int n, double inc, int shift) { if ( !(fix->cls->attrs[n].props & ATTR_16BIT) ) return inc; if ( shift ) return inc; return inc * 100.0; } static gboolean motion_sig(GtkWidget *da, GdkEventMotion *event, struct nanolight *nl) { int i; double x_inc, y_inc; int shift; if ( !nl->dragging ) return FALSE; x_inc = (event->x - nl->x_orig)/3; y_inc = (nl->y_orig - event->y)/3; /* Mouse up means increase */ shift = event->state & GDK_SHIFT_MASK; if ( shift != nl->fine ) { nl->fine = shift; set_start_attrs(nl, nl->sel_attr); nl->x_orig = event->x; nl->y_orig = event->y; return FALSE; } if ( nl->sel_attr == ATT_TILT ) { for ( i=0; in_sel; i++ ) { int n; struct fixture *fix = &nl->fixtures[nl->selection[i]]; if ( find_attribute(fix, ATT_PAN, &n) ) { double inc = maybe_fine(fix, n, x_inc, event->state & GDK_SHIFT_MASK); signed int nv = fix->attr_vals_start[n] + inc; cap_value(fix, n, &nv); fix->attr_vals[n] = nv; } if ( find_attribute(fix, ATT_TILT, &n) ) { double inc = maybe_fine(fix, n, y_inc, event->state & GDK_SHIFT_MASK); signed int nv = fix->attr_vals_start[n] + inc; cap_value(fix, n, &nv); fix->attr_vals[n] = nv; } } } else { for ( i=0; in_sel; i++ ) { int n; struct fixture *fix = &nl->fixtures[nl->selection[i]]; if ( find_attribute(fix, nl->sel_attr, &n) ) { double inc = maybe_fine(fix, n, y_inc, event->state & GDK_SHIFT_MASK); signed int nv = fix->attr_vals_start[n] + inc; cap_value(fix, n, &nv); if ( !(fix->cls->attrs[n].props & ATTR_STOP) ) { fix->attr_vals[n] = nv; } else { printf("Can't change step attr with mouse\n"); } } } } redraw(nl); return FALSE; } static void change_stop_attr(struct nanolight *nl, signed int inc) { int i; for ( i=0; in_sel; i++ ) { struct fixture *fix = &nl->fixtures[nl->selection[i]]; int n; if ( find_attribute(fix, nl->sel_attr, &n) ) { signed int nv; if ( !(fix->cls->attrs[n].props & ATTR_STOP) ) { printf("Can't change continuous attr with keys\n"); continue; } nv = fix->attr_vals[n] + inc; if ( (nv>=0) && (nvcls->attrs[n].n_stops) ) { fix->attr_vals[n] = nv; } } } redraw(nl); } static void home_value(struct nanolight *nl) { int i; for ( i=0; in_sel; i++ ) { struct fixture *fix = &nl->fixtures[nl->selection[i]]; int n; if ( nl->sel_attr == ATT_TILT ) { if ( find_attribute(fix, ATT_PAN, &n) ) { fix->attr_vals[n] = fix->cls->attrs[n].home; } if ( find_attribute(fix, ATT_TILT, &n) ) { fix->attr_vals[n] = fix->cls->attrs[n].home; } nl->dragging = 0; } else { if ( find_attribute(fix, nl->sel_attr, &n) ) { fix->attr_vals[n] = fix->cls->attrs[n].home; } } } redraw(nl); } static gboolean key_press_sig(GtkWidget *da, GdkEventKey *event, struct nanolight *nl) { gboolean r; int claim = 1; switch ( event->keyval ) { case GDK_KEY_Left : break; case GDK_KEY_Right : break; case GDK_KEY_Up : change_stop_attr(nl, +1); break; case GDK_KEY_Down : change_stop_attr(nl, -1); break; case GDK_KEY_Return : execute_command(nl); break; case GDK_KEY_Escape : nl->cmdline[0] = '\0'; nl->cursor_idx = 0; nl->n_sel = 0; nl->dragging = 0; break; case GDK_KEY_BackSpace : nl->cursor_idx -= delete_char(nl->cmdline); break; case GDK_KEY_KP_Enter : printf("Go!\n"); break; case GDK_KEY_KP_Add : printf("Stop/back!\n"); break; case GDK_KEY_Home : home_value(nl); break; case GDK_KEY_F1 : nl->sel_attr = key_attrs[1]; nl->dragging = 0; break; case GDK_KEY_F2 : nl->sel_attr = key_attrs[2]; nl->dragging = 0; break; case GDK_KEY_F3 : nl->sel_attr = key_attrs[3]; nl->dragging = 0; break; case GDK_KEY_F4 : nl->sel_attr = key_attrs[4]; nl->dragging = 0; break; case GDK_KEY_F5 : nl->sel_attr = key_attrs[5]; nl->dragging = 0; break; case GDK_KEY_F6 : nl->sel_attr = key_attrs[6]; nl->dragging = 0; break; case GDK_KEY_F7 : nl->sel_attr = key_attrs[7]; nl->dragging = 0; break; case GDK_KEY_F8 : nl->sel_attr = key_attrs[8]; nl->dragging = 0; break; case GDK_KEY_F9 : nl->sel_attr = key_attrs[9]; nl->dragging = 0; break; case GDK_KEY_F10 : nl->sel_attr = key_attrs[10]; nl->dragging = 0; break; case GDK_KEY_F11 : nl->sel_attr = key_attrs[11]; nl->dragging = 0; break; case GDK_KEY_F12 : nl->sel_attr = key_attrs[12]; nl->dragging = 0; break; default : claim = 0; break; } if ( !claim ) { /* Throw the event to the IM context and let it sort things out */ r = gtk_im_context_filter_keypress(GTK_IM_CONTEXT(nl->im_context), event); if ( r ) claim = 1; } else { redraw(nl); } if ( claim ) return TRUE; return FALSE; } static gint realise_sig(GtkWidget *da, struct nanolight *nl) { GdkWindow *win = gtk_widget_get_window(da); PangoContext *pc; PangoFontDescription *fontdesc; /* Keyboard and input method stuff */ nl->im_context = gtk_im_multicontext_new(); gtk_im_context_set_client_window(GTK_IM_CONTEXT(nl->im_context), win); gdk_window_set_accept_focus(win, TRUE); g_signal_connect(G_OBJECT(nl->im_context), "commit", G_CALLBACK(im_commit_sig), nl); g_signal_connect(G_OBJECT(da), "key-press-event", G_CALLBACK(key_press_sig), nl); g_signal_connect(G_OBJECT(da), "button-press-event", G_CALLBACK(button_press_sig), nl); g_signal_connect(G_OBJECT(da), "button-release-event", G_CALLBACK(button_release_sig), nl); g_signal_connect(G_OBJECT(da), "motion-notify-event", G_CALLBACK(motion_sig), nl); pc = gtk_widget_get_pango_context(da); fontdesc = pango_font_description_from_string("Comfortaa Bold 16"); nl->layout = pango_layout_new(pc); pango_layout_set_alignment(nl->layout, PANGO_ALIGN_LEFT); pango_layout_set_font_description(nl->layout, fontdesc); nl->sa_layout = pango_layout_new(pc); pango_layout_set_alignment(nl->sa_layout, PANGO_ALIGN_RIGHT); pango_layout_set_font_description(nl->sa_layout, fontdesc); return FALSE; } static gboolean scanout_cb(gpointer data) { scanout_all((struct nanolight *)data); return G_SOURCE_CONTINUE; } int main(int argc, char *argv[]) { struct nanolight nl; struct fixture_class cls; struct attribute attrs[128]; int c; GtkWidget *mainwindow; GtkWidget *da; gtk_init(&argc, &argv); const struct option longopts[] = { {"help", 0, NULL, 'h'}, {0, 0, NULL, 0} }; while ((c = getopt_long(argc, argv, "h", longopts, NULL)) != -1) { switch (c) { case 'h' : show_help(argv[0]); return 0; case 0 : break; default : return 1; } } #if !GLIB_CHECK_VERSION(2,36,0) g_type_init(); #endif bindtextdomain("nanolight", LOCALEDIR); textdomain("nanolight"); /* Set up data structures */ cls.name = "Dummy fixture"; cls.n_attrs = 12; cls.attrs = attrs; cls.attrs[0].cls = ATT_INTENSITY; cls.attrs[0].props = 0; cls.attrs[0].addr_offset = 49; cls.attrs[0].home = 0; cls.attrs[1].cls = ATT_PAN; cls.attrs[1].props = ATTR_16BIT; cls.attrs[1].addr_offset = 0; cls.attrs[1].home = 32768; cls.attrs[2].cls = ATT_TILT; cls.attrs[2].props = ATTR_16BIT; cls.attrs[2].addr_offset = 2; cls.attrs[2].home = 32768; cls.attrs[3].cls = ATT_STROBE; cls.attrs[3].props = 0; cls.attrs[3].addr_offset = 48; cls.attrs[3].home = 32; cls.attrs[4].cls = ATT_CYAN; cls.attrs[4].props = ATTR_16BIT; cls.attrs[4].addr_offset = 8; cls.attrs[4].home = 0; cls.attrs[5].cls = ATT_MAGENTA; cls.attrs[5].props = ATTR_16BIT; cls.attrs[5].addr_offset = 10; cls.attrs[5].home = 0; cls.attrs[6].cls = ATT_YELLOW; cls.attrs[6].props = ATTR_16BIT; cls.attrs[6].addr_offset = 12; cls.attrs[6].home = 0; int rgobo_stops[] = {0, 6, 10, 15, 19, 24, 28}; cls.attrs[7].cls = ATT_RGOBO; cls.attrs[7].props = ATTR_STOP; cls.attrs[7].addr_offset = 24; cls.attrs[7].home = 0; cls.attrs[7].stops = rgobo_stops; cls.attrs[7].n_stops = 7; cls.attrs[8].cls = ATT_ZOOM; cls.attrs[8].props = ATTR_16BIT; cls.attrs[8].addr_offset = 32; cls.attrs[8].home = 0; cls.attrs[9].cls = ATT_FOCUS; cls.attrs[9].props = ATTR_16BIT; cls.attrs[9].addr_offset = 34; cls.attrs[9].home = 0; int gobo_stops[] = {0, 67, 73, 78, 84, 89, 95, 100, 106}; cls.attrs[10].cls = ATT_GOBO; cls.attrs[10].props = ATTR_STOP; cls.attrs[10].addr_offset = 22; cls.attrs[10].home = 0; cls.attrs[10].stops = gobo_stops; cls.attrs[10].n_stops = 9; int prism_stops[] = {0, 50}; cls.attrs[11].cls = ATT_PRISM; cls.attrs[11].props = ATTR_STOP; cls.attrs[11].addr_offset = 27; cls.attrs[11].home = 0; cls.attrs[11].stops = prism_stops; cls.attrs[11].n_stops = 2; nl.fixture_width = 80.0; nl.fixtures = NULL; nl.n_fixtures = 0; nl.max_fixtures = 0; nl.cmdline[0] = '\0'; nl.cursor_idx = 0; nl.n_sel = 0; nl.sel_attr = ATT_INTENSITY; nl.dragging = 0; create_fixture(&nl, &cls, "mh1", 0, 1); create_fixture(&nl, &cls, "mh2", 0, 52); create_fixture(&nl, &cls, "mh3", 0, 103); create_fixture(&nl, &cls, "mh4", 0, 154); /* Set up output */ g_timeout_add(100, scanout_cb, &nl); /* Create main window */ mainwindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_fullscreen(GTK_WINDOW(mainwindow)); g_signal_connect_swapped(G_OBJECT(mainwindow), "destroy", gtk_main_quit, NULL); da = gtk_drawing_area_new(); nl.da = da; gtk_container_add(GTK_CONTAINER(mainwindow), GTK_WIDGET(da)); gtk_widget_set_can_focus(GTK_WIDGET(da), TRUE); gtk_widget_add_events(da, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_MOTION_MASK); g_signal_connect(G_OBJECT(da), "draw", G_CALLBACK(draw_sig), &nl); g_signal_connect(G_OBJECT(da), "realize", G_CALLBACK(realise_sig), &nl); gtk_widget_grab_focus(GTK_WIDGET(da)); gtk_widget_show_all(mainwindow); gtk_main(); return 0; }