/* * sensei-raw-ctl-gui.c: SteelSeries Sensei Raw control utility - GTK+ GUI * * Very tightly coupled with the sensei-raw-ctl utility. * * Copyright (c) 2013, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #include #include #include #include #include #include "config.h" #if ! GLIB_CHECK_VERSION (2, 34, 0) #define g_clear_pointer(p, destroy) \ G_STMT_START if (*(p)) { (destroy) (*(p)); *(p) = NULL; } G_STMT_END #endif // ! GLIB_CHECK_VERSION (2, 34) /** User interface string for GtkBuilder. */ extern const char ui[]; /* To translate combo box entries into sensei-raw-ctl arguments. */ static gchar *pulsation_list[] = { "steady", "slow", "medium", "fast", "trigger", NULL }; static gchar *intensity_list[] = { "off", "low", "medium", "high", NULL }; /* GtkNotebook pages within the UI. */ enum { PAGE_PROBING, PAGE_NO_DEVICE, PAGE_SETTINGS, PAGE_COUNT }; /* sensei-raw-ctl output values. */ enum { OUT_INTENSITY, OUT_PULSATION, OUT_CPI_LED_OFF, OUT_CPI_LED_ON, OUT_POLLING, OUT_COUNT }; // ----- User interface ------------------------------------------------------- static gboolean user_wants_to_retry_critical_op (GtkWidget *parent, const gchar *message) { GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (parent), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_NONE, _("Error")); gtk_dialog_add_buttons (GTK_DIALOG (dialog), _("_Retry"), TRUE, _("_Close"), FALSE, NULL); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", message); gboolean success = gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); if (!success) gtk_main_quit (); return success; } static void set_page (GtkBuilder *builder, gint page) { GtkNotebook *notebook = GTK_NOTEBOOK (gtk_builder_get_object (builder, "notebook")); gtk_notebook_set_current_page (notebook, page); } static gchar * spawn_ctl (gchar **argv, GtkBuilder *builder) { GtkWidget *win = GTK_WIDGET (gtk_builder_get_object (builder, "win")); gboolean trying = TRUE; while (trying) { GError *error = NULL; gchar *out = NULL, *err = NULL; gint status; if (!g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &out, &err, &status, &error)) { trying = user_wants_to_retry_critical_op (win, error->message); g_error_free (error); continue; } if (WIFEXITED (status) && WEXITSTATUS (status) == 0) { g_free (err); return out; } trying = FALSE; if (strstr (err, "no suitable device")) set_page (builder, PAGE_NO_DEVICE); else trying = user_wants_to_retry_critical_op (win, err); g_free (out); g_free (err); } return NULL; } static gboolean set_combo (GtkComboBox *combo, gchar *list[], const gchar *word) { gint i; for (i = 0; list[i]; i++) if (!strcmp (word, list[i])) { gtk_combo_box_set_active (combo, i); return TRUE; } return FALSE; } static gboolean set_scale (GtkRange *scale, const gchar *word, const gchar *follows) { gchar *end; gint64 value = g_ascii_strtoll (word, &end, 10); if (strcmp (end, follows)) return FALSE; gtk_range_set_value (scale, value); return TRUE; } static void load_configuration (GtkBuilder *builder) { gchar *argv[] = { "pkexec", PROJECT_INSTALL_BINDIR "/" PROJECT_NAME, "--show", NULL }; gchar *out; if (!(out = spawn_ctl (argv, builder))) return; GRegex *regex = g_regex_new ("(?<=: ).*$", G_REGEX_MULTILINE, 0, NULL); GMatchInfo *info; g_regex_match (regex, out, 0, &info); gint line = 0; gchar *word = NULL; while (g_match_info_matches (info)) { g_free (word); word = g_match_info_fetch (info, 0); switch (line++) { case OUT_INTENSITY: if (!set_combo (GTK_COMBO_BOX (gtk_builder_get_object (builder, "intensity_combo")), intensity_list, word)) goto out; break; case OUT_PULSATION: if (!set_combo (GTK_COMBO_BOX (gtk_builder_get_object (builder, "pulsation_combo")), pulsation_list, word)) goto out; break; case OUT_CPI_LED_OFF: if (!set_scale (GTK_RANGE (gtk_builder_get_object (builder, "cpi_off_scale")), word, "")) goto out; break; case OUT_CPI_LED_ON: if (!set_scale (GTK_RANGE (gtk_builder_get_object (builder, "cpi_on_scale")), word, "")) goto out; break; case OUT_POLLING: if (!set_scale (GTK_RANGE (gtk_builder_get_object (builder, "polling_scale")), word, "Hz")) goto out; } g_match_info_next (info, NULL); } set_page (builder, PAGE_SETTINGS); out: g_free (word); g_match_info_free (info); g_regex_unref (regex); g_free (out); if (line != OUT_COUNT) { GtkWidget *dialog = gtk_message_dialog_new ( GTK_WINDOW (gtk_builder_get_object (builder, "win")), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Internal error: backend mismatch")); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); } } static void retry_load (GtkBuilder *builder) { set_page (builder, PAGE_PROBING); load_configuration (builder); } static void save_configuration (GtkBuilder *builder) { gchar *polling = g_strdup_printf ("%.0f", gtk_range_get_value (GTK_RANGE (gtk_builder_get_object (builder, "polling_scale")))); gchar *cpi_on = g_strdup_printf ("%.0f", gtk_range_get_value (GTK_RANGE (gtk_builder_get_object (builder, "cpi_on_scale")))); gchar *cpi_off = g_strdup_printf ("%.0f", gtk_range_get_value (GTK_RANGE (gtk_builder_get_object (builder, "cpi_off_scale")))); GtkComboBox *combo; gint active; combo = GTK_COMBO_BOX (gtk_builder_get_object (builder, "pulsation_combo")); active = gtk_combo_box_get_active (combo); g_assert (active >= 0 && active < G_N_ELEMENTS (pulsation_list) - 1); gchar *pulsation = pulsation_list[active]; combo = GTK_COMBO_BOX (gtk_builder_get_object (builder, "intensity_combo")); active = gtk_combo_box_get_active (combo); g_assert (active >= 0 && active < G_N_ELEMENTS (intensity_list) - 1); gchar *intensity = intensity_list[active]; gchar *argv[] = { "pkexec", PROJECT_INSTALL_BINDIR "/" PROJECT_NAME, "--polling", polling, "--cpi-on", cpi_on, "--cpi-off", cpi_off, "--pulsation", pulsation, "--intensity", intensity, NULL }; g_free (spawn_ctl (argv, builder)); g_free (polling); g_free (cpi_on); g_free (cpi_off); } static void on_set_mode_normal (GtkBuilder *builder) { gchar *argv[] = { "pkexec", PROJECT_INSTALL_BINDIR "/" PROJECT_NAME, "--mode", "normal", NULL }; g_free (spawn_ctl (argv, builder)); } static void on_set_mode_legacy (GtkBuilder *builder) { gchar *argv[] = { "pkexec", PROJECT_INSTALL_BINDIR "/" PROJECT_NAME, "--mode", "legacy", NULL }; g_free (spawn_ctl (argv, builder)); } // ----- User interface ------------------------------------------------------- static gboolean on_change_value (GtkRange *range, GtkScrollType scroll, gdouble value, gpointer user_data) { GtkAdjustment *adjustment = gtk_range_get_adjustment (range); static const gint steps[] = { 125, 250, 500, 1000 }; switch (scroll) { gint i; case GTK_SCROLL_NONE: case GTK_SCROLL_JUMP: for (i = 0; i < G_N_ELEMENTS (steps); i++) if (i == G_N_ELEMENTS (steps) - 1 || value < (steps[i] + steps[i + 1]) / 2) { value = steps[i]; break; } break; case GTK_SCROLL_STEP_BACKWARD: case GTK_SCROLL_PAGE_BACKWARD: value = gtk_adjustment_get_value (adjustment); for (i = 0; i < G_N_ELEMENTS (steps) - 1; i++) if (steps[i + 1] >= value) { value = steps[i]; break; } break; case GTK_SCROLL_STEP_FORWARD: case GTK_SCROLL_PAGE_FORWARD: value = gtk_adjustment_get_value (adjustment); for (i = 0; i < G_N_ELEMENTS (steps); i++) if (steps[i] > value) { value = steps[i]; break; } break; case GTK_SCROLL_START: value = steps[0]; break; case GTK_SCROLL_END: value = steps[G_N_ELEMENTS (steps) - 1]; break; default: g_assert_not_reached (); } gtk_adjustment_set_value (adjustment, value); return TRUE; } static gboolean on_change_value_steps (GtkRange *range, GtkScrollType scroll, gdouble value, gpointer user_data) { GtkAdjustment *adjustment = gtk_range_get_adjustment (range); gdouble lower = gtk_adjustment_get_lower (adjustment); gdouble step = gtk_adjustment_get_step_increment (adjustment); value = lower + (int) ((value - lower) / step + 0.5) * step; gtk_adjustment_set_value (adjustment, value); return TRUE; } static gchar * on_format_value (GtkScale *scale, gdouble value) { return g_strdup_printf (_("%gHz"), value); } int main (int argc, char *argv[]) { gtk_init (&argc, &argv); gtk_window_set_default_icon_name (PROJECT_NAME "-gui"); GError *error = NULL; GtkBuilder *builder = gtk_builder_new (); if (!gtk_builder_add_from_string (builder, ui, -1, &error)) { g_printerr ("%s: %s\n", _("Error"), error->message); exit (EXIT_FAILURE); } GtkWidget *win = GTK_WIDGET (gtk_builder_get_object (builder, "win")); g_signal_connect (win, "destroy", G_CALLBACK (gtk_main_quit), NULL); g_signal_connect_swapped (win, "map-event", G_CALLBACK (load_configuration), builder); gtk_widget_show_all (win); g_signal_connect (gtk_builder_get_object (builder, "polling_scale"), "change-value", G_CALLBACK (on_change_value), NULL); g_signal_connect (gtk_builder_get_object (builder, "polling_scale"), "format-value", G_CALLBACK (on_format_value), NULL); g_signal_connect (gtk_builder_get_object (builder, "cpi_off_scale"), "change-value", G_CALLBACK (on_change_value_steps), NULL); g_signal_connect (gtk_builder_get_object (builder, "cpi_on_scale"), "change-value", G_CALLBACK (on_change_value_steps), NULL); g_signal_connect_swapped (gtk_builder_get_object (builder, "retry_button"), "clicked", G_CALLBACK (retry_load), builder); g_signal_connect_swapped (gtk_builder_get_object (builder, "normal_button"), "clicked", G_CALLBACK (on_set_mode_normal), builder); g_signal_connect_swapped (gtk_builder_get_object (builder, "legacy_button"), "clicked", G_CALLBACK (on_set_mode_legacy), builder); g_signal_connect_swapped (gtk_builder_get_object (builder, "apply_button"), "clicked", G_CALLBACK (save_configuration), builder); gtk_main (); g_object_unref (builder); return 0; }