From 0ebea86cdb7cd6fa2cfc67d0c84d6a57b44e2203 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Fri, 12 Feb 2021 15:46:07 -0800 Subject: [PATCH 01/17] Add code for boolean config file options --- src/config.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config.c b/src/config.c index 18d1ee9..220b294 100644 --- a/src/config.c +++ b/src/config.c @@ -44,6 +44,8 @@ g_key_file_save_to_file (GKeyFile *key_file, #define g_key_file_get_int g_key_file_get_integer #define g_key_file_set_int g_key_file_set_integer // the devil may take glib +#define g_key_file_get_bool g_key_file_get_boolean +#define g_key_file_set_bool g_key_file_set_boolean void load_config(struct main_window *w) { From 13269eea89a18ab48161333039f855a0e7b7690d Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 00:26:20 -0800 Subject: [PATCH 02/17] Create utilities for menu items Create some utilities for creating menu items so there isn't so much copy and paste of the code. --- src/interface.c | 55 +++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/interface.c b/src/interface.c index 7c1d3ca..707c766 100644 --- a/src/interface.c +++ b/src/interface.c @@ -694,6 +694,29 @@ static void load(GtkMenuItem *m, struct main_window *w) gtk_widget_destroy(dialog); } +/* Add a checkbox with name to the given menu, with initial state active and + * attach the supplied callback and parameter to the toggled signal. Set is set + * before attaching the signal, so the callback is not called when created. */ +static GtkWidget* add_checkbox(GtkWidget* menu, const char* name, bool active, GCallback callback, void* param) +{ + GtkWidget *checkbox = gtk_check_menu_item_new_with_label(name); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(checkbox), active); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), checkbox); + g_signal_connect(checkbox, "toggled", callback, param); + return checkbox; +} + +/* Add a menu item with given label to the given menu, with the supplied initial +* sensitivity, callback, and callback parameter. */ +static GtkWidget* add_menu_item(GtkWidget* menu, const char* label, bool sensitive, GCallback callback, void* param) +{ + GtkWidget *item = gtk_menu_item_new_with_label(label); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + gtk_widget_set_sensitive(item, sensitive); + g_signal_connect(item, "activate", callback, param); + return item; +} + /* Set up the main window and populate with widgets */ static void init_main_window(struct main_window *w) { @@ -800,47 +823,29 @@ static void init_main_window(struct main_window *w) gtk_box_pack_end(GTK_BOX(hbox), command_menu_button, FALSE, FALSE, 0); // ... Open - GtkWidget *open_item = gtk_menu_item_new_with_label("Open"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), open_item); - g_signal_connect(open_item, "activate", G_CALLBACK(load), w); + add_menu_item(command_menu, "Open", true, G_CALLBACK(load), w); // ... Save - w->save_item = gtk_menu_item_new_with_label("Save current display"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->save_item); - g_signal_connect(w->save_item, "activate", G_CALLBACK(save_current), w); - gtk_widget_set_sensitive(w->save_item, FALSE); + w->save_item = add_menu_item(command_menu, "Save current display", false, G_CALLBACK(save_current), w); // ... Save all - w->save_all_item = gtk_menu_item_new_with_label("Save all snapshots"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->save_all_item); - g_signal_connect(w->save_all_item, "activate", G_CALLBACK(save_all), w); - gtk_widget_set_sensitive(w->save_all_item, FALSE); + w->save_all_item = add_menu_item(command_menu, "Save all snapshots", false, G_CALLBACK(save_all), w); gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); // ... Light checkbox - GtkWidget *light_checkbox = gtk_check_menu_item_new_with_label("Light algorithm"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), light_checkbox); - g_signal_connect(light_checkbox, "toggled", G_CALLBACK(handle_light), w); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(light_checkbox), w->is_light); + add_checkbox(command_menu, "Light algorithm", w->is_light, G_CALLBACK(handle_light), w); // ... Calibrate checkbox - w->cal_button = gtk_check_menu_item_new_with_label("Calibrate"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->cal_button); - g_signal_connect(w->cal_button, "toggled", G_CALLBACK(handle_calibrate), w); + w->cal_button = add_checkbox(command_menu, "Calibrate", false, G_CALLBACK(handle_calibrate), w); gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); // ... Close all - w->close_all_item = gtk_menu_item_new_with_label("Close all snapshots"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->close_all_item); - g_signal_connect(w->close_all_item, "activate", G_CALLBACK(close_all), w); - gtk_widget_set_sensitive(w->close_all_item, FALSE); + w->close_all_item = add_menu_item(command_menu, "Close all snapshots", false, G_CALLBACK(close_all), w); // ... Quit - GtkWidget *quit_item = gtk_menu_item_new_with_label("Quit"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), quit_item); - g_signal_connect(quit_item, "activate", G_CALLBACK(handle_quit), w); + add_menu_item(command_menu, "Quit", true, G_CALLBACK(handle_quit), w); gtk_widget_show_all(command_menu); From 41ee57a03c5c03be6317eda01e4b876e8525384e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 00:38:17 -0800 Subject: [PATCH 03/17] Break up the output panel GUI creation function It's quite large. Split it into new -functions that create the various parts of the output panel. The waveforms (tic, toc, period, debug), the paper strip chart and its controls, and the window under the output display that contains them, each get their own function. There is some minimal support for a vertical vs horizontal paperstrip chart, but it's not used yet. --- src/output_panel.c | 121 ++++++++++++++++++++++++++++++--------------- src/tg.h | 3 ++ 2 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 0e7fc8b..81b567e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -782,89 +782,130 @@ void op_destroy(struct output_panel *op) free(op); } -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +/* Creates the paperstrip, with buttons. Returns top level Widget that contains + * them. Vertical controls orientation of paper strip. */ +static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) { - struct output_panel *op = malloc(sizeof(struct output_panel)); - - op->computer = comp; - op->snst = snst; - - op->panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_container_set_border_width(GTK_CONTAINER(op->panel), border); - - // Info area on top - op->output_drawing_area = gtk_drawing_area_new(); - gtk_widget_set_size_request(op->output_drawing_area, 0, OUTPUT_WINDOW_HEIGHT); - gtk_box_pack_start(GTK_BOX(op->panel),op->output_drawing_area, FALSE, TRUE, 0); - g_signal_connect (op->output_drawing_area, "draw", G_CALLBACK(output_draw_event), op); - gtk_widget_set_events(op->output_drawing_area, GDK_EXPOSURE_MASK); - - GtkWidget *hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(op->panel), hbox2, TRUE, TRUE, 0); - - GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_box_pack_start(GTK_BOX(hbox2), vbox2, FALSE, TRUE, 0); + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // Paperstrip op->paperstrip_drawing_area = gtk_drawing_area_new(); gtk_widget_set_size_request(op->paperstrip_drawing_area, 300, 0); - gtk_box_pack_start(GTK_BOX(vbox2), op->paperstrip_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->paperstrip_drawing_area, "draw", G_CALLBACK(paperstrip_draw_event), op); gtk_widget_set_events(op->paperstrip_drawing_area, GDK_EXPOSURE_MASK); - GtkWidget *hbox3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(vbox2), hbox3, FALSE, TRUE, 0); + // Buttons + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); // < button - GtkWidget *left_button = gtk_button_new_with_label("<"); - gtk_box_pack_start(GTK_BOX(hbox3), left_button, TRUE, TRUE, 0); - g_signal_connect (left_button, "clicked", G_CALLBACK(handle_left), op); + op->left_button = gtk_button_new_from_icon_name( + vertical ? "pan-start-symbolic" : "pan-up-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_box_pack_start(GTK_BOX(hbox), op->left_button, TRUE, TRUE, 0); + g_signal_connect (op->left_button, "clicked", G_CALLBACK(handle_left), op); // CLEAR button - if(comp) { + if(op->computer) { op->clear_button = gtk_button_new_with_label("Clear"); - gtk_box_pack_start(GTK_BOX(hbox3), op->clear_button, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), op->clear_button, TRUE, TRUE, 0); g_signal_connect (op->clear_button, "clicked", G_CALLBACK(handle_clear_trace), op); - gtk_widget_set_sensitive(op->clear_button, !snst->calibrate); + gtk_widget_set_sensitive(op->clear_button, !op->snst->calibrate); } // CENTER button GtkWidget *center_button = gtk_button_new_with_label("Center"); - gtk_box_pack_start(GTK_BOX(hbox3), center_button, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), center_button, TRUE, TRUE, 0); g_signal_connect (center_button, "clicked", G_CALLBACK(handle_center_trace), op); // > button - GtkWidget *right_button = gtk_button_new_with_label(">"); - gtk_box_pack_start(GTK_BOX(hbox3), right_button, TRUE, TRUE, 0); - g_signal_connect (right_button, "clicked", G_CALLBACK(handle_right), op); + op->right_button = gtk_button_new_from_icon_name( + vertical ? "pan-end-symbolic" : "pan-down-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_box_pack_start(GTK_BOX(hbox), op->right_button, TRUE, TRUE, 0); + g_signal_connect (op->right_button, "clicked", G_CALLBACK(handle_right), op); + + return vbox; +} - GtkWidget *vbox3 = gtk_box_new(GTK_ORIENTATION_VERTICAL,10); - gtk_box_pack_start(GTK_BOX(hbox2), vbox3, TRUE, TRUE, 0); +/* Create the tic, toc, and period waveforms. Returns the GtkBox that contains + * them. Vertical controls how the waves are stacked. */ +static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) +{ + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->tic_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), op->tic_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->tic_drawing_area, "draw", G_CALLBACK(tic_draw_event), op); gtk_widget_set_events(op->tic_drawing_area, GDK_EXPOSURE_MASK); // Toc waveform area op->toc_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->toc_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), op->toc_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->toc_drawing_area, "draw", G_CALLBACK(toc_draw_event), op); gtk_widget_set_events(op->toc_drawing_area, GDK_EXPOSURE_MASK); // Period waveform area op->period_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->period_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), op->period_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->period_drawing_area, "draw", G_CALLBACK(period_draw_event), op); gtk_widget_set_events(op->period_drawing_area, GDK_EXPOSURE_MASK); #ifdef DEBUG op->debug_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->debug_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), op->debug_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->debug_drawing_area, "draw", G_CALLBACK(debug_draw_event), op); gtk_widget_set_events(op->debug_drawing_area, GDK_EXPOSURE_MASK); #endif + return box; +} + +/* Create container and place paperstrip and waveforms in either vertical or + * horizontal paperstrip orientation. Puts container in the panel and shows it. */ +static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWidget *waveforms, bool vertical) +{ + op->displays = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_pack_start(GTK_BOX(op->displays), paperstrip, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(op->displays), waveforms, TRUE, TRUE, 0); + + gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); + gtk_widget_show(op->displays); +} + +/* Create the paperstrip and waveforms, a container for them, and place it into + * the panel. Returns containing Widget. Vertical controls paperstrip + * orientation. */ +static GtkWidget *create_displays(struct output_panel *op, bool vertical) +{ + // The paperstrip and buttons + GtkWidget *paperstrip = create_paperstrip(op, vertical); + // Tic/toc/period waveform area + GtkWidget *waveforms = create_waveforms(op, vertical); + + place_displays(op, paperstrip, waveforms, vertical); + + return op->displays; +} + +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +{ + struct output_panel *op = malloc(sizeof(struct output_panel)); + + op->computer = comp; + op->snst = snst; + + op->panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_container_set_border_width(GTK_CONTAINER(op->panel), border); + + // Info area on top + op->output_drawing_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(op->output_drawing_area, 0, OUTPUT_WINDOW_HEIGHT); + gtk_box_pack_start(GTK_BOX(op->panel),op->output_drawing_area, FALSE, TRUE, 0); + g_signal_connect (op->output_drawing_area, "draw", G_CALLBACK(output_draw_event), op); + gtk_widget_set_events(op->output_drawing_area, GDK_EXPOSURE_MASK); + + create_displays(op, true); + return op; } diff --git a/src/tg.h b/src/tg.h index 789f66c..86785d4 100644 --- a/src/tg.h +++ b/src/tg.h @@ -200,11 +200,14 @@ struct output_panel { GtkWidget *panel; GtkWidget *output_drawing_area; + GtkWidget *displays; GtkWidget *tic_drawing_area; GtkWidget *toc_drawing_area; GtkWidget *period_drawing_area; GtkWidget *paperstrip_drawing_area; GtkWidget *clear_button; + GtkWidget *left_button; + GtkWidget *right_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif From 4e68612842def7dc3eacfde613e62d7e0abadc52 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 01:47:15 -0800 Subject: [PATCH 04/17] Use resizble paned widget for paperstrip and waveforms This lets the size of the paperstrip vs the waveforms be adjusted by sliding the pane between them. To get the layout right, we need to give the waveforms a minimum size. 300x150 seems about the minimum. It should look basically the same as it did before. Each snapshot has its own unique pane position. Adjusting one tab in the notebook doesn't adjust the other tabs. It might be nice if that weren't the case and they were all synced. --- src/output_panel.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 81b567e..095d31b 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -835,18 +835,21 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(op->tic_drawing_area, 300, 150); gtk_box_pack_start(GTK_BOX(box), op->tic_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->tic_drawing_area, "draw", G_CALLBACK(tic_draw_event), op); gtk_widget_set_events(op->tic_drawing_area, GDK_EXPOSURE_MASK); // Toc waveform area op->toc_drawing_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(op->toc_drawing_area, 300, 150); gtk_box_pack_start(GTK_BOX(box), op->toc_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->toc_drawing_area, "draw", G_CALLBACK(toc_draw_event), op); gtk_widget_set_events(op->toc_drawing_area, GDK_EXPOSURE_MASK); // Period waveform area op->period_drawing_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(op->period_drawing_area, 300, 150); gtk_box_pack_start(GTK_BOX(box), op->period_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->period_drawing_area, "draw", G_CALLBACK(period_draw_event), op); gtk_widget_set_events(op->period_drawing_area, GDK_EXPOSURE_MASK); @@ -865,9 +868,10 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) * horizontal paperstrip orientation. Puts container in the panel and shows it. */ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWidget *waveforms, bool vertical) { - op->displays = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(op->displays), paperstrip, FALSE, TRUE, 0); - gtk_box_pack_start(GTK_BOX(op->displays), waveforms, TRUE, TRUE, 0); + op->displays = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); + gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, FALSE, FALSE); + gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); gtk_widget_show(op->displays); From 05e85bc3ccf2f4b745e8e64275e2e208ba0e9dff Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 02:23:26 -0800 Subject: [PATCH 05/17] Add vertical/horizontal paperstrip option This allow changing the layout so the paperstrip is horizontal, scrolling to the left, with the three waveforms arraigned horizontally below the paperstrip. The effect is to make the paperstrip longer and give it more space. For many tasks the paper strip is much more useful than the waveform displays. This allows adjusting the layout to be more useful in those cases. A new checkbox in the menu is added to control this. It can be changed on the fly and the widgets will be re-arraigned. A config file option, "vertical_paperstrip", is added to remember the current setting. Default vertical layout to true if not present in config file. If multiple snapshots are present, the change of orientation will affect all of them, rather that just the current viewed one. It would be possible to change this. --- src/interface.c | 24 ++++++++++++++-- src/output_panel.c | 70 ++++++++++++++++++++++++++++++++++++++-------- src/tg.h | 13 +++++++-- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/interface.c b/src/interface.c index 707c766..9ae4cb7 100644 --- a/src/interface.c +++ b/src/interface.c @@ -422,7 +422,7 @@ static GtkWidget *make_tab_label(char *name, struct output_panel *panel_to_close static void add_new_tab(struct snapshot *s, char *name, struct main_window *w) { - struct output_panel *op = init_output_panel(NULL, s, 5); + struct output_panel *op = init_output_panel(NULL, s, 5, w->vertical_layout); GtkWidget *label = make_tab_label(name, op); gtk_widget_show_all(op->panel); @@ -694,6 +694,22 @@ static void load(GtkMenuItem *m, struct main_window *w) gtk_widget_destroy(dialog); } +static void handle_layout(GtkCheckMenuItem *b, struct main_window *w) +{ + const bool vertical = gtk_check_menu_item_get_active(b) == TRUE; + + w->vertical_layout = vertical; + set_panel_layout(w->active_panel, vertical); + + int n = 0; + GtkWidget *panel; + while ((panel = gtk_notebook_get_nth_page(GTK_NOTEBOOK(w->notebook), n++))) { + struct output_panel *op = g_object_get_data(G_OBJECT(panel), "op-pointer"); + if(op) + set_panel_layout(op, vertical); + } +} + /* Add a checkbox with name to the given menu, with initial state active and * attach the supplied callback and parameter to the toggled signal. Set is set * before attaching the signal, so the callback is not called when created. */ @@ -839,6 +855,9 @@ static void init_main_window(struct main_window *w) // ... Calibrate checkbox w->cal_button = add_checkbox(command_menu, "Calibrate", false, G_CALLBACK(handle_calibrate), w); + // Layout checkbox + add_checkbox(command_menu, "Vertical", w->vertical_layout, G_CALLBACK(handle_layout), w); + gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); // ... Close all @@ -936,6 +955,7 @@ static void start_interface(GApplication* app, void *p) w->la = DEFAULT_LA; w->calibrate = 0; w->is_light = 0; + w->vertical_layout = true; load_config(w); @@ -959,7 +979,7 @@ static void start_interface(GApplication* app, void *p) w->computer->curr = NULL; compute_results(w->active_snapshot); - w->active_panel = init_output_panel(w->computer, w->active_snapshot, 0); + w->active_panel = init_output_panel(w->computer, w->active_snapshot, 0, w->vertical_layout); init_main_window(w); diff --git a/src/output_panel.c b/src/output_panel.c index 095d31b..97b00cf 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -549,9 +549,19 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp GtkAllocation temp; gtk_widget_get_allocation (op->paperstrip_drawing_area, &temp); + int width, height; - int width = temp.width; - int height = temp.height; + /* The paperstrip is coded to be vertical; horizontal uses cairo to rotate it. */ + if(op->vertical_layout) { + width = temp.width; + height = temp.height; + } else { + width = temp.height; + height = temp.width; + + cairo_translate(c, height, 0); + cairo_rotate(c, M_PI/2); + } int stopped = 0; if( snst->events_count && @@ -790,7 +800,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) // Paperstrip op->paperstrip_drawing_area = gtk_drawing_area_new(); - gtk_widget_set_size_request(op->paperstrip_drawing_area, 300, 0); + gtk_widget_set_size_request(op->paperstrip_drawing_area, 150, 150); gtk_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->paperstrip_drawing_area, "draw", G_CALLBACK(paperstrip_draw_event), op); gtk_widget_set_events(op->paperstrip_drawing_area, GDK_EXPOSURE_MASK); @@ -831,7 +841,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) * them. Vertical controls how the waves are stacked. */ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) { - GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + GtkWidget *box = gtk_box_new(vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL, 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); @@ -868,11 +878,24 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) * horizontal paperstrip orientation. Puts container in the panel and shows it. */ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWidget *waveforms, bool vertical) { - op->displays = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + op->vertical_layout = vertical; + + op->displays = gtk_paned_new(vertical ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL); gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); - gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, FALSE, FALSE); + + gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, vertical ? FALSE : TRUE, FALSE); + + gtk_orientable_set_orientation(GTK_ORIENTABLE(waveforms), vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL); gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); + /* Make paperstrip arrows buttons point correct way */ + GtkWidget *left_arrow = gtk_button_get_image(GTK_BUTTON(op->left_button)); + gtk_image_set_from_icon_name(GTK_IMAGE(left_arrow), + vertical ? "pan-start-symbolic" : "pan-up-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + GtkWidget *right_arrow = gtk_button_get_image(GTK_BUTTON(op->right_button)); + gtk_image_set_from_icon_name(GTK_IMAGE(right_arrow), + vertical ? "pan-end-symbolic" : "pan-down-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); gtk_widget_show(op->displays); } @@ -883,16 +906,41 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi static GtkWidget *create_displays(struct output_panel *op, bool vertical) { // The paperstrip and buttons - GtkWidget *paperstrip = create_paperstrip(op, vertical); + op->paperstrip_box = create_paperstrip(op, vertical); // Tic/toc/period waveform area - GtkWidget *waveforms = create_waveforms(op, vertical); + op->waveforms_box = create_waveforms(op, vertical); - place_displays(op, paperstrip, waveforms, vertical); + place_displays(op, op->paperstrip_box, op->waveforms_box, vertical); return op->displays; } -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +/* Change orientation of existing output panel. Is a no-op if orientation is + * not changed. */ +void set_panel_layout(struct output_panel *op, bool vertical) +{ + if (op->vertical_layout == vertical) + return; + + /* Remove waveforms and paperstrip containers from displays container, + * then use place_displays() to put them into a new displays container. + * The need to be refed so they are not deleted when removed from the + * container. */ + g_object_ref(op->waveforms_box); + gtk_container_remove(GTK_CONTAINER(op->displays), op->waveforms_box); + + g_object_ref(op->paperstrip_box); + gtk_container_remove(GTK_CONTAINER(op->displays), op->paperstrip_box); + + gtk_widget_destroy(op->displays); op->displays = NULL; + place_displays(op, op->paperstrip_box, op->waveforms_box, vertical); + + /* They are now refed by op->displays so we don't need our refs anymore */ + g_object_unref(op->paperstrip_box); + g_object_unref(op->waveforms_box); +} + +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border, bool vertical) { struct output_panel *op = malloc(sizeof(struct output_panel)); @@ -909,7 +957,7 @@ struct output_panel *init_output_panel(struct computer *comp, struct snapshot *s g_signal_connect (op->output_drawing_area, "draw", G_CALLBACK(output_draw_event), op); gtk_widget_set_events(op->output_drawing_area, GDK_EXPOSURE_MASK); - create_displays(op, true); + create_displays(op, vertical); return op; } diff --git a/src/tg.h b/src/tg.h index 86785d4..f92d640 100644 --- a/src/tg.h +++ b/src/tg.h @@ -201,22 +201,28 @@ struct output_panel { GtkWidget *output_drawing_area; GtkWidget *displays; + GtkWidget *waveforms_box; GtkWidget *tic_drawing_area; GtkWidget *toc_drawing_area; GtkWidget *period_drawing_area; + GtkWidget *paperstrip_box; GtkWidget *paperstrip_drawing_area; GtkWidget *clear_button; GtkWidget *left_button; GtkWidget *right_button; + GtkWidget *zoom_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif + bool vertical_layout; + struct computer *computer; struct snapshot *snst; }; void initialize_palette(); -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border); +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border, bool vertical_layout); +void set_panel_layout(struct output_panel *op, bool vertical); void redraw_op(struct output_panel *op); void op_set_snapshot(struct output_panel *op, struct snapshot *snst); void op_set_border(struct output_panel *op, int i); @@ -253,6 +259,8 @@ struct main_window { int cal; // 0.1 s/d int nominal_sr; + bool vertical_layout; + GKeyFile *config_file; gchar *config_file_name; struct conf_data *conf_data; @@ -275,7 +283,8 @@ void error(char *format,...); OP(bph, bph, int) \ OP(lift_angle, la, double) \ OP(calibration, cal, int) \ - OP(light_algorithm, is_light, int) + OP(light_algorithm, is_light, int) \ + OP(vertical_paperstrip, vertical_layout, bool) struct conf_data { #define DEF(NAME,PLACE,TYPE) TYPE PLACE; From 6f088c01f695ad80e814ba7116d42fa3878522ff Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 7 Mar 2021 21:43:36 -0800 Subject: [PATCH 06/17] Scale font using paperstrip/waveform window size Using the size of the main app window, what was done, doesn't work so well now that the space allocation between the paperstrip and waveforms can be changed. This way making the waveforms (or paperstrip) smaller will give a smaller font that fits in the smaller space better. --- src/output_panel.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 97b00cf..c34b597 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -322,10 +322,9 @@ static void expose_waveform( int width = temp.width; int height = temp.height; - gtk_widget_get_allocation(gtk_widget_get_toplevel(da), &temp); - int font = temp.width / 90; - if(font < 12) - font = 12; + int font = width / 25; + font = font < 12 ? 12 : font > 24 ? 24 : font; + int i; cairo_set_font_size(c,font); @@ -670,18 +669,14 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_line_to(c, right_margin + .5, height - 20.5); cairo_fill(c); - char s[100]; - cairo_text_extents_t extents; + int font = width / 25; + cairo_set_font_size(c, font < 12 ? 12 : font > 24 ? 24 : font); - gtk_widget_get_allocation(gtk_widget_get_toplevel(widget), &temp); - int font = temp.width / 90; - if(font < 12) - font = 12; - cairo_set_font_size(c,font); - - sprintf(s, "%.1f ms", snst->calibrate ? + char s[32]; + snprintf(s, sizeof(s), "%.1f ms", snst->calibrate ? 1000. / zoom_factor : 3600000. / (snst->guessed_bph * zoom_factor)); + cairo_text_extents_t extents; cairo_text_extents(c,s,&extents); cairo_move_to(c, (width - extents.x_advance)/2, height - 30); cairo_show_text(c,s); From 9f1f263862cab37f42a6ad2cdfd7932fce086bfe Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 7 Mar 2021 02:58:10 -0800 Subject: [PATCH 07/17] Move snapshot display parameters to their own struct Create a new struct for the output_panel snapshot display parameters. Instead of being mixed into the snapshot data from the computer, there will just be a pointer to the display parameter struct in the snapshot. The clone and delete snapshot functions will clone and delete the display parameters, if they exist. The computer's snapshots don't have display parameters so the pointer will just be NULL for them. Each time a new snapshot is passed from the computer to the interface, the pointer to the display parameters can be kept so that each parameter doesn't need to be copied. paperstrip_draw_event() will allocate a new display parameter when it gets a snapshot for the first time. This will make it a lot easier to add new display parameters just by adding them to the struct. And they can be initialized in the output panel code that uses them and knows what they should be. The computer doesn't need to know anything about them. This does break the savefile format slightly since the struct field names are coded into the savefile. Maybe that design has some drawbacks when it comes to backward compatibility. In this case the trace centering parameter is the only one broken and it doesn't really matter. --- src/computer.c | 6 +++++- src/interface.c | 4 ++-- src/output_panel.c | 16 ++++++++++++---- src/serializer.c | 11 +++++------ src/tg.h | 10 +++++++++- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/computer.c b/src/computer.c index 03942d3..a4f2bda 100644 --- a/src/computer.c +++ b/src/computer.c @@ -48,12 +48,17 @@ struct snapshot *snapshot_clone(struct snapshot *s) t->events_wp = 0; t->events = NULL; } + if(s->d) { + t->d = malloc(sizeof(*t->d)); + *t->d = *s->d; + } return t; } void snapshot_destroy(struct snapshot *s) { if(s->pb) pb_destroy_clone(s->pb); + if(s->d) free(s->d); free(s->events); free(s); } @@ -267,7 +272,6 @@ struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int memset(s->events,0,EVENTS_COUNT * sizeof(uint64_t)); s->events_wp = 0; s->events_from = 0; - s->trace_centering = 0; s->bph = bph; s->la = la; s->cal = cal; diff --git a/src/interface.c b/src/interface.c index 9ae4cb7..59f145d 100644 --- a/src/interface.c +++ b/src/interface.c @@ -899,11 +899,11 @@ guint refresh(struct main_window *w) lock_computer(w->computer); struct snapshot *s = w->computer->curr; if(s) { - double trace_centering = w->active_snapshot->trace_centering; + s->d = w->active_snapshot->d; + w->active_snapshot->d = NULL; snapshot_destroy(w->active_snapshot); w->active_snapshot = s; w->computer->curr = NULL; - s->trace_centering = trace_centering; if(w->computer->clear_trace && !s->calibrate) memset(s->events,0,s->events_count*sizeof(uint64_t)); if(s->calibrate && s->cal_state == 1 && s->cal_result != w->cal) { diff --git a/src/output_panel.c b/src/output_panel.c index c34b597..299cce3 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -528,11 +528,19 @@ static gboolean period_draw_event(GtkWidget *widget, cairo_t *c, struct output_p static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct output_panel *op) { int i; - struct snapshot *snst = op->snst; + const struct snapshot *snst = op->snst; + struct display *ssd = snst->d; uint64_t time = snst->timestamp ? snst->timestamp : get_timestamp(snst->is_light); double sweep; int zoom_factor; double slope = 1000; // detected rate: 1000 -> do not display + + // Allocate initial display parameters. Will persist across each new + // snapshot after this. + if (!ssd) { + ssd = op->snst->d = calloc(1, sizeof(*snst->d)); + } + if(snst->calibrate) { sweep = snst->nominal_sr; zoom_factor = PAPERSTRIP_ZOOM_CAL; @@ -628,7 +636,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_source(c,stopped?yellow:white); for(i = snst->events_wp;;) { if(!snst->events_count || !snst->events[i]) break; - double event = now - snst->events[i] + snst->trace_centering + sweep * PAPERSTRIP_MARGIN / (2 * zoom_factor); + double event = now - snst->events[i] + snst->d->trace_centering + sweep * PAPERSTRIP_MARGIN / (2 * zoom_factor); int column = floor(fmod(event, (sweep / zoom_factor)) * strip_width / (sweep / zoom_factor)); int row = floor(event / sweep); if(row >= height) break; @@ -742,7 +750,7 @@ static void handle_center_trace(GtkButton *b, struct output_panel *op) new_centering = fmod(last_ev + .5*sweep , sweep); } else new_centering = 0; - snst->trace_centering = new_centering; + snst->d->trace_centering = new_centering; gtk_widget_queue_draw(op->paperstrip_drawing_area); } @@ -754,7 +762,7 @@ static void shift_trace(struct output_panel *op, double direction) sweep = (double) snst->nominal_sr / PAPERSTRIP_ZOOM_CAL; else sweep = snst->sample_rate * 3600. / (PAPERSTRIP_ZOOM * snst->guessed_bph); - snst->trace_centering = fmod(snst->trace_centering + sweep * (1.+.1*direction), sweep); + snst->d->trace_centering = fmod(snst->d->trace_centering + sweep * (1.+.1*direction), sweep); gtk_widget_queue_draw(op->paperstrip_drawing_area); } diff --git a/src/serializer.c b/src/serializer.c index e772f66..f7eee8f 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -347,7 +347,7 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,rate); SERIALIZE(double,be); SERIALIZE(double,amp); - SERIALIZE(double,trace_centering); + SERIALIZE(double,d->trace_centering); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -370,10 +370,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) if(strcmp("realtime-snapshot", l)) return eat_object(f); - *s = malloc(sizeof(struct snapshot)); - memset(*s, 0, sizeof(struct snapshot)); - (*s)->pb = malloc(sizeof(struct processing_buffers)); - memset((*s)->pb, 0, sizeof(struct processing_buffers)); + *s = calloc(1, sizeof(**s)); + (*s)->pb = calloc(1, sizeof(*(*s)->pb)); + (*s)->d = calloc(1, sizeof(*(*s)->d)); *name = NULL; n = 0; @@ -423,7 +422,7 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,rate); SCAN(double,be); SCAN(double,amp); - SCAN(double,trace_centering); + SCAN(double,d->trace_centering); SCAN(int,is_light); if(eat_object(f)) goto error; diff --git a/src/tg.h b/src/tg.h index f92d640..cd94301 100644 --- a/src/tg.h +++ b/src/tg.h @@ -133,6 +133,8 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) void set_audio_light(bool light); /* computer.c */ +struct display; + struct snapshot { struct processing_buffers *pb; int is_old; @@ -163,7 +165,8 @@ struct snapshot { double be; double amp; - double trace_centering; + // State related to displaying the snapshot, not generated by computer + struct display *d; }; struct computer { @@ -196,6 +199,11 @@ void unlock_computer(struct computer *c); void compute_results(struct snapshot *s); /* output_panel.c */ +/* Snapshot display parameters, e.g. scale, centering. */ +struct display { + double trace_centering; +}; + struct output_panel { GtkWidget *panel; From 5e3212e59dac4f78703e9a05233309057d3220f6 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 16:49:28 -0700 Subject: [PATCH 08/17] Size fonts based on drawing area Since the space allocation between the paperstrip and waveforms can be changed now, using just the main window size to control font size doesn't work as well. E.g., a small waveform in a large app window that is mostly paperstrip will still get a large font, because the app window is large. Also adjust font spacing some. Use the vertical size of the font to adjust legend in paperstrip and provide consistent margins in waveforms. --- src/output_panel.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 299cce3..4d32ccd 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -319,16 +319,21 @@ static void expose_waveform( GtkAllocation temp; gtk_widget_get_allocation(da, &temp); - int width = temp.width; - int height = temp.height; + const int width = temp.width, height = temp.height; - int font = width / 25; - font = font < 12 ? 12 : font > 24 ? 24 : font; + int fontw = width / 40; + fontw = fontw < 12 ? 12 : fontw > 20 ? 20 : fontw; + int fonth = height / 12; + fonth = fonth < 12 ? 12 : fonth > 20 ? 20 : fonth; + const int font = MIN(fontw, fonth); + cairo_set_font_size(c, font); - int i; + cairo_font_extents_t fextents; + cairo_font_extents(c, &fextents); - cairo_set_font_size(c,font); + const int margin = 6; + int i; for(i = 1-NEGATIVE_SPAN; i < POSITIVE_SPAN; i++) { int x = (NEGATIVE_SPAN + i) * width / (POSITIVE_SPAN + NEGATIVE_SPAN); cairo_move_to(c, x + .5, height / 2 + .5); @@ -345,7 +350,7 @@ static void expose_waveform( int x = (NEGATIVE_SPAN + i) * width / (POSITIVE_SPAN + NEGATIVE_SPAN); char s[10]; sprintf(s,"%d",i); - cairo_move_to(c,x+font/4,height-font/2); + cairo_move_to(c,x+font/4, height - margin); cairo_show_text(c,s); } } @@ -353,7 +358,7 @@ static void expose_waveform( cairo_text_extents_t extents; cairo_text_extents(c,"ms",&extents); - cairo_move_to(c,width - extents.x_advance - font/4,height-font/2); + cairo_move_to(c,width - extents.x_advance - font/4, height - margin); cairo_show_text(c,"ms"); struct snapshot *snst = op->snst; @@ -385,7 +390,7 @@ static void expose_waveform( char s[10]; sprintf(s,"%d",abs(i)); - cairo_move_to(c, x + font/4, font * 3 / 2); + cairo_move_to(c, x + font/4, margin + fextents.ascent); cairo_show_text(c,s); cairo_text_extents(c,s,&extents); last_x = x + font/4 + extents.x_advance; @@ -393,7 +398,7 @@ static void expose_waveform( } cairo_text_extents(c,"deg",&extents); - cairo_move_to(c,width - extents.x_advance - font/4,font * 3 / 2); + cairo_move_to(c,width - extents.x_advance - font/4, margin + fextents.ascent); cairo_show_text(c,"deg"); if(p) { @@ -685,8 +690,10 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp 1000. / zoom_factor : 3600000. / (snst->guessed_bph * zoom_factor)); cairo_text_extents_t extents; - cairo_text_extents(c,s,&extents); - cairo_move_to(c, (width - extents.x_advance)/2, height - 30); + cairo_font_extents_t fextents; + cairo_text_extents(c, s, &extents); + cairo_font_extents(c, &fextents); + cairo_move_to(c, (width - extents.width)/2 - extents.x_bearing, (height - 25.5) + fextents.ascent - fextents.height); cairo_show_text(c,s); return FALSE; From 5fc7d6733fb4f205e2d93415a4311e3866a86fa3 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 16:53:51 -0700 Subject: [PATCH 09/17] Use widget parameter instead of state in paperstrip expose Doesn't depend on state having the widget saved or what name is used this way. Also avoids unused parameter warning. --- src/output_panel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output_panel.c b/src/output_panel.c index 4d32ccd..8058512 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -560,7 +560,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_init(c); GtkAllocation temp; - gtk_widget_get_allocation (op->paperstrip_drawing_area, &temp); + gtk_widget_get_allocation(widget, &temp); int width, height; /* The paperstrip is coded to be vertical; horizontal uses cairo to rotate it. */ From 7b81287b1e72e33650756caa6f93b453e8fe69c2 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 17:29:19 -0700 Subject: [PATCH 10/17] Add zoom slider for paperstrip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a pair of buttons that overlay the paperstrip. One is a Gtk slider button which allows zooming in and out with a slider. The other will reset to the default zoom. This zoom is just zoom the chart in the horizontal direction, i.e. error in each beat. Not vertically, which would show more or fewer beats at once. The zoom ranges from the 1.0 to 0.01. E.g., for a 10 beat/sec movement, the chart's width will be at most 100 ms, an entire beat, to 10 ms, the default, down to 1 ms. The forced switch from 0.1 in normal mode to 0.01 in calibration mode is removed. If one wants to zoom in for calibration, then one is free to do that. Due to the way the paperstrip is drawn, only zoom values that are exact integer fractions will work. E.g., 0.5 = 1/2, 0.2 = 1/5, and 0.10 = 1/10, will all work. But 0.12 = 1/8.333 will not, because 8⅓ is not an integer. This makes the initial zoom jumps quite large. --- src/output_panel.c | 89 ++++++++++++++++++++++++++++++++++++++++++---- src/serializer.c | 5 +++ src/tg.h | 3 ++ 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 8058512..b808b9c 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -18,6 +18,13 @@ #include "tg.h" +// Zoom slider ranges from 1 to 100 +static const double zoom_min = 1, zoom_max = 100, zoom_mid = (zoom_min + zoom_max)/2; +// Scale ranges from 1x beat length to zoomed in by 100x +static const double scale_min = 1, scale_max = 100; + +static inline double spb(const struct snapshot *snst); + cairo_pattern_t *black,*white,*red,*green,*blue,*blueish,*yellow; static void define_color(cairo_pattern_t **gc,double r,double g,double b) @@ -530,6 +537,27 @@ static gboolean period_draw_event(GtkWidget *widget, cairo_t *c, struct output_p return FALSE; } +/* Return scale value from button. A scale of 1.0 means the beat length, while + * a scale of 0.10 would be one tenth of a beat length. */ +static double get_beatscale(GtkScaleButton *b) +{ + const double σ = log(scale_max/scale_min) / (zoom_max - zoom_min); + const double ω = pow(scale_max/scale_min, -1.0/(zoom_max - zoom_min)) / scale_max; + + const double zoom = gtk_scale_button_get_value(b); + const double scale = ω * exp(σ*zoom); + debug("Zoom slider %.0f to scale %.03f = %.0fx\n", zoom, scale, 1/scale); + + // Zoom must be integral fraction for display to work + return 1.0 / round(1.0 / scale); +} + +// Convert value in samples to milliseconds. +static inline double s2ms(const struct snapshot *snst, double samples) +{ + return samples * 1000.0 / snst->sample_rate; +} + static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct output_panel *op) { int i; @@ -537,25 +565,27 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp struct display *ssd = snst->d; uint64_t time = snst->timestamp ? snst->timestamp : get_timestamp(snst->is_light); double sweep; - int zoom_factor; + double zoom_factor; double slope = 1000; // detected rate: 1000 -> do not display // Allocate initial display parameters. Will persist across each new // snapshot after this. if (!ssd) { ssd = op->snst->d = calloc(1, sizeof(*snst->d)); + ssd->beat_scale = get_beatscale(GTK_SCALE_BUTTON(op->zoom_button)); } + zoom_factor = 1/ssd->beat_scale; if(snst->calibrate) { sweep = snst->nominal_sr; - zoom_factor = PAPERSTRIP_ZOOM_CAL; slope = (double) snst->cal * zoom_factor / (10 * 3600 * 24); } else { sweep = snst->sample_rate * 3600. / snst->guessed_bph; - zoom_factor = PAPERSTRIP_ZOOM; if(snst->events_count && snst->events[snst->events_wp]) slope = - snst->rate * zoom_factor / (3600. * 24.); } + // Width of chart's displayed portion of the beat, in samples + const double chart_width = sweep * ssd->beat_scale; cairo_init(c); @@ -585,6 +615,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // Beat error slope lines or calibration slope lines cairo_set_line_width(c,1.3); slope *= strip_width; @@ -616,6 +647,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_line_width(c,1); + // Margin lines int left_margin = (width - strip_width) / 2; int right_margin = (width + strip_width) / 2; cairo_move_to(c, left_margin + .5, .5); @@ -625,6 +657,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_source(c, green); cairo_stroke(c); + // Time grid lines double now = sweep*ceil(time/sweep); double ten_s = snst->sample_rate * 10 / sweep; double last_line = fmod(now/sweep, ten_s); @@ -638,6 +671,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_stroke(c); } + // Ticks and tocks dots cairo_set_source(c,stopped?yellow:white); for(i = snst->events_wp;;) { if(!snst->events_count || !snst->events[i]) break; @@ -665,6 +699,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp if(i == snst->events_wp) break; } + // Legend line cairo_set_source(c,white); cairo_set_line_width(c,2); cairo_move_to(c, left_margin + 3, height - 20.5); @@ -686,9 +721,8 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_font_size(c, font < 12 ? 12 : font > 24 ? 24 : font); char s[32]; - snprintf(s, sizeof(s), "%.1f ms", snst->calibrate ? - 1000. / zoom_factor : - 3600000. / (snst->guessed_bph * zoom_factor)); + snprintf(s, sizeof(s), "%.1f ms", s2ms(snst, chart_width)); + cairo_text_extents_t extents; cairo_font_extents_t fextents; cairo_text_extents(c, s, &extents); @@ -785,6 +819,25 @@ static void handle_right(GtkButton *b, struct output_panel *op) shift_trace(op,1); } +static void handle_zoom_original(GtkScaleButton *b, struct output_panel *op) +{ + UNUSED(b); + gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); + gtk_widget_queue_draw(op->paperstrip_drawing_area); +} + +static void handle_zoom(GtkScaleButton *b, struct output_panel *op) +{ + struct display *ssd = op->snst->d; + if (!ssd) return; // Maybe chart hasn't been displayed even once yet? + + const double scale = get_beatscale(b); + + ssd->beat_scale = scale; + + gtk_widget_queue_draw(op->paperstrip_drawing_area); +} + void op_set_snapshot(struct output_panel *op, struct snapshot *snst) { op->snst = snst; @@ -808,13 +861,35 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) { GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + GtkWidget *overlay = gtk_overlay_new(); + gtk_box_pack_start(GTK_BOX(vbox), overlay, TRUE, TRUE, 0); + // Paperstrip op->paperstrip_drawing_area = gtk_drawing_area_new(); gtk_widget_set_size_request(op->paperstrip_drawing_area, 150, 150); - gtk_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(overlay), op->paperstrip_drawing_area); g_signal_connect (op->paperstrip_drawing_area, "draw", G_CALLBACK(paperstrip_draw_event), op); gtk_widget_set_events(op->paperstrip_drawing_area, GDK_EXPOSURE_MASK); + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_set_border_width(GTK_CONTAINER(box), 15); + gtk_widget_set_margin_bottom(box, 5); + gtk_widget_set_halign(box, GTK_ALIGN_START); + gtk_widget_set_valign(box, GTK_ALIGN_END); + gtk_widget_set_opacity(box, 0.8); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); + + GtkWidget *zoom_orig = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(zoom_orig), GTK_RELIEF_NONE); + g_signal_connect(zoom_orig, "clicked", G_CALLBACK(handle_zoom_original), op); + gtk_box_pack_start(GTK_BOX(box), zoom_orig, FALSE, FALSE, 0); + + op->zoom_button = gtk_scale_button_new(GTK_ICON_SIZE_BUTTON, zoom_min, zoom_max, 1, + (const char *[]){"zoom-in-symbolic", NULL}); + gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); + g_signal_connect(op->zoom_button, "value-changed", G_CALLBACK(handle_zoom), op); + gtk_box_pack_start(GTK_BOX(box), op->zoom_button, FALSE, FALSE, 0); + // Buttons GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); diff --git a/src/serializer.c b/src/serializer.c index f7eee8f..485afc4 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -348,6 +348,7 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,be); SERIALIZE(double,amp); SERIALIZE(double,d->trace_centering); + SERIALIZE(double,d->beat_scale); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -423,6 +424,7 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,be); SCAN(double,amp); SCAN(double,d->trace_centering); + SCAN(double,d->beat_scale); SCAN(int,is_light); if(eat_object(f)) goto error; @@ -452,6 +454,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) if((*s)->be < 0 || (*s)->be > 99.9) goto error; debug("serializer: checking amplitude\n"); if((*s)->amp < 0 || (*s)->amp > 360) goto error; + debug("serializer: checking scale\n"); + if((*s)->d->beat_scale == 0) (*s)->d->beat_scale = 1.0/PAPERSTRIP_ZOOM; + if((*s)->d->beat_scale < 0 || (*s)->d->beat_scale > 1) goto error; (*s)->pb->events = NULL; #ifdef DEBUG (*s)->pb->debug = NULL; diff --git a/src/tg.h b/src/tg.h index cd94301..f35c277 100644 --- a/src/tg.h +++ b/src/tg.h @@ -202,6 +202,9 @@ void compute_results(struct snapshot *s); /* Snapshot display parameters, e.g. scale, centering. */ struct display { double trace_centering; + // Scaling factor for each beat. 1 means the chart is 1 beat wide, 0.5 + // means half a beat, etc. + double beat_scale; }; struct output_panel { From 0ab8b06d0d1d7de7e39e71a5ecc4ba119d198d8f Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 17:51:26 -0700 Subject: [PATCH 11/17] Redo paperstrip display, new dot algorithm Re-write most of the code for generating the paperstrip. A new algorithm is used to place the dots which has an advantage in that it can display correctly with any level of zoom, not just at integer scales. What we do is find each event's relative position from the one before it. A relative offset of 0 would mean the event is exactly a multiple of the beat time from the previous event. A positive or negative value means it is earlier or later than expected. We can then scale this offset by any value. Since only the relative position between events is used there is no longer a problem when the accumulated offset wraps around a beat length but not the chart width. With all event positions relative, we need some absolute position from which to start. This is done by saving the absolute offset of the most recent event as an anchor. The next display will place the previous anchor at the same absolute offset and update the anchor to the new most recent event. This way the dots do not shift side-to-side as they scroll. Having done this, much of the other code relating to the paperstrip becomes simpler and there is a net reduction in lines of code. The dots are placed in nearly the same positions as before, with one significant difference. The slope is reversed, i.e. a movement running fast will slope up and to the left. This works better for the math, and appears to match what other timegraphers do. --- src/output_panel.c | 268 ++++++++++++++++++++++++++------------------- src/serializer.c | 6 +- src/tg.h | 7 +- 3 files changed, 166 insertions(+), 115 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index b808b9c..24c3480 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -548,8 +548,7 @@ static double get_beatscale(GtkScaleButton *b) const double scale = ω * exp(σ*zoom); debug("Zoom slider %.0f to scale %.03f = %.0fx\n", zoom, scale, 1/scale); - // Zoom must be integral fraction for display to work - return 1.0 / round(1.0 / scale); + return scale; } // Convert value in samples to milliseconds. @@ -558,15 +557,40 @@ static inline double s2ms(const struct snapshot *snst, double samples) return samples * 1000.0 / snst->sample_rate; } +// Samples per beat +static inline double spb(const struct snapshot *snst) +{ + if (snst->calibrate) + return snst->nominal_sr; // one second per beat, no calibration applied + + return (snst->sample_rate * 3600) / snst->guessed_bph; +} + +// 1x1 box with upper left corner at x, y +static void box(cairo_t *c, double x, double y) +{ + cairo_move_to(c, x, y); + cairo_line_to(c, x+1, y); + cairo_line_to(c, x+1, y+1); + cairo_line_to(c, x, y+1); + cairo_close_path(c); + cairo_fill(c); +} + static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct output_panel *op) { int i; const struct snapshot *snst = op->snst; struct display *ssd = snst->d; uint64_t time = snst->timestamp ? snst->timestamp : get_timestamp(snst->is_light); - double sweep; - double zoom_factor; - double slope = 1000; // detected rate: 1000 -> do not display + + bool stopped = false; + if( snst->events_count && + snst->events[snst->events_wp] && + time > 5 * snst->nominal_sr + snst->events[snst->events_wp]) { + time = 5 * snst->nominal_sr + snst->events[snst->events_wp]; + stopped = true; + } // Allocate initial display parameters. Will persist across each new // snapshot after this. @@ -575,18 +599,6 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp ssd->beat_scale = get_beatscale(GTK_SCALE_BUTTON(op->zoom_button)); } - zoom_factor = 1/ssd->beat_scale; - if(snst->calibrate) { - sweep = snst->nominal_sr; - slope = (double) snst->cal * zoom_factor / (10 * 3600 * 24); - } else { - sweep = snst->sample_rate * 3600. / snst->guessed_bph; - if(snst->events_count && snst->events[snst->events_wp]) - slope = - snst->rate * zoom_factor / (3600. * 24.); - } - // Width of chart's displayed portion of the beat, in samples - const double chart_width = sweep * ssd->beat_scale; - cairo_init(c); GtkAllocation temp; @@ -605,107 +617,143 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_rotate(c, M_PI/2); } - int stopped = 0; - if( snst->events_count && - snst->events[snst->events_wp] && - time > 5 * snst->nominal_sr + snst->events[snst->events_wp]) { - time = 5 * snst->nominal_sr + snst->events[snst->events_wp]; - stopped = 1; - } - - int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // Beat time in samples, which is the height of a row (measured in samples) + const double beat_length = spb(snst); + // Width of chart's displayed portion of the beat, in samples + const double chart_width = beat_length * ssd->beat_scale; + // Width in pixels of main chart area, which corresponds to chart_width in samples + const int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // Width in samples of one pixel + const double pixel_width = (double)chart_width / strip_width; + + /* Round time to multiple of beat rate, to avoid "jumping" of a point + * compared to others or grid lines while scrolling. E.g., points at + * 1.0 and 1.2, both are rounded to row 1. Advance time by 0.4, points + * now at 1.4 and 1.6, the first remains row 1, but the second is + * rounded to row 2, causing it to appear to jump. This is avoided by + * only advancing time by a multiple of a row. */ + time -= time % (int)(beat_length + 0.5); // Beat error slope lines or calibration slope lines - cairo_set_line_width(c,1.3); - - slope *= strip_width; - if(slope <= 2 && slope >= -2) { - for(i=0; i<4; i++) { - double y = 0; - cairo_move_to(c, (double)width * (i+.5) / 4, 0); - for(;;) { - double x = y * slope + (double)width * (i+.5) / 4; - x = fmod(x, width); - if(x < 0) x += width; - double nx = x + slope * (height - y); - if(nx >= 0 && nx <= width) { - cairo_line_to(c, nx, height); - break; - } else { - double d = slope > 0 ? width - x : x; - y += d / fabs(slope); - cairo_line_to(c, slope > 0 ? width : 0, y); - y += 1; - if(y > height) break; - cairo_move_to(c, slope > 0 ? 0 : width, y); - } - } - } + // Slope of rate lines, in pixels per beat + const double slope = (snst->calibrate ? -snst->cal/10.0 : snst->rate) * + strip_width / (24 * 3600) / ssd->beat_scale; + if (slope > -2 && slope < 2) { + cairo_set_line_width(c, 1.3); cairo_set_source(c, blue); + /* X intercept of line starting at lower left corner, in quarter widths left+4 + * is intercept from lower right corner. Intercepts at top left/right corners + * are always 0 and 4. We need to draw lines from the lesser of the left corner + * intercepts to the greater of the right corner intercepts to cover the width + * of the chart at the top and botom. */ + const int left = ceil(-slope * height / width * 4 - 0.5); + for (i = MIN(left, 0); i < MAX(4, left+4); i++) { + // i is x position in quarter chart widths + const double x0 = (i + 0.5) / 4 * width; + cairo_move_to(c, x0, 0); + cairo_line_to(c, x0 + slope * height, height); + } cairo_stroke(c); } - cairo_set_line_width(c,1); - // Margin lines - int left_margin = (width - strip_width) / 2; - int right_margin = (width + strip_width) / 2; + const int left_margin = (width - strip_width) / 2; + const int right_margin = (width + strip_width) / 2; + + cairo_set_line_width(c, 1); + cairo_set_source(c, green); cairo_move_to(c, left_margin + .5, .5); cairo_line_to(c, left_margin + .5, height - .5); cairo_move_to(c, right_margin + .5, .5); cairo_line_to(c, right_margin + .5, height - .5); - cairo_set_source(c, green); cairo_stroke(c); // Time grid lines - double now = sweep*ceil(time/sweep); - double ten_s = snst->sample_rate * 10 / sweep; - double last_line = fmod(now/sweep, ten_s); - int last_tenth = floor(now/(sweep*ten_s)); - for(i=0;;i++) { - double y = 0.5 + round(last_line + i*ten_s); - if(y > height) break; - cairo_move_to(c, .5, y); - cairo_line_to(c, width-.5, y); - cairo_set_source(c, (last_tenth-i)%6 ? green : red); + cairo_set_line_width(c, 1); + // Space between lines in samples = 10 sec + const double line_spacing = 10 * snst->sample_rate; + // The topmost line is this many samples from start + const double top_line = fmod(time, line_spacing); + const int minute_offset = (int)(time / line_spacing) % 6; + for(i = 0; ; i++) { + const double position = top_line + i * line_spacing; // position in samples + const double row = round(position / beat_length); // …in pixels + if (row > height) + break; + cairo_move_to(c, 0, row); + cairo_line_to(c, width, row); + cairo_set_source(c, (i - minute_offset) % 6 ? green : red); cairo_stroke(c); } - // Ticks and tocks dots - cairo_set_source(c,stopped?yellow:white); - for(i = snst->events_wp;;) { - if(!snst->events_count || !snst->events[i]) break; - double event = now - snst->events[i] + snst->d->trace_centering + sweep * PAPERSTRIP_MARGIN / (2 * zoom_factor); - int column = floor(fmod(event, (sweep / zoom_factor)) * strip_width / (sweep / zoom_factor)); - int row = floor(event / sweep); - if(row >= height) break; - cairo_move_to(c,column,row); - cairo_line_to(c,column+1,row); - cairo_line_to(c,column+1,row+1); - cairo_line_to(c,column,row+1); - cairo_line_to(c,column,row); - cairo_fill(c); - if(column < width - strip_width && row > 0) { - column += strip_width; - row -= 1; - cairo_move_to(c,column,row); - cairo_line_to(c,column+1,row); - cairo_line_to(c,column+1,row+1); - cairo_line_to(c,column,row+1); - cairo_line_to(c,column,row); - cairo_fill(c); + // Ticks and tocks + cairo_set_line_width(c, 0); + cairo_set_source(c, stopped ? yellow : white); + /* Compute lag 1 difference between events, find residuals modulo beat + * length (BL) of those differences, convert to range (-BL/2, BL/2], and + * accumulate. + * While doing this, look for the previous anchor point, for which we + * have saved the value of its accumulated residuals, and find the + * offset needed to produce the same value. */ + + double display_offset = chart_width/2; // Value used if no anchor found + double offsets[snst->events_count]; + if (snst->events_count) { + double accumulated_offset = 0.0; + uint64_t prev_event = snst->events[snst->events_wp]; // Start with first event + for (i = snst->events_count; i > 0; i--) { // Scan order is newest to oldest + const int idx = (snst->events_wp + i) % snst->events_count; + const uint64_t event = snst->events[idx]; + if (!event) break; + + double residual = fmod(prev_event - event, beat_length); + if (residual > beat_length/2) residual -= beat_length; + accumulated_offset -= residual; + offsets[idx] = accumulated_offset; + + // Is this the anchor? + if (event == snst->d->anchor_time) + display_offset = ssd->anchor_offset - accumulated_offset; + + prev_event = event; } - if(--i < 0) i = snst->events_count - 1; - if(i == snst->events_wp) break; + // Save offset of newest point as new anchor + ssd->anchor_time = snst->events[snst->events_wp]; + ssd->anchor_offset = offsets[snst->events_wp] + display_offset; + } + + display_offset += left_margin * pixel_width; // Adjust for margin + for (i = snst->events_count; i > 0; i--) { + const int idx = (snst->events_wp + i) % snst->events_count; + const uint64_t event = snst->events[idx]; + if (!event) break; + + // Row 0 is at "time", each row is one beat earlier than that. + const double row = round((time - event) / beat_length); + if(row > height) break; + + double chart_phase = fmod(offsets[idx] + display_offset, chart_width); + if (chart_phase < 0) chart_phase += chart_width; + const double column = round(chart_phase / pixel_width); + + box(c, column, row); + if (column < width - strip_width) + box(c, column + strip_width, row); +#if DEBUG + const double cycles = (time - event) / beat_length; + debug("point %2d: %7lu, cycle %.1f, offset %.1f ms, chart phase = %.1f ms, column %.0f\n", idx, event, cycles, + s2ms(snst, offsets[idx] + display_offset), s2ms(snst, chart_phase), column); +#endif } + cairo_stroke(c); // Legend line - cairo_set_source(c,white); - cairo_set_line_width(c,2); + cairo_set_source(c, white); + cairo_set_line_width(c, 2); cairo_move_to(c, left_margin + 3, height - 20.5); cairo_line_to(c, right_margin - 3, height - 20.5); cairo_stroke(c); - cairo_set_line_width(c,1); + cairo_set_line_width(c, 1); cairo_move_to(c, left_margin + .5, height - 20.5); cairo_line_to(c, left_margin + 5.5, height - 15.5); cairo_line_to(c, left_margin + 5.5, height - 25.5); @@ -780,30 +828,22 @@ static void handle_center_trace(GtkButton *b, struct output_panel *op) struct snapshot *snst = op->snst; if(!snst || !snst->events) return; - uint64_t last_ev = snst->events[snst->events_wp]; - double new_centering; - if(last_ev) { - double sweep; - if(snst->calibrate) - sweep = (double) snst->nominal_sr / PAPERSTRIP_ZOOM_CAL; - else - sweep = snst->sample_rate * 3600. / (PAPERSTRIP_ZOOM * snst->guessed_bph); - new_centering = fmod(last_ev + .5*sweep , sweep); - } else - new_centering = 0; - snst->d->trace_centering = new_centering; + + const double chart_width = snst->d->beat_scale * spb(snst); + // Anchor point should be most recent point, or close enough to it + snst->d->anchor_offset = chart_width / 2; + gtk_widget_queue_draw(op->paperstrip_drawing_area); } static void shift_trace(struct output_panel *op, double direction) { struct snapshot *snst = op->snst; - double sweep; - if(snst->calibrate) - sweep = (double) snst->nominal_sr / PAPERSTRIP_ZOOM_CAL; - else - sweep = snst->sample_rate * 3600. / (PAPERSTRIP_ZOOM * snst->guessed_bph); - snst->d->trace_centering = fmod(snst->d->trace_centering + sweep * (1.+.1*direction), sweep); + + // Chart with in samples + const double chart_width = snst->d->beat_scale * spb(snst); + snst->d->anchor_offset += chart_width * 0.10 * direction; + gtk_widget_queue_draw(op->paperstrip_drawing_area); } @@ -833,6 +873,10 @@ static void handle_zoom(GtkScaleButton *b, struct output_panel *op) const double scale = get_beatscale(b); + /* Attempt to position archor_offset at same point in chart in new scale */ + if (ssd->beat_scale) + ssd->anchor_offset *= scale / ssd->beat_scale; + ssd->beat_scale = scale; gtk_widget_queue_draw(op->paperstrip_drawing_area); diff --git a/src/serializer.c b/src/serializer.c index 485afc4..f74141e 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -347,8 +347,9 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,rate); SERIALIZE(double,be); SERIALIZE(double,amp); - SERIALIZE(double,d->trace_centering); SERIALIZE(double,d->beat_scale); + SERIALIZE(uint64_t,d->anchor_time); + SERIALIZE(double,d->anchor_offset); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -423,8 +424,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,rate); SCAN(double,be); SCAN(double,amp); - SCAN(double,d->trace_centering); SCAN(double,d->beat_scale); + SCAN(uint64_t,d->anchor_time); + SCAN(double,d->anchor_offset); SCAN(int,is_light); if(eat_object(f)) goto error; diff --git a/src/tg.h b/src/tg.h index f35c277..396da46 100644 --- a/src/tg.h +++ b/src/tg.h @@ -201,10 +201,15 @@ void compute_results(struct snapshot *s); /* output_panel.c */ /* Snapshot display parameters, e.g. scale, centering. */ struct display { - double trace_centering; // Scaling factor for each beat. 1 means the chart is 1 beat wide, 0.5 // means half a beat, etc. double beat_scale; + /* Time of point used to anchor the paperstrip. Each paperstrip point's position is + * relative to the previous point. This point is the one with an absolute position that + * is kept the same, so that all the dots do not shift side to side as the scroll. */ + uint64_t anchor_time; + // Phase offset of point at anchor_time + double anchor_offset; }; struct output_panel { From cd45a9f478bbf8a21671db46384c5dcb45cbe047 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 15:42:01 -0700 Subject: [PATCH 12/17] Fix missing initialization in snapshot The snapshot display parameters should be initialized to NULL. Really, it would be better to always use calloc() so that every field is automatically initialized to zero/NULL rather than needing to remember to manaully do it for each field. --- src/computer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/computer.c b/src/computer.c index a4f2bda..ea68576 100644 --- a/src/computer.c +++ b/src/computer.c @@ -276,6 +276,7 @@ struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int s->la = la; s->cal = cal; s->is_light = light; + s->d = NULL; struct computer *c = malloc(sizeof(struct computer)); c->cdata = cd; From a161e643ced44f8f7925f79fc3e13ce364f711da Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 20:03:08 -0700 Subject: [PATCH 13/17] Don't draw slope when no rate is known Check to see if the computer has figured out a rate, beat error, amplitude, etc. before drawing the rate slope lines. Otherwise the value of rate is uninitialized. --- src/output_panel.c | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 24c3480..f0befa4 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -635,25 +635,27 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp time -= time % (int)(beat_length + 0.5); // Beat error slope lines or calibration slope lines - // Slope of rate lines, in pixels per beat - const double slope = (snst->calibrate ? -snst->cal/10.0 : snst->rate) * - strip_width / (24 * 3600) / ssd->beat_scale; - if (slope > -2 && slope < 2) { - cairo_set_line_width(c, 1.3); - cairo_set_source(c, blue); - /* X intercept of line starting at lower left corner, in quarter widths left+4 - * is intercept from lower right corner. Intercepts at top left/right corners - * are always 0 and 4. We need to draw lines from the lesser of the left corner - * intercepts to the greater of the right corner intercepts to cover the width - * of the chart at the top and botom. */ - const int left = ceil(-slope * height / width * 4 - 0.5); - for (i = MIN(left, 0); i < MAX(4, left+4); i++) { - // i is x position in quarter chart widths - const double x0 = (i + 0.5) / 4 * width; - cairo_move_to(c, x0, 0); - cairo_line_to(c, x0 + slope * height, height); + if (snst->pb) { // pb == NULL means no rate, beat error, etc. + // Slope of rate lines, in pixels per beat + const double slope = (snst->calibrate ? -snst->cal/10.0 : snst->rate) * + strip_width / (24 * 3600) / ssd->beat_scale; + if (slope > -2 && slope < 2) { + cairo_set_line_width(c, 1.3); + cairo_set_source(c, blue); + /* X intercept of line starting at lower left corner, in quarter widths left+4 + * is intercept from lower right corner. Intercepts at top left/right corners + * are always 0 and 4. We need to draw lines from the lesser of the left corner + * intercepts to the greater of the right corner intercepts to cover the width + * of the chart at the top and botom. */ + const int left = ceil(-slope * height / width * 4 - 0.5); + for (i = MIN(left, 0); i < MAX(4, left+4); i++) { + // i is x position in quarter chart widths + const double x0 = (i + 0.5) / 4 * width; + cairo_move_to(c, x0, 0); + cairo_line_to(c, x0 + slope * height, height); + } + cairo_stroke(c); } - cairo_stroke(c); } // Margin lines From a187e556bf06ce349af996dec7983364e871b591 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 29 Mar 2021 17:28:16 -0700 Subject: [PATCH 14/17] Hide zoom reset button when at original scale When at the default zoom scale, hide the zoom reset button. No need to have it on the screen since it won't do anything. And it tells you that the scale is already at default. I noticed the zoom reset button in Chrome disappeared when at 100% and liked this feature. --- src/output_panel.c | 12 +++++++----- src/tg.h | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index f0befa4..baec60e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -881,6 +881,7 @@ static void handle_zoom(GtkScaleButton *b, struct output_panel *op) ssd->beat_scale = scale; + gtk_widget_set_visible(op->zoom_orig_button, gtk_scale_button_get_value(b) != zoom_mid); gtk_widget_queue_draw(op->paperstrip_drawing_area); } @@ -925,17 +926,18 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) gtk_widget_set_opacity(box, 0.8); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); - GtkWidget *zoom_orig = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); - gtk_button_set_relief(GTK_BUTTON(zoom_orig), GTK_RELIEF_NONE); - g_signal_connect(zoom_orig, "clicked", G_CALLBACK(handle_zoom_original), op); - gtk_box_pack_start(GTK_BOX(box), zoom_orig, FALSE, FALSE, 0); - op->zoom_button = gtk_scale_button_new(GTK_ICON_SIZE_BUTTON, zoom_min, zoom_max, 1, (const char *[]){"zoom-in-symbolic", NULL}); gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); g_signal_connect(op->zoom_button, "value-changed", G_CALLBACK(handle_zoom), op); gtk_box_pack_start(GTK_BOX(box), op->zoom_button, FALSE, FALSE, 0); + op->zoom_orig_button = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(op->zoom_orig_button), GTK_RELIEF_NONE); + g_signal_connect(op->zoom_orig_button, "clicked", G_CALLBACK(handle_zoom_original), op); + gtk_box_pack_start(GTK_BOX(box), op->zoom_orig_button, FALSE, FALSE, 0); + gtk_widget_set_no_show_all(op->zoom_orig_button, true); + // Buttons GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); diff --git a/src/tg.h b/src/tg.h index 396da46..8d03342 100644 --- a/src/tg.h +++ b/src/tg.h @@ -227,6 +227,7 @@ struct output_panel { GtkWidget *left_button; GtkWidget *right_button; GtkWidget *zoom_button; + GtkWidget *zoom_orig_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif From e6cc7a55a2503c6c25e7860853f4bbd7fa165319 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 30 Mar 2021 19:02:19 -0700 Subject: [PATCH 15/17] Round chart step up, rather than down, to beat length Rounding the current time down might cause the newest event to appear to be from the future. That confuses the paper strip math. Round up so that doesn't happen. --- src/output_panel.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/output_panel.c b/src/output_panel.c index baec60e..a65153e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -632,6 +632,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp * now at 1.4 and 1.6, the first remains row 1, but the second is * rounded to row 2, causing it to appear to jump. This is avoided by * only advancing time by a multiple of a row. */ + time += (int)(beat_length + 0.5) - 1; time -= time % (int)(beat_length + 0.5); // Beat error slope lines or calibration slope lines From 21965d1e63ae1b679a6985b3748c42618eb7a9e8 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 31 Mar 2021 13:35:14 -0700 Subject: [PATCH 16/17] Utility functions for gtk orientation Turn a boolean vertical flag into a gtk orientation. Was tired of repeating this long expression. --- src/output_panel.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index a65153e..3646061 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -903,6 +903,17 @@ void op_destroy(struct output_panel *op) free(op); } +/* Wrappers around gtk_orientable_set_orientation() */ +static GtkOrientation vert_to_orient(bool vertical) +{ + return vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL; +} + +static void set_orientation(GtkWidget *widget, bool vertical) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(widget), vert_to_orient(vertical)); +} + /* Creates the paperstrip, with buttons. Returns top level Widget that contains * them. Vertical controls orientation of paper strip. */ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) @@ -975,7 +986,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) * them. Vertical controls how the waves are stacked. */ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) { - GtkWidget *box = gtk_box_new(vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL, 10); + GtkWidget *box = gtk_box_new(vert_to_orient(vertical), 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); @@ -1014,12 +1025,12 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi { op->vertical_layout = vertical; - op->displays = gtk_paned_new(vertical ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL); + op->displays = gtk_paned_new(vert_to_orient(!vertical)); gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, vertical ? FALSE : TRUE, FALSE); - gtk_orientable_set_orientation(GTK_ORIENTABLE(waveforms), vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL); + set_orientation(waveforms, vert_to_orient(vertical)); gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); /* Make paperstrip arrows buttons point correct way */ From fcf239970b7316a71cb2897ab72c289d6d710be4 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 31 Mar 2021 13:37:33 -0700 Subject: [PATCH 17/17] Adjust zoom button for vertical/horizontal layout Rotating the paperstrip just does the graphics, not the GUI widgets, so they weren't working as well in horizontal mode. Add some code to pack and orient them well for either vertical or horizontal layout mode. --- src/output_panel.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 3646061..370bf6d 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -930,17 +930,18 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) g_signal_connect (op->paperstrip_drawing_area, "draw", G_CALLBACK(paperstrip_draw_event), op); gtk_widget_set_events(op->paperstrip_drawing_area, GDK_EXPOSURE_MASK); - GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + GtkWidget *box = gtk_box_new(vert_to_orient(!vertical), 0); gtk_container_set_border_width(GTK_CONTAINER(box), 15); gtk_widget_set_margin_bottom(box, 5); gtk_widget_set_halign(box, GTK_ALIGN_START); - gtk_widget_set_valign(box, GTK_ALIGN_END); + gtk_widget_set_valign(box, vertical ? GTK_ALIGN_END : GTK_ALIGN_START); gtk_widget_set_opacity(box, 0.8); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); op->zoom_button = gtk_scale_button_new(GTK_ICON_SIZE_BUTTON, zoom_min, zoom_max, 1, (const char *[]){"zoom-in-symbolic", NULL}); gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); + set_orientation(op->zoom_button, vertical); g_signal_connect(op->zoom_button, "value-changed", G_CALLBACK(handle_zoom), op); gtk_box_pack_start(GTK_BOX(box), op->zoom_button, FALSE, FALSE, 0); @@ -1040,6 +1041,11 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi GtkWidget *right_arrow = gtk_button_get_image(GTK_BUTTON(op->right_button)); gtk_image_set_from_icon_name(GTK_IMAGE(right_arrow), vertical ? "pan-end-symbolic" : "pan-down-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + /* Orientation of zoom buttons in papestrip */ + GtkWidget *button_box = gtk_widget_get_parent(op->zoom_button); + gtk_widget_set_valign(button_box, vertical ? GTK_ALIGN_END : GTK_ALIGN_START); + set_orientation(button_box, !vertical); + set_orientation(op->zoom_button, vertical); gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); gtk_widget_show(op->displays);