/* mainwin.c - main window * * Copyright 2010 Petteri Hintsanen * * This file is part of abx. * * abx 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. * * abx 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 abx. If not, see . */ #include "gtkui.h" #include "test.h" #include GtkWidget *main_window; /* How often we should update the location scale (in ms). Note that GTK+ can't handle very rapid updates. */ static const int UPDATE_LOCATION_TIMEOUT = 1000; /* Glib event source id for update_location */ static guint timeout_id; static GtkWidget *new_test_menu_item; static GtkWidget *about_menu_item; static GtkWidget *status_bar; static gint play_context; static gint non_play_context; static GtkWidget *first_sample; static GtkWidget *second_sample; static GtkWidget *pause_button; static GtkWidget *test_sample; static GtkWidget *prev_marker; static GtkWidget *next_marker; static GtkObject *adjustment; static GtkWidget *scale; static int is_user_seeking; static GtkWidget *markers; static GtkListStore *marker_list; static GtkWidget *trial_label; static GtkWidget *meta_a_label; static GtkWidget *meta_b_label; static gboolean recurrent_update_location(gpointer data); static void clear_status_bar(void); static GtkWidget *create_metadata_box(void); static GtkWidget *create_test_box(void); static GtkWidget *create_menu_bar(void); static void create_main_window(void); static void create_status_bar(void); static GtkWidget *create_playback_box(void); static GtkWidget *create_marker_box(void); static void destroy_event_handler(GtkWidget *widget, gpointer data); static void marker_activated(GtkTreeView *markers, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data); static void rewind_button_clicked(GtkWidget *widget, gpointer data); static void pause_button_clicked(GtkWidget *widget, gpointer data); static void play_button_clicked(GtkWidget *widget, gpointer data); static void decide_button_clicked(GtkWidget *widget, gpointer data); static gboolean scale_button_pressed_or_released(GtkWidget *widget, GdkEventButton *event, gpointer data); static void add_marker_clicked(GtkWidget *widget, gpointer data); static void remove_marker_clicked(GtkWidget *widget, gpointer data); static void menu_item_clicked(GtkWidget *widget, gpointer data); /* * Show the main window, creating it if necessary. */ void show_main_window(void) { if (!main_window) { create_main_window(); gtk_widget_show_all(main_window); } else { gtk_window_present(GTK_WINDOW(main_window)); } } /* * Update trial and metadata if test is in progress, that is, if * current_trial >= 0. */ void update_main_window(void) { static GString *label = NULL; static GString *meta_a = NULL; static GString *meta_b = NULL; if (!label) label = g_string_new(NULL); if (!meta_a) meta_a = g_string_new(NULL); if (!meta_b) meta_b = g_string_new(NULL); if (current_trial < 0) { g_string_printf(label, "No test in progress."); g_string_printf(meta_a, "A: (none)"); g_string_printf(meta_b, "B: (none)"); g_object_set(adjustment, "upper", 0.0, "value", 0.0, NULL); gtk_list_store_clear(marker_list); } else if (current_trial == 0) { g_string_printf(label, "Test trial 1 of %d", num_test_trials()); g_string_printf(meta_a, "Sample A: '%s', %d Hz, %d bits, %d channels", basename_a, metadata_a.rate, metadata_a.bits, metadata_a.channels); g_string_printf(meta_b, "Sample B: '%s', %d Hz, %d bits, %d channels", basename_b, metadata_b.rate, metadata_b.bits, metadata_b.channels); g_object_set(adjustment, "upper", (gdouble) metadata_a.duration, "value", 0.0, NULL); gtk_list_store_clear(marker_list); } else { g_string_printf(label, "Test trial %d of %d", current_trial + 1, num_test_trials()); } gtk_label_set_text(GTK_LABEL(trial_label), label->str); gtk_label_set_text(GTK_LABEL(meta_a_label), meta_a->str); gtk_label_set_text(GTK_LABEL(meta_b_label), meta_b->str); } /* * Update time slider to the current playback location. Remove * possible previous recurrent update timer, and reinstall a new one. * (This is done mostly for consistent UI behaviour.) */ static void update_location(void) { if (timeout_id) { g_source_remove(timeout_id); timeout_id = 0; } if (recurrent_update_location(NULL)) { timeout_id = g_timeout_add(UPDATE_LOCATION_TIMEOUT, recurrent_update_location, NULL); } } /* * Update current location to the time slider. This function is added * to the Glib main event loop whenever update_location is called. * * Return FALSE if playback has been stopped, so that glib will remove * the function from its event loop. Otherwise, return TRUE. */ static gboolean recurrent_update_location(gpointer data) { if (!is_user_seeking) { Player_state state; if (get_playback_state(&state) != -1) { gtk_range_set_value(GTK_RANGE(scale), state.location); if (state.playback == STOPPED) { /* g_debug("removing update 1"); */ timeout_id = 0; clear_status_bar(); return FALSE; } } else { /* g_debug("removing update 2"); */ timeout_id = 0; return FALSE; } } return TRUE; } /* * Restore "Stopped" to the status bar. */ static void clear_status_bar(void) { /* pop twice to remove possible "Paused" from the status bar */ gtk_statusbar_pop(GTK_STATUSBAR(status_bar), play_context); gtk_statusbar_pop(GTK_STATUSBAR(status_bar), play_context); } /* * Create main window elements and lay them into main_box. */ static void create_main_window(void) { GtkWidget *left_box; GtkWidget *main_hbox; GtkWidget *main_box; GtkWidget *playback_box; GtkWidget *test_box; GtkWidget *marker_box; GtkWidget *meta_box; GtkWidget *menu_bar; main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); playback_box = create_playback_box(); test_box = create_test_box(); marker_box = create_marker_box(); meta_box = create_metadata_box(); menu_bar = create_menu_bar(); create_status_bar(); left_box = gtk_vbox_new(FALSE, 10); gtk_box_pack_start(GTK_BOX(left_box), test_box, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(left_box), meta_box, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(left_box), playback_box, TRUE, FALSE, 0); main_hbox = gtk_hbox_new(FALSE, 10); gtk_container_set_border_width(GTK_CONTAINER(main_hbox), 10); gtk_box_pack_start(GTK_BOX(main_hbox), left_box, FALSE, TRUE, 0); gtk_box_pack_start(GTK_BOX(main_hbox), marker_box, TRUE, TRUE, 0); main_box = gtk_vbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(main_box), menu_bar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(main_box), main_hbox, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(main_box), status_bar, FALSE, FALSE, 0); gtk_window_set_title(GTK_WINDOW(main_window), "ABX Tester"); g_signal_connect(G_OBJECT(main_window), "destroy", G_CALLBACK(destroy_event_handler), NULL); gtk_container_add(GTK_CONTAINER(main_window), main_box); } /* * Return a new stock-like "Play" button. */ static GtkWidget * create_play_button(gchar *label) { GtkWidget *aligned_box = gtk_hbox_new(TRUE, 0); GtkWidget *box = gtk_hbox_new(FALSE, 3); gtk_box_pack_start(GTK_BOX(box), gtk_image_new_from_stock(GTK_STOCK_MEDIA_PLAY, GTK_ICON_SIZE_BUTTON), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(box), gtk_label_new_with_mnemonic(label), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(aligned_box), box, TRUE, FALSE, 0); return aligned_box; } /* * Create playback-related widgets (time scale and control buttons), * and lay them into a vbox. Return the box. */ static GtkWidget * create_playback_box(void) { GtkWidget *playback_box; GtkWidget *button_box; GtkWidget *first_row; GtkWidget *second_row; adjustment = gtk_adjustment_new(0, 0, 60, 1.0, 15.0, 0); scale = gtk_hscale_new(GTK_ADJUSTMENT(adjustment)); gtk_range_set_update_policy(GTK_RANGE(scale), GTK_UPDATE_DISCONTINUOUS); is_user_seeking = 0; g_signal_connect(G_OBJECT(scale), "button_press_event", G_CALLBACK(scale_button_pressed_or_released), NULL); g_signal_connect(G_OBJECT(scale), "button_release_event", G_CALLBACK(scale_button_pressed_or_released), NULL); first_sample = gtk_button_new(); gtk_container_add(GTK_CONTAINER(first_sample), create_play_button("Play _A")); g_signal_connect(G_OBJECT(first_sample), "clicked", G_CALLBACK(play_button_clicked), NULL); second_sample = gtk_button_new(); gtk_container_add(GTK_CONTAINER(second_sample), create_play_button("Play _B")); g_signal_connect(G_OBJECT(second_sample), "clicked", G_CALLBACK(play_button_clicked), NULL); test_sample = gtk_button_new(); gtk_container_add(GTK_CONTAINER(test_sample), create_play_button("Play _X")); g_signal_connect(G_OBJECT(test_sample), "clicked", G_CALLBACK(play_button_clicked), NULL); pause_button = gtk_button_new_from_stock(GTK_STOCK_MEDIA_PAUSE); g_signal_connect(G_OBJECT(pause_button), "clicked", G_CALLBACK(pause_button_clicked), NULL); prev_marker = gtk_button_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS); g_signal_connect(G_OBJECT(prev_marker), "clicked", G_CALLBACK(rewind_button_clicked), NULL); next_marker = gtk_button_new_from_stock(GTK_STOCK_MEDIA_NEXT); g_signal_connect(G_OBJECT(next_marker), "clicked", G_CALLBACK(rewind_button_clicked), NULL); button_box = gtk_vbox_new(TRUE, 5); first_row = gtk_hbox_new(TRUE, 5); second_row = gtk_hbox_new(TRUE, 5); gtk_box_pack_start(GTK_BOX(first_row), first_sample, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(first_row), second_sample, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(first_row), test_sample, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(second_row), pause_button, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(second_row), prev_marker, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(second_row), next_marker, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(button_box), first_row, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(button_box), second_row, TRUE, TRUE, 0); playback_box = gtk_vbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(playback_box), scale, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(playback_box), button_box, FALSE, FALSE, 0); return playback_box; } /* * Create marker-related widgets and lay them into a vbox. Return the * box. */ static GtkWidget * create_marker_box(void) { GtkWidget *scrolled_window; GtkWidget *add_marker; GtkWidget *remove_marker; GtkWidget *clear_markers; GtkWidget *upper_buttons_box; GtkWidget *marker_buttons_box; GtkWidget *marker_box; GtkTreeViewColumn *column; marker_list = gtk_list_store_new(1, G_TYPE_DOUBLE); markers = gtk_tree_view_new_with_model(GTK_TREE_MODEL(marker_list)); g_signal_connect(G_OBJECT(markers), "row-activated", G_CALLBACK(marker_activated), NULL); column = (gtk_tree_view_column_new_with_attributes ("_Markers", gtk_cell_renderer_text_new(), "text", 0, NULL)); gtk_tree_view_append_column(GTK_TREE_VIEW(markers), column); add_marker = gtk_button_new_from_stock(GTK_STOCK_ADD); remove_marker = gtk_button_new_from_stock(GTK_STOCK_REMOVE); clear_markers = gtk_button_new_from_stock(GTK_STOCK_CLEAR); g_signal_connect(G_OBJECT(add_marker), "clicked", G_CALLBACK(add_marker_clicked), NULL); g_signal_connect(G_OBJECT(remove_marker), "clicked", G_CALLBACK(remove_marker_clicked), NULL); g_signal_connect_swapped(G_OBJECT(clear_markers), "clicked", G_CALLBACK(gtk_list_store_clear), G_OBJECT(marker_list)); upper_buttons_box = gtk_hbox_new(TRUE, 5); gtk_box_pack_start(GTK_BOX(upper_buttons_box), add_marker, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(upper_buttons_box), remove_marker, TRUE, TRUE, 0); marker_buttons_box = gtk_vbox_new(TRUE, 5); gtk_box_pack_start(GTK_BOX(marker_buttons_box), upper_buttons_box, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(marker_buttons_box), clear_markers, TRUE, TRUE, 0); scrolled_window = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW(scrolled_window), markers); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); marker_box = gtk_vbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(marker_box), GTK_WIDGET(scrolled_window), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(marker_box), marker_buttons_box, FALSE, FALSE, 0); return marker_box; } /* * Create test-related widgets and lay them into test_box. */ static GtkWidget * create_test_box(void) { GtkWidget *test_box = gtk_hbox_new(FALSE, 5); GtkWidget *decide = gtk_button_new_with_mnemonic("_Decide X"); g_signal_connect(G_OBJECT(decide), "clicked", G_CALLBACK(decide_button_clicked), NULL); trial_label = gtk_label_new("(no test in progress)"); gtk_box_pack_start(GTK_BOX(test_box), trial_label, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(test_box), decide, FALSE, FALSE, 0); return test_box; } /* * Create metadata-related widgets and lay them into a vbox. Return * the box. */ static GtkWidget * create_metadata_box(void) { GtkWidget *meta_a_hbox; GtkWidget *meta_b_hbox; GtkWidget *meta_vbox; meta_a_label = gtk_label_new("Sample A: (none)"); meta_b_label = gtk_label_new("Sample B: (none)"); meta_a_hbox = gtk_hbox_new(FALSE, 0); meta_b_hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(meta_a_hbox), meta_a_label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(meta_b_hbox), meta_b_label, FALSE, FALSE, 0); meta_vbox = gtk_vbox_new(TRUE, 5); gtk_box_pack_start(GTK_BOX(meta_vbox), meta_a_hbox, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(meta_vbox), meta_b_hbox, FALSE, FALSE, 0); return meta_vbox; } /* * Create menu bar and items. Return the menu bar. */ static GtkWidget * create_menu_bar(void) { GtkWidget *menu_bar; GtkWidget *test_menu_item; GtkWidget *test_menu; GtkWidget *quit_menu_item; GtkWidget *help_menu; GtkWidget *help_menu_item; test_menu = gtk_menu_new(); new_test_menu_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_NEW, NULL); quit_menu_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); gtk_menu_shell_append(GTK_MENU_SHELL (test_menu), new_test_menu_item); gtk_menu_shell_append(GTK_MENU_SHELL (test_menu), quit_menu_item); g_signal_connect(G_OBJECT (new_test_menu_item), "activate", G_CALLBACK(menu_item_clicked), (gpointer) "test.new"); g_signal_connect_swapped(G_OBJECT(quit_menu_item), "activate", G_CALLBACK(gtk_widget_destroy), G_OBJECT(main_window)); test_menu_item = gtk_menu_item_new_with_mnemonic ("_Test"); gtk_menu_item_set_submenu(GTK_MENU_ITEM (test_menu_item), test_menu); help_menu = gtk_menu_new(); about_menu_item = gtk_image_menu_item_new_from_stock (GTK_STOCK_ABOUT, NULL); gtk_menu_shell_append(GTK_MENU_SHELL (help_menu), about_menu_item); g_signal_connect(G_OBJECT (about_menu_item), "activate", G_CALLBACK (menu_item_clicked), (gpointer) "help.about"); help_menu_item = gtk_menu_item_new_with_mnemonic("_Help"); gtk_menu_item_set_submenu (GTK_MENU_ITEM (help_menu_item), help_menu); menu_bar = gtk_menu_bar_new(); gtk_menu_bar_append(GTK_MENU_BAR(menu_bar), test_menu_item); gtk_menu_bar_append(GTK_MENU_BAR(menu_bar), help_menu_item); return menu_bar; } /* * Create the status bar. */ static void create_status_bar(void) { status_bar = gtk_statusbar_new(); play_context = (gtk_statusbar_get_context_id (GTK_STATUSBAR(status_bar), "Playback status")); non_play_context = (gtk_statusbar_get_context_id (GTK_STATUSBAR(status_bar), "Non-playback status")); gtk_statusbar_push(GTK_STATUSBAR(status_bar), non_play_context, "Stopped"); } /* * Callback functions. */ /* * Quit GTK+ main loop. */ static void destroy_event_handler(GtkWidget *widget, gpointer data) { gtk_main_quit(); } static void marker_activated(GtkTreeView *markers, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data) { GtkTreeIter iter; gdouble marker; if (!gtk_tree_model_get_iter (GTK_TREE_MODEL(marker_list), &iter, path)) { g_warning("can't get iterator for marked item"); return; } gtk_tree_model_get(GTK_TREE_MODEL(marker_list), &iter, 0, &marker, -1); seek_playback(marker); update_location(); } /* * Handle scale button dragging. */ static gboolean scale_button_pressed_or_released(GtkWidget *widget, GdkEventButton *event, gpointer data) { if (event->type == GDK_BUTTON_PRESS) { is_user_seeking = 1; } else if (event->type == GDK_BUTTON_RELEASE) { is_user_seeking = 0; seek_playback(gtk_adjustment_get_value (GTK_ADJUSTMENT(adjustment))); update_location(); } return 0; } /* * Add current location as marker. If playback is in progress, use * playback location. Otherwise, use slider value. */ static void add_marker_clicked(GtkWidget *widget, gpointer data) { GtkTreeIter new; Player_state state; gtk_list_store_append(marker_list, &new); if (get_playback_state(&state) != -1) { gtk_list_store_set(marker_list, &new, 0, state.location, -1); } else { gtk_list_store_set(marker_list, &new, 0, gtk_adjustment_get_value (GTK_ADJUSTMENT(adjustment)), -1); } } /* * Remove the selected marker. */ static void remove_marker_clicked(GtkWidget *widget, gpointer data) { GtkTreeIter iter; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection(GTK_TREE_VIEW(markers)), NULL, &iter)) { gtk_list_store_remove(marker_list, &iter); } } /* * Handle menu item clicks. */ static void menu_item_clicked(GtkWidget *widget, gpointer data) { static char *authors[] = { "Petteri Hintsanen ", NULL }; /* split licence text to two parts to satisfy ISO C standard */ static char license_1[] = "This program is free software: you can redistribute it and/or modify\n" "it under the terms of the GNU General Public License as published by\n" "the Free Software Foundation, either version 3 of the License, or\n" "(at your option) any later version.\n\n"; static char license_2[] = "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n\n" "You should have received a copy of the GNU General Public License\n" "along with this program. If not, see ."; char license[sizeof(license_1) + sizeof(license_2)]; strcpy(license, license_1); strcat(license, license_2); if (widget == new_test_menu_item) { show_new_test_window(); } else if (widget == about_menu_item) { gtk_show_about_dialog (NULL, "program-name", "ABX Tester", "title", "About ABX Tester", "authors", authors, "comments", "Fidelity testing program.", "website", "http://iki.fi/petterih/abx.html", "license", license, NULL); } } /* * Show decide dialog. */ static void decide_button_clicked(GtkWidget *widget, gpointer data) { if (current_trial >= 0) { stop_playback(); clear_status_bar(); show_decide_dialog(GTK_WINDOW(main_window)); } } /* * Start playback and update the status bar. */ static void play_button_clicked(GtkWidget *widget, gpointer data) { GtkTreeIter iter; gint sampleid; gdouble marker; if (current_trial < 0) return; if (widget == first_sample) { sampleid = 0; } else if (widget == second_sample) { sampleid = 1; } else { sampleid = get_answer(current_trial); } /* g_debug("playing sample %d", sampleid); */ /* check for marker selection */ if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection(GTK_TREE_VIEW(markers)), NULL, &iter)) { gtk_tree_model_get(GTK_TREE_MODEL(marker_list), &iter, 0, &marker, -1); /* g_debug("playing sample %d from %f", sampleid, marker); */ start_playback(sampleid, marker); } else { Player_state state; if (get_playback_state(&state) != -1 && state.playback == PLAYING) { start_playback(sampleid, state.location); } else { start_playback(sampleid, gtk_adjustment_get_value (GTK_ADJUSTMENT(adjustment))); } } clear_status_bar(); if (widget == first_sample) { gtk_statusbar_push(GTK_STATUSBAR(status_bar), play_context, "Playing sample A"); } else if (widget == second_sample) { gtk_statusbar_push(GTK_STATUSBAR(status_bar), play_context, "Playing sample B"); } else { gtk_statusbar_push(GTK_STATUSBAR(status_bar), play_context, "Playing sample X"); } update_location(); } /* * Pause or resume the playback. */ static void pause_button_clicked(GtkWidget *widget, gpointer data) { int paused = pause_or_resume_playback(); switch (paused) { case 0: gtk_statusbar_push(GTK_STATUSBAR(status_bar), play_context, "Paused"); break; case 1: gtk_statusbar_pop(GTK_STATUSBAR(status_bar), play_context); break; default: clear_status_bar(); } update_location(); } /* * Seek to the next or previous marker. */ static void rewind_button_clicked(GtkWidget *widget, gpointer data) { GtkTreeSelection *selection; GtkTreeIter iter; GtkTreePath *path; gdouble marker; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(markers)); if (gtk_tree_selection_get_selected(selection, NULL, &iter) == FALSE) { return; } path = gtk_tree_model_get_path(GTK_TREE_MODEL(marker_list), &iter); if (widget == prev_marker && gtk_tree_path_prev(path)) { gtk_tree_selection_select_path(selection, path); } else if (widget == next_marker) { gtk_tree_path_next (path); gtk_tree_selection_select_path(selection, path); } if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection(GTK_TREE_VIEW(markers)), NULL, &iter)) { gtk_tree_model_get(GTK_TREE_MODEL(marker_list), &iter, 0, &marker, -1); seek_playback(marker); update_location(); } gtk_tree_path_free(path); }