diff --git a/src/fe-gtk/editlist.c b/src/fe-gtk/editlist.c index 31d67d40..0d53bc29 100644 --- a/src/fe-gtk/editlist.c +++ b/src/fe-gtk/editlist.c @@ -41,6 +41,8 @@ #include "gtkutil.h" #include "maingui.h" #include "editlist.h" +#include "preferences-persistence.h" +#include "setup.h" #define ICON_EDITLIST_NEW "document-new" #define ICON_EDITLIST_DELETE "edit-delete" @@ -373,3 +375,301 @@ editlist_gui_open (char *title1, char *title2, GSList *list, char *title, char * gtk_widget_show (editlist_win); } + +/* Preferences editors are independent instances: several lists may be open + * at once, and their models must never share the legacy dialog globals. */ +typedef struct +{ + GtkWidget *view; + GtkWidget *delete_button; + GtkWidget *up_button; + GtkWidget *down_button; + GSList **target; + const char *file; + char *original; + char *pending; +} PreferencesList; + +static GSList *preferences_lists; + +static void +preferences_list_update_buttons (PreferencesList *editor) +{ + GtkTreeModel *model; + GtkTreeIter iter, next; + GtkTreePath *path; + gboolean selected = gtk_tree_selection_get_selected ( + gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter); + gboolean up = FALSE, down = FALSE; + if (selected) + { + path = gtk_tree_model_get_path (model, &iter); + up = gtk_tree_path_get_indices (path)[0] > 0; + gtk_tree_path_free (path); + next = iter; + down = gtk_tree_model_iter_next (model, &next); + } + gtk_widget_set_sensitive (editor->delete_button, selected); + gtk_widget_set_sensitive (editor->up_button, up); + gtk_widget_set_sensitive (editor->down_button, down); +} + +static void +preferences_list_selection_changed (GtkTreeSelection *selection, PreferencesList *editor) +{ + preferences_list_update_buttons (editor); +} + +static void +preferences_list_rows_reordered (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, + gint *order, PreferencesList *editor) +{ + preferences_list_update_buttons (editor); +} + +static char * +preferences_list_serialize (GSList *list) +{ + GString *text = g_string_new (""); + for (; list; list = list->next) + { + struct popup *item = list->data; + g_string_append_printf (text, "NAME %s\nCMD %s\n\n", item->name, item->cmd); + } + return g_string_free (text, FALSE); +} + +static void +preferences_list_free (gpointer data) +{ + PreferencesList *editor = data; + preferences_lists = g_slist_remove (preferences_lists, editor); + g_free (editor->original); + g_free (editor->pending); + g_free (editor); +} + +static void +preferences_list_edited (GtkCellRenderer *renderer, char *path, char *text, PreferencesList *editor) +{ + GtkTreeIter iter; + GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view)); + if (gtk_tree_model_get_iter_from_string (model, &iter, path)) + gtk_list_store_set (GTK_LIST_STORE (model), &iter, + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (renderer), "column")), text, -1); +} + +static void +preferences_list_add (GtkButton *button, PreferencesList *editor) +{ + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeView *view = GTK_TREE_VIEW (editor->view); + GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view)); + (void)button; + gtk_list_store_insert_with_values (store, &iter, -1, NAME_COLUMN, "", CMD_COLUMN, "", -1); + path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter); + gtk_tree_view_set_cursor (view, path, gtk_tree_view_get_column (view, 0), TRUE); + gtk_tree_view_scroll_to_cell (view, path, NULL, FALSE, 0, 0); + gtk_tree_path_free (path); + preferences_list_update_buttons (editor); +} + +static void +preferences_list_delete (GtkButton *button, PreferencesList *editor) +{ + GtkTreeIter iter; + GtkTreeModel *model; + (void)button; + if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter)) + gtk_list_store_remove (GTK_LIST_STORE (model), &iter); +} + +static void +preferences_list_move (GtkButton *button, PreferencesList *editor) +{ + GtkTreeModel *model; + GtkTreeIter iter, other; + GtkTreePath *path; + gboolean valid; + if (!gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter)) + return; + path = gtk_tree_model_get_path (model, &iter); + if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "direction")) < 0) + valid = gtk_tree_path_prev (path); + else { gtk_tree_path_next (path); valid = TRUE; } + if (valid && gtk_tree_model_get_iter (model, &other, path)) + { + gtk_list_store_swap (GTK_LIST_STORE (model), &iter, &other); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (editor->view), path, NULL, FALSE, 0, 0); + } + gtk_tree_path_free (path); +} + +static gboolean +preferences_list_key (GtkWidget *view, GdkEventKey *event, PreferencesList *editor) +{ + GtkWidget *button; + int direction; + (void)view; + if (!(event->state & GDK_SHIFT_MASK) || (event->keyval != GDK_KEY_Up && event->keyval != GDK_KEY_Down)) + return FALSE; + direction = event->keyval == GDK_KEY_Up ? -1 : 1; + button = gtk_button_new (); + g_object_ref_sink (button); + g_object_set_data (G_OBJECT (button), "direction", GINT_TO_POINTER (direction)); + preferences_list_move (GTK_BUTTON (button), editor); + g_object_unref (button); + return TRUE; +} + +GtkWidget * +editlist_preferences_create (GSList **list, const char *file, const char *title1, + const char *title2, const char *help) +{ + PreferencesList *editor = g_new0 (PreferencesList, 1); + GtkWidget *box, *scroll, *buttons, *button; + GtkListStore *store; + GtkCellRenderer *renderer; + int column; + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + editor->target = list; + editor->file = file; + editor->original = preferences_list_serialize (*list); + preferences_lists = g_slist_append (preferences_lists, editor); + g_object_set_data_full (G_OBJECT (box), "preferences-list", editor, preferences_list_free); + store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING); + editor->view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW (editor->view), TRUE); + gtk_tree_view_set_search_column (GTK_TREE_VIEW (editor->view), NAME_COLUMN); + for (column = 0; column < N_COLUMNS; column++) + { + GtkTreeViewColumn *col; + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, "editable", TRUE, NULL); + g_object_set_data (G_OBJECT (renderer), "column", GINT_TO_POINTER (column)); + g_signal_connect (renderer, "edited", G_CALLBACK (preferences_list_edited), editor); + col = gtk_tree_view_column_new_with_attributes (column ? title2 : title1, renderer, "text", column, NULL); + gtk_tree_view_column_set_resizable (col, TRUE); + gtk_tree_view_column_set_min_width (col, column ? 240 : 140); + gtk_tree_view_column_set_expand (col, column != 0); + gtk_tree_view_append_column (GTK_TREE_VIEW (editor->view), col); + } + editlist_load (store, *list); + g_object_unref (store); + g_signal_connect (editor->view, "key-press-event", G_CALLBACK (preferences_list_key), editor); + scroll = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 220); + gtk_container_add (GTK_CONTAINER (scroll), editor->view); + gtk_box_pack_start (GTK_BOX (box), scroll, TRUE, TRUE, 0); + buttons = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start (GTK_BOX (box), buttons, FALSE, FALSE, 0); + button = gtk_button_new_with_mnemonic (_("_Add")); + g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_add), editor); + gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0); + button = gtk_button_new_with_mnemonic (_("_Delete")); + editor->delete_button = button; + g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_delete), editor); + gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0); + for (column = -1; column <= 1; column += 2) + { + button = gtk_button_new_with_label (column < 0 ? _("Move up") : _("Move down")); + if (column < 0) editor->up_button = button; + else editor->down_button = button; + g_object_set_data (G_OBJECT (button), "direction", GINT_TO_POINTER (column)); + g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_move), editor); + gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0); + } + g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), "changed", + G_CALLBACK (preferences_list_selection_changed), editor); + g_signal_connect (gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view)), "rows-reordered", + G_CALLBACK (preferences_list_rows_reordered), editor); + preferences_list_update_buttons (editor); + if (help) + { + GtkWidget *expander = gtk_expander_new (_("Formatting codes and examples")); + GtkWidget *label = gtk_label_new (help); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_selectable (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 60); + gtk_widget_set_margin_top (label, 10); + gtk_container_add (GTK_CONTAINER (expander), label); + gtk_box_pack_start (GTK_BOX (box), expander, FALSE, FALSE, 0); + } + return box; +} + +gboolean +editlist_preferences_stage (GError **error) +{ + GSList *entry; + for (entry = preferences_lists; entry; entry = entry->next) + { + PreferencesList *editor = entry->data; + GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view)); + GtkTreeIter iter; + GString *text = g_string_new (""); + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + char *live; + g_clear_pointer (&editor->pending, g_free); + while (valid) + { + char *name, *cmd; + gtk_tree_model_get (model, &iter, NAME_COLUMN, &name, CMD_COLUMN, &cmd, -1); + if ((name && strpbrk (name, "\r\n")) || (cmd && strpbrk (cmd, "\r\n")) || + ((!name || !*name) && cmd && *cmd)) + { + GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + int column = !name || !*name || strpbrk (name, "\r\n") ? NAME_COLUMN : CMD_COLUMN; + setup_reveal_widget (editor->view); + gtk_widget_grab_focus (editor->view); + gtk_tree_view_set_cursor (GTK_TREE_VIEW (editor->view), path, + gtk_tree_view_get_column (GTK_TREE_VIEW (editor->view), column), TRUE); + gtk_tree_path_free (path); + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + _("Give this entry a name and keep each field on one line.")); + g_free (name); g_free (cmd); g_string_free (text, TRUE); + return FALSE; + } + if (name && *name) + g_string_append_printf (text, "NAME %s\nCMD %s\n\n", name, cmd ? cmd : ""); + g_free (name); g_free (cmd); + valid = gtk_tree_model_iter_next (model, &iter); + } + if (strcmp (text->str, editor->original) == 0) { g_string_free (text, TRUE); continue; } + live = preferences_list_serialize (*editor->target); + if (strcmp (live, editor->original) != 0) + { + setup_reveal_widget (editor->view); + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_AGAIN, + _("These entries changed outside Preferences. Reopen Preferences before editing them.")); + g_free (live); g_string_free (text, TRUE); + return FALSE; + } + g_free (live); + editor->pending = g_string_free (text, FALSE); + preferences_persistence_stage_file (editor->file, editor->pending); + } + return TRUE; +} + +void +editlist_preferences_commit (void) +{ + GSList *entry, *sessions; + for (entry = preferences_lists; entry; entry = entry->next) + { + PreferencesList *editor = entry->data; + if (!editor->pending) continue; + list_free (editor->target); + list_loadconf ((char *)editor->file, editor->target, NULL); + if (editor->target == &usermenu_list) usermenu_update (); + for (sessions = sess_list; sessions; sessions = sessions->next) + { + if (editor->target == &button_list) fe_buttons_update (sessions->data); + if (editor->target == &dlgbutton_list) fe_dlgbuttons_update (sessions->data); + } + } +} diff --git a/src/fe-gtk/editlist.h b/src/fe-gtk/editlist.h index b267c129..faecaa0c 100644 --- a/src/fe-gtk/editlist.h +++ b/src/fe-gtk/editlist.h @@ -21,5 +21,10 @@ #define ZOITECHAT_EDITLIST_H void editlist_gui_open (char *title1, char *title2, GSList * list, char *title, char *wmclass, char *file, char *help); +GtkWidget *editlist_preferences_create (GSList **list, const char *file, + const char *title1, const char *title2, + const char *help); +gboolean editlist_preferences_stage (GError **error); +void editlist_preferences_commit (void); #endif diff --git a/src/fe-gtk/fkeys.c b/src/fe-gtk/fkeys.c index a5b5b888..88e9dc20 100644 --- a/src/fe-gtk/fkeys.c +++ b/src/fe-gtk/fkeys.c @@ -58,6 +58,8 @@ #ifdef __APPLE__ #include "macos-shortcuts.h" #endif +#include "setup.h" +#include "preferences-persistence.h" #define ICON_FKEYS_NEW "document-new" #define ICON_FKEYS_DELETE "edit-delete" @@ -110,7 +112,7 @@ struct gcomp_data static int key_load_kbs (void); static int key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list); -static int key_save_kbs (void); + static void key_dialog_load (GtkListStore *store); static void key_dialog_reset (GtkWidget *wid, gpointer userdata); static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, @@ -814,75 +816,6 @@ key_dialog_selection_changed (GtkTreeSelection *sel, gpointer userdata) key_dialog_print_text (xtext, _("Select a row to get help information on its Action.")); } -static void -key_dialog_close (GtkWidget *wid, gpointer userdata) -{ - gtk_widget_destroy (key_dialog); - key_dialog = NULL; -} - -static void -key_dialog_save (GtkWidget *wid, gpointer userdata) -{ - GtkTreeModel *store = get_store (); - GtkTreeIter iter; - struct key_binding *kb; - char *data1, *data2, *accel, *actiontext; - guint keyval; - GdkModifierType mod; - - if (keybind_list) - { - g_slist_free_full (keybind_list, key_free); - keybind_list = NULL; - } - - if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter)) - { - do - { - kb = g_new0 (struct key_binding, 1); - - gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, ACCEL_COLUMN, &accel, - ACTION_COLUMN, &actiontext, - D1_COLUMN, &data1, - D2_COLUMN, &data2, - -1); - kb->data1 = data1; - kb->data2 = data2; - - if (accel) - { - gtk_accelerator_parse (accel, &keyval, &mod); - - kb->keyval = keyval; - kb->mod = key_modifier_get_valid (mod); - - g_free (accel); - } - - if (actiontext) - { - kb->action = key_get_action_from_string (actiontext); - g_free (actiontext); - } - - if (!accel || !actiontext) - key_free (kb); - else - keybind_list = g_slist_append (keybind_list, kb); - - } - while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter)); - } - - if (key_save_kbs () == 0) - { - menu_update_quit_accel (); - key_dialog_close (wid, NULL); - } -} - static void key_dialog_add (GtkWidget *wid, gpointer userdata) { @@ -1227,25 +1160,25 @@ key_dialog_load (GtkListStore *store) } } -void -key_dialog_show (void) +static char *key_preferences_serialize (GtkTreeModel *model, GError **error); + +static void +key_preferences_preview_realize (GtkWidget *widget, gpointer data) +{ + gtk_xtext_set_font (GTK_XTEXT (widget), prefs.hex_text_font); +} + +GtkWidget * +key_preferences_create (void) { GtkWidget *vbox, *box, *pane, *scroll; GtkWidget *view, *xtext, *delete_button; GtkListStore *store; XTextColor xtext_palette[XTEXT_COLS]; - char buf[128]; - - if (key_dialog) - { - mg_bring_tofront (key_dialog); - return; - } - - g_snprintf(buf, sizeof(buf), _("Keyboard Shortcuts - %s"), _(DISPLAY_NAME)); - key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close, - NULL, 600, 360, &vbox, 0); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + key_dialog = vbox; + g_object_add_weak_pointer (G_OBJECT (vbox), (gpointer *)&key_dialog); pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL); gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0); box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); @@ -1260,7 +1193,7 @@ key_dialog_show (void) theme_get_xtext_colors (xtext_palette, XTEXT_COLS); xtext = gtk_xtext_new (xtext_palette, 0); gtk_container_add (GTK_CONTAINER (scroll), xtext); - gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font); + g_signal_connect (xtext, "realize", G_CALLBACK (key_preferences_preview_realize), NULL); g_object_set_data (G_OBJECT (key_dialog), "view", view); g_object_set_data (G_OBJECT (key_dialog), "xtext", xtext); @@ -1281,59 +1214,99 @@ key_dialog_show (void) gtk_widget_set_sensitive (delete_button, FALSE); gtkutil_button (box, ICON_FKEYS_RESET, NULL, key_dialog_reset, NULL, _("Reset")); - gtkutil_button (box, ICON_FKEYS_CANCEL, NULL, key_dialog_close, - NULL, _("Cancel")); - gtkutil_button (box, ICON_FKEYS_SAVE, NULL, key_dialog_save, - NULL, _("Save")); store = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (view))); key_dialog_load (store); + g_object_set_data_full (G_OBJECT (vbox), "original-bindings", key_preferences_serialize (GTK_TREE_MODEL (store), NULL), g_free); gtk_widget_show_all (key_dialog); + return vbox; } -static int -key_save_kbs (void) +void +key_dialog_show (void) { - int fd; - char buf[512]; - char *accel_text; - GSList *list = keybind_list; - struct key_binding *kb; + setup_open_page ("keyboard"); +} - fd = zoitechat_open_file ("keybindings.conf", O_CREAT | O_TRUNC | O_WRONLY, - 0x180, XOF_DOMODE); - if (fd < 0) - return 1; - write (fd, buf, g_snprintf (buf, 510, "# ZoiteChat key bindings config file\n\n")); - - while (list) +static char * +key_preferences_serialize (GtkTreeModel *model, GError **error) +{ + GtkTreeIter iter; + GString *text = g_string_new ("# ZoiteChat key bindings config file\n\n"); + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) { - kb = list->data; - - accel_text = gtk_accelerator_name (kb->keyval, kb->mod); - - g_snprintf (buf, 510, "ACCEL=%s\n%s\n", accel_text, key_actions[kb->action].name); - write (fd, buf, strlen (buf)); - g_free (accel_text); - - if (kb->data1 && kb->data1[0]) - write (fd, buf, g_snprintf (buf, 510, "D1:%s\n", kb->data1)); - else - write (fd, "D1!\n", 4); - - if (kb->data2 && kb->data2[0]) - write (fd, buf, g_snprintf (buf, 510, "D2:%s\n", kb->data2)); - else - write (fd, "D2!\n", 4); - - write (fd, "\n", 1); - - list = g_slist_next (list); + char *accel, *action, *data1, *data2; + guint key; + GdkModifierType modifiers; + gtk_tree_model_get (model, &iter, ACCEL_COLUMN, &accel, ACTION_COLUMN, &action, + D1_COLUMN, &data1, D2_COLUMN, &data2, -1); + if ((!accel || !*accel) && (!action || !*action) && (!data1 || !*data1) && (!data2 || !*data2)) + goto next; + gtk_accelerator_parse (accel ? accel : "", &key, &modifiers); + if (!key || !action || strcmp (key_actions[key_get_action_from_string (action)].name, action) != 0 || + (data1 && strpbrk (data1, "\r\n")) || (data2 && strpbrk (data2, "\r\n"))) + { + GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view"); + GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + setup_reveal_widget (GTK_WIDGET (view)); + gtk_widget_grab_focus (GTK_WIDGET (view)); + gtk_tree_view_set_cursor (view, path, NULL, FALSE); + gtk_tree_view_scroll_to_cell (view, path, NULL, FALSE, 0, 0); + gtk_tree_path_free (path); + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + _("Complete each keyboard shortcut with a key and an action. Data fields must stay on one line.")); + g_free (accel); g_free (action); g_free (data1); g_free (data2); + g_string_free (text, TRUE); + return NULL; + } + g_string_append_printf (text, "ACCEL=%s\n%s\n", accel, action); + if (data1 && *data1) g_string_append_printf (text, "D1:%s\n", data1); + else g_string_append (text, "D1!\n"); + if (data2 && *data2) g_string_append_printf (text, "D2:%s\n", data2); + else g_string_append (text, "D2!\n"); + g_string_append_c (text, '\n'); + next: + g_free (accel); g_free (action); g_free (data1); g_free (data2); + valid = gtk_tree_model_iter_next (model, &iter); } + return g_string_free (text, FALSE); +} - close (fd); - return 0; +gboolean +key_preferences_stage (GError **error) +{ + char *contents; + const char *original; + if (!key_dialog) return TRUE; + contents = key_preferences_serialize (get_store (), error); + if (!contents) return FALSE; + original = g_object_get_data (G_OBJECT (key_dialog), "original-bindings"); + g_object_set_data_full (G_OBJECT (key_dialog), "pending-bindings", NULL, g_free); + if (g_strcmp0 (contents, original) != 0) + { + preferences_persistence_stage_file ("keybindings.conf", contents); + g_object_set_data_full (G_OBJECT (key_dialog), "pending-bindings", g_strdup (contents), g_free); + } + g_free (contents); + return TRUE; +} + +void +key_preferences_commit (void) +{ + GSList *bindings = NULL; + const char *contents; + if (!key_dialog) return; + contents = g_object_get_data (G_OBJECT (key_dialog), "pending-bindings"); + if (!contents) return; + if (key_load_kbs_from_buffer (g_strdup (contents), strlen (contents), &bindings) == 0) + { + g_slist_free_full (keybind_list, key_free); + keybind_list = bindings; + menu_update_quit_accel (); + } } #define KBSTATE_MOD 0 diff --git a/src/fe-gtk/fkeys.h b/src/fe-gtk/fkeys.h index d15b47e9..25cc6b9b 100644 --- a/src/fe-gtk/fkeys.h +++ b/src/fe-gtk/fkeys.h @@ -31,6 +31,9 @@ void key_init (void); void key_dialog_show (void); +GtkWidget *key_preferences_create (void); +gboolean key_preferences_stage (GError **error); +void key_preferences_commit (void); int key_handle_key_press (GtkWidget * wid, GdkEventKey * evt, session *sess); int key_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2, session *sess); diff --git a/src/fe-gtk/ignoregui.c b/src/fe-gtk/ignoregui.c index b115eda6..0d1b7210 100644 --- a/src/fe-gtk/ignoregui.c +++ b/src/fe-gtk/ignoregui.c @@ -31,6 +31,7 @@ #include "../common/fe.h" #include "gtkutil.h" #include "maingui.h" +#include "setup.h" #define ICON_IGNORE_NEW "document-new" #define ICON_IGNORE_DELETE "edit-delete" @@ -234,6 +235,8 @@ ignore_delete_entry_clicked (GtkWidget * wid, struct session *sess) static void ignore_store_new (int cancel, char *mask, gpointer data) { + if (cancel || !ignorewin) + return; GtkTreeView *view = g_object_get_data (G_OBJECT (ignorewin), "view"); GtkListStore *store = GTK_LIST_STORE (get_store ()); GtkTreeIter iter; @@ -265,6 +268,11 @@ ignore_store_new (int cancel, char *mask, gpointer data) static void ignore_clear_cb (GtkDialog *dialog, gint response) { + if (!ignorewin) + { + gtk_widget_destroy (GTK_WIDGET (dialog)); + return; + } GtkListStore *store = GTK_LIST_STORE (get_store ()); GtkTreeIter iter; char *mask; @@ -293,7 +301,7 @@ ignore_clear_entry_clicked (GtkWidget * wid) { GtkWidget *dialog; - dialog = gtk_message_dialog_new (NULL, 0, + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (ignorewin)), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, _("Are you sure you want to remove all ignores?")); theme_manager_attach_window (dialog); @@ -335,8 +343,8 @@ ignore_stats_entry (GtkWidget * box, char *label, int value) return wid; } -void -ignore_gui_open (void) +GtkWidget * +ignore_preferences_create (void) { GtkWidget *vbox, *box, *stat_box, *frame; GtkWidget *view; @@ -345,20 +353,9 @@ ignore_gui_open (void) GSList *temp = ignore_list; char *mask; gboolean private, chan, notice, ctcp, dcc, invite, unignore; - char buf[128]; - - if (ignorewin) - { - mg_bring_tofront (ignorewin); - return; - } - - g_snprintf(buf, sizeof(buf), _("Ignore list - %s"), _(DISPLAY_NAME)); - ignorewin = - mg_create_generic_tab ("IgnoreList", buf, FALSE, TRUE, - close_ignore_gui_callback, - NULL, 700, 300, &vbox, 0); - gtkutil_destroy_on_esc (ignorewin); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + ignorewin = vbox; + g_signal_connect (vbox, "destroy", G_CALLBACK (close_ignore_gui_callback), NULL); view = ignore_treeview_new (vbox); g_object_set_data (G_OBJECT (ignorewin), "view", view); @@ -421,7 +418,14 @@ ignore_gui_open (void) temp = temp->next; } - gtk_widget_show (ignorewin); + gtk_widget_show_all (ignorewin); + return vbox; +} + +void +ignore_gui_open (void) +{ + setup_open_page ("ignore"); } void diff --git a/src/fe-gtk/menu.c b/src/fe-gtk/menu.c index 58245f52..b1f870c2 100644 --- a/src/fe-gtk/menu.c +++ b/src/fe-gtk/menu.c @@ -50,6 +50,7 @@ #include "banlist.h" #include "chanlist.h" #include "editlist.h" +#include "setup.h" #include "fkeys.h" #ifdef __APPLE__ #include "macos-shortcuts.h" @@ -1321,7 +1322,6 @@ menu_open_server_list (GtkWidget *wid, gpointer none) static void menu_settings (GtkWidget * wid, gpointer none) { - extern void setup_open (void); setup_open (); } @@ -1336,20 +1336,10 @@ menu_macos_minimize (GtkWidget *widget, gpointer userdata) } #endif -static void -menu_usermenu (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("User menu - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, usermenu_list, buf, "usermenu", "usermenu.conf", 0); -} - static void usermenu_create (GtkWidget *menu) { menu_create (menu, usermenu_list, "", FALSE); - menu_quick_item (0, 0, menu, XCMENU_SHADED, 0, 0); /* sep */ - menu_quick_item_with_callback (menu_usermenu, _("Edit This Menu" ELLIPSIS), menu, 0); } static void @@ -1658,33 +1648,6 @@ menu_banlist (GtkWidget * wid, gpointer none) banlist_opengui (current_sess); } -#ifdef USE_PLUGIN - -static void -menu_loadplugin (void) -{ - plugingui_load (); -} - -static void -menu_pluginlist (void) -{ - plugingui_open (); -} - -#else - -static void -menu_noplugin_info (void) -{ - fe_message (_(DISPLAY_NAME " has been build without plugin support."), FE_MSG_INFO); -} - -#define menu_loadplugin menu_noplugin_info -#define menu_pluginlist menu_noplugin_info - -#endif - #define usercommands_help _("User Commands - Special codes:\n\n"\ "%c = current channel\n"\ "%e = current network name\n"\ @@ -1741,73 +1704,47 @@ menu_noplugin_info (void) "shell instead of ZoiteChat") static void -menu_usercommands (void) +menu_preferences_selector_changed (GtkComboBox *combo, GtkStack *stack) { - char buf[128]; - g_snprintf(buf, sizeof(buf), _("User Defined Commands - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, command_list, buf, "commands", "commands.conf", - usercommands_help); + const char *id = gtk_combo_box_get_active_id (combo); + if (id) gtk_stack_set_visible_child_name (stack, id); } -static void -menu_ulpopup (void) +GtkWidget * +menu_preferences_create (const char *id) { - char buf[128]; - g_snprintf(buf, sizeof(buf), _("Userlist Popup menu - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, popup_list, buf, "popup", "popup.conf", ulbutton_help); -} - -static void -menu_rpopup (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("Replace - %s"), _(DISPLAY_NAME)); - editlist_gui_open (_("Text"), _("Replace with"), replace_list, buf, "replace", "replace.conf", 0); -} - -static void -menu_urlhandlers (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("URL Handlers - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, urlhandler_list, buf, "urlhandlers", "urlhandlers.conf", url_help); -} - -static void -menu_evtpopup (void) -{ - pevent_dialog_show (); -} - -static void -menu_keypopup (void) -{ - key_dialog_show (); -} - -static void -menu_ulbuttons (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("Userlist buttons - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, button_list, buf, "buttons", "buttons.conf", ulbutton_help); -} - -static void -menu_dlgbuttons (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("Dialog buttons - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, dlgbutton_list, buf, "dlgbuttons", "dlgbuttons.conf", - dlgbutton_help); -} - -static void -menu_ctcpguiopen (void) -{ - char buf[128]; - g_snprintf(buf, sizeof(buf), _("CTCP Replies - %s"), _(DISPLAY_NAME)); - editlist_gui_open (NULL, NULL, ctcp_list, buf, "ctcpreply", "ctcpreply.conf", ctcp_help); + if (!strcmp (id, "replace")) + return editlist_preferences_create (&replace_list, "replace.conf", _("Text"), _("Replace with"), NULL); + if (!strcmp (id, "commands")) + return editlist_preferences_create (&command_list, "commands.conf", _("Command"), _("Expansion"), usercommands_help); + if (!strcmp (id, "urls")) + return editlist_preferences_create (&urlhandler_list, "urlhandlers.conf", _("Name"), _("Command"), url_help); + if (!strcmp (id, "ctcp")) + return editlist_preferences_create (&ctcp_list, "ctcpreply.conf", _("Request"), _("Reply"), ctcp_help); + { + GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + GtkWidget *selector = gtk_combo_box_text_new (); + GtkWidget *stack = gtk_stack_new (); + const char *ids[] = {"buttons", "popup", "dlgbuttons", "usermenu"}; + const char *names[] = {N_("User list buttons"), N_("User list popup"), N_("Dialog buttons"), N_("User menu")}; + const char *files[] = {"buttons.conf", "popup.conf", "dlgbuttons.conf", "usermenu.conf"}; + GSList **lists[] = {&button_list, &popup_list, &dlgbutton_list, &usermenu_list}; + const char *help[] = {ulbutton_help, ulbutton_help, dlgbutton_help, ulbutton_help}; + int i; + gtk_box_pack_start (GTK_BOX (box), selector, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), stack, TRUE, TRUE, 0); + gtk_stack_set_homogeneous (GTK_STACK (stack), FALSE); + for (i = 0; i < 4; i++) + { + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (selector), ids[i], _(names[i])); + gtk_stack_add_named (GTK_STACK (stack), editlist_preferences_create (lists[i], files[i], _("Name"), _("Command"), help[i]), ids[i]); + } + g_signal_connect (selector, "changed", G_CALLBACK (menu_preferences_selector_changed), stack); + gtk_combo_box_set_active (GTK_COMBO_BOX (selector), 0); + g_object_set_data (G_OBJECT (box), "menus-selector", selector); + g_object_set_data (G_OBJECT (stack), "preferences-selector", selector); + return box; + } } static void @@ -2000,7 +1937,7 @@ menu_about (GtkWidget *wid, gpointer sess) static struct mymenu mymenu[] = { {N_("_ZoiteChat"), 0, 0, M_NEWMENU, MENU_ID_ZOITECHAT, 0, 1}, - {N_("Network Li_st"), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1}, + {N_("Network Li_st" ELLIPSIS), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1}, {0, 0, 0, M_SEP, 0, 0, 0}, {N_("_New"), 0, 0, M_MENUSUB, 0, 0, 1}, @@ -2011,14 +1948,14 @@ static struct mymenu mymenu[] = { {0, 0, 0, M_END, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0}, - {N_("_Load Plugin or Script" ELLIPSIS), menu_loadplugin, 0, M_MENUITEM, 0, 0, 1}, - {0, 0, 0, M_SEP, 0, 0, 0}, /* 11 */ -#define DETACH_OFFSET (12) - {0, menu_detach, 0, M_MENUITEM, 0, 0, 1}, /* 12 */ -#define CLOSE_OFFSET (13) +#define DETACH_OFFSET (10) + {0, menu_detach, 0, M_MENUITEM, 0, 0, 1}, +#define CLOSE_OFFSET (11) {0, menu_close, 0, M_MENUITEM, 0, 0, 1}, {0, 0, 0, M_SEP, 0, 0, 0}, - {N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1}, /* 15 */ + {N_("_Preferences" ELLIPSIS), menu_settings, 0, M_MENUITEM, 0, 0, 1}, + {0, 0, 0, M_SEP, 0, 0, 0}, + {N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1}, {N_("_View"), 0, 0, M_NEWMENU, 0, 0, 1}, #define MENUBAR_OFFSET (17) @@ -2028,18 +1965,18 @@ static struct mymenu mymenu[] = { {N_("U_ser List Buttons"), menu_ulbuttons_toggle, 0, M_MENUTOG, MENU_ID_ULBUTTONS, 0, 1}, {N_("M_ode Buttons"), menu_cmbuttons_toggle, 0, M_MENUTOG, MENU_ID_MODEBUTTONS, 0, 1}, {0, 0, 0, M_SEP, 0, 0, 0}, - {N_("_Channel Switcher"), 0, 0, M_MENUSUB, 0, 0, 1}, /* 23 */ + {N_("_Channel Switcher"), 0, 0, M_MENUSUB, 0, 0, 1}, #define TABS_OFFSET (24) {N_("_Tabs"), menu_layout_cb, 0, M_MENURADIO, MENU_ID_LAYOUT_TABS, 0, 1}, {N_("T_ree"), 0, 0, M_MENURADIO, MENU_ID_LAYOUT_TREE, 0, 1}, {0, 0, 0, M_END, 0, 0, 0}, - {N_("_Network Meters"), 0, 0, M_MENUSUB, 0, 0, 1}, /* 27 */ + {N_("_Network Meters"), 0, 0, M_MENUSUB, 0, 0, 1}, #define METRE_OFFSET (28) {N_("Off"), menu_metres_off, 0, M_MENURADIO, 0, 0, 1}, {N_("Graph"), menu_metres_graph, 0, M_MENURADIO, 0, 0, 1}, {N_("Text"), menu_metres_text, 0, M_MENURADIO, 0, 0, 1}, {N_("Both"), menu_metres_both, 0, M_MENURADIO, 0, 0, 1}, - {0, 0, 0, M_END, 0, 0, 0}, /* 32 */ + {0, 0, 0, M_END, 0, 0, 0}, { 0, 0, 0, M_SEP, 0, 0, 0 }, {N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1}, @@ -2052,20 +1989,7 @@ static struct mymenu mymenu[] = { #define AWAY_OFFSET (41) {N_("Marked _Away"), menu_away_toggle, 0, M_MENUITEM, MENU_ID_AWAY, 0, 1}, - {N_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1}, /* 40 */ - - {N_("S_ettings"), 0, 0, M_NEWMENU, 0, 0, 1}, - {N_("_Preferences"), menu_settings, 0, M_MENUITEM, 0, 0, 1}, - {0, 0, 0, M_SEP, 0, 0, 0}, - {N_("Auto Replace"), menu_rpopup, 0, M_MENUITEM, 0, 0, 1}, - {N_("CTCP Replies"), menu_ctcpguiopen, 0, M_MENUITEM, 0, 0, 1}, - {N_("Dialog Buttons"), menu_dlgbuttons, 0, M_MENUITEM, 0, 0, 1}, - {N_("Keyboard Shortcuts"), menu_keypopup, 0, M_MENUITEM, 0, 0, 1}, - {N_("Text Events"), menu_evtpopup, 0, M_MENUITEM, 0, 0, 1}, - {N_("URL Handlers"), menu_urlhandlers, 0, M_MENUITEM, 0, 0, 1}, - {N_("User Commands"), menu_usercommands, 0, M_MENUITEM, 0, 0, 1}, - {N_("User List Buttons"), menu_ulbuttons, 0, M_MENUITEM, 0, 0, 1}, - {N_("User List Popup"), menu_ulpopup, 0, M_MENUITEM, 0, 0, 1}, /* 52 */ + {N_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1}, {N_("_Window"), 0, 0, M_NEWMENU, 0, 0, 1}, {N_("_Ban List"), menu_banlist, 0, M_MENUITEM, 0, 0, 1}, @@ -2073,9 +1997,7 @@ static struct mymenu mymenu[] = { {N_("Direct Chat"), menu_dcc_chat_win, 0, M_MENUITEM, 0, 0, 1}, {N_("File _Transfers"), menu_dcc_win, 0, M_MENUITEM, 0, 0, 1}, {N_("Friends List"), notify_opengui, 0, M_MENUITEM, 0, 0, 1}, - {N_("Ignore List"), ignore_gui_open, 0, M_MENUITEM, 0, 0, 1}, - {N_("_Plugins and Scripts"), menu_pluginlist, 0, M_MENUITEM, 0, 0, 1}, - {N_("_Raw Log"), menu_rawlog, 0, M_MENUITEM, 0, 0, 1}, /* 61 */ + {N_("_Raw Log"), menu_rawlog, 0, M_MENUITEM, 0, 0, 1}, {N_("_URL Grabber"), url_opengui, 0, M_MENUITEM, 0, 0, 1}, {0, 0, 0, M_SEP, 0, 0, 0}, {N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1}, @@ -2083,14 +2005,14 @@ static struct mymenu mymenu[] = { {N_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1}, {N_("C_lear Text"), menu_flushbuffer, 0, M_MENUITEM, 0, 0, 1}, {N_("Save Text" ELLIPSIS), menu_savebuffer, 0, M_MENUITEM, 0, 0, 1}, -#define SEARCH_OFFSET (71) +#define SEARCH_OFFSET (57) {N_("Search"), 0, 0, M_MENUSUB, 0, 0, 1}, {N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 0, 0, 1}, {N_("Search Next" ), menu_search_next, 0, M_MENUITEM, 0, 0, 1}, {N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1}, {0, 0, 0, M_END, 0, 0, 0}, - {N_("_Help"), 0, 0, M_NEWMENU, 0, 0, 1}, /* 74 */ + {N_("_Help"), 0, 0, M_NEWMENU, 0, 0, 1}, {N_("_Contents"), menu_docs, 0, M_MENUITEM, 0, 0, 1}, {N_("_About"), menu_about, 0, M_MENUITEM, 0, 0, 1}, @@ -2100,42 +2022,24 @@ static struct mymenu mymenu[] = { static const char * menu_get_key_action_name (int index) { - switch (index) - { - case 1: - return "network-list"; - case 4: - return "new-server-tab"; - case 6: - return "new-server-window"; - case CLOSE_OFFSET: - return "close"; - case 15: - return "quit"; - case MENUBAR_OFFSET: - return "menu-toggle"; - case MENUBAR_OFFSET + 2: - return "user-list-toggle"; - case 34: - return "fullscreen-toggle"; - case AWAY_OFFSET: - return "away-toggle"; - case 66: - return "reset-marker"; - case 67: - return "move-marker"; - case 68: - return "copy-selection"; - case SEARCH_OFFSET + 1: - return "search-text"; - case SEARCH_OFFSET + 2: - return "search-next"; - case SEARCH_OFFSET + 3: - return "search-previous"; - case 77: - return "contents"; - } - + /* Bind accelerators to actions rather than positions in the menu. */ + void *callback = mymenu[index].callback; + if (callback == menu_open_server_list) return "network-list"; + if (callback == menu_newserver_tab) return "new-server-tab"; + if (callback == menu_newserver_window) return "new-server-window"; + if (callback == menu_close) return "close"; + if (callback == menu_quit) return "quit"; + if (callback == menu_bar_toggle_cb) return "menu-toggle"; + if (callback == menu_userlist_toggle) return "user-list-toggle"; + if (callback == menu_fullscreen_toggle) return "fullscreen-toggle"; + if (callback == menu_away_toggle) return "away-toggle"; + if (callback == menu_resetmarker) return "reset-marker"; + if (callback == menu_movetomarker) return "move-marker"; + if (callback == menu_copy_selection) return "copy-selection"; + if (callback == menu_search) return "search-text"; + if (callback == menu_search_next) return "search-next"; + if (callback == menu_search_prev) return "search-previous"; + if (callback == menu_docs) return "contents"; return NULL; } @@ -3008,7 +2912,7 @@ togitem: #ifdef HAVE_GTK_MAC /* We want ZoiteChat to be the app menu, not including Quit or ZoiteChat itself */ - if (bar && item && i <= CLOSE_OFFSET + 1 && mymenu[i].id != MENU_ID_ZOITECHAT) + if (bar && item && (i <= CLOSE_OFFSET + 1 || mymenu[i].callback == menu_settings) && mymenu[i].id != MENU_ID_ZOITECHAT) { if (!submenu || mymenu[i].type == M_MENUSUB) gtkosx_application_insert_app_menu_item (osx_app, item, appmenu_offset++); diff --git a/src/fe-gtk/menu.h b/src/fe-gtk/menu.h index 41a03b90..e883ca61 100644 --- a/src/fe-gtk/menu.h +++ b/src/fe-gtk/menu.h @@ -42,6 +42,7 @@ void menu_bar_toggle (void); void menu_add_plugin_items (GtkWidget *menu, char *root, char *target); void menu_change_layout (void); void menu_update_quit_accel (void); +GtkWidget *menu_preferences_create (const char *id); gboolean menu_key_action (const char *name, guint keyval, GdkModifierType state); void menu_set_away (session_gui *gui, int away); diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index 699d0ccc..a878b65d 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -62,7 +62,8 @@ if host_machine.system() == 'darwin' and gtk_mac_dep.found() gtk_dep = declare_dependency(dependencies: [gtk_dep, gtk_mac_dep]) endif -zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep] +theme_math_dep = cc.find_library('m', required: false) +zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep, theme_math_dep] if host_machine.system() != 'windows' appindicator_opt = get_option('appindicator') @@ -243,6 +244,17 @@ theme_runtime_tests = executable('theme_runtime_persistence_tests', link_with: zoitechat_theme_lib, ) +preferences_persistence_tests = executable('preferences_persistence_tests', + 'tests/test-preferences-persistence.c', + include_directories: [config_h_include], + dependencies: [gtk_dep], +) + +test('Preferences Persistence Tests', preferences_persistence_tests, + protocol: 'tap', + timeout: 120, +) + test('Theme Runtime Persistence Tests', theme_runtime_tests, protocol: 'tap', timeout: 120, @@ -277,7 +289,7 @@ test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests, theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests', 'theme/tests/test-theme-preferences-gtk3-populate.c', include_directories: [config_h_include], - dependencies: [gtk_dep], + dependencies: [gtk_dep, theme_math_dep], ) test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_tests, diff --git a/src/fe-gtk/plugingui.c b/src/fe-gtk/plugingui.c index 6cdb132c..ae70f2e5 100644 --- a/src/fe-gtk/plugingui.c +++ b/src/fe-gtk/plugingui.c @@ -35,6 +35,8 @@ typedef struct session zoitechat_context; #include "../common/cfgfiles.h" #include "gtkutil.h" #include "maingui.h" +#include "setup.h" +#include "plugingui.h" /* model for the plugin treeview */ enum @@ -357,24 +359,15 @@ plugingui_reloadbutton_cb (GtkWidget *wid, GtkTreeView *view) } } -void -plugingui_open (void) +GtkWidget * +plugingui_preferences_create (void) { GtkWidget *view; GtkWidget *view_scroll; GtkWidget *vbox, *hbox; - char buf[128]; - - if (plugin_window) - { - mg_bring_tofront (plugin_window); - return; - } - - g_snprintf(buf, sizeof(buf), _("Plugins and Scripts - %s"), _(DISPLAY_NAME)); - plugin_window = mg_create_generic_tab ("Addons", buf, FALSE, TRUE, plugingui_close, NULL, - 700, 300, &vbox, 0); - gtkutil_destroy_on_esc (plugin_window); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + plugin_window = vbox; + g_signal_connect (vbox, "destroy", G_CALLBACK (plugingui_close), NULL); view = plugingui_treeview_new (vbox); view_scroll = gtk_widget_get_parent (view); @@ -404,4 +397,11 @@ plugingui_open (void) fe_pluginlist_update (); gtk_widget_show_all (plugin_window); + return vbox; +} + +void +plugingui_open (void) +{ + setup_open_page ("plugins"); } diff --git a/src/fe-gtk/plugingui.h b/src/fe-gtk/plugingui.h index b21235b0..d90fbd48 100644 --- a/src/fe-gtk/plugingui.h +++ b/src/fe-gtk/plugingui.h @@ -21,6 +21,7 @@ #define ZOITECHAT_PLUGINGUI_H void plugingui_open (void); +GtkWidget *plugingui_preferences_create (void); void plugingui_load (void); #endif diff --git a/src/fe-gtk/preferences-persistence.c b/src/fe-gtk/preferences-persistence.c index 719108ea..3c0cdc57 100644 --- a/src/fe-gtk/preferences-persistence.c +++ b/src/fe-gtk/preferences-persistence.c @@ -23,64 +23,227 @@ #include "../common/cfgfiles.h" #include "theme/theme-runtime.h" +#include +#include +#include +#include +#ifdef WIN32 +#include +#else +#include +#endif + +typedef struct +{ + char *name; + char *contents; + char *temporary; + char *backup; + gboolean existed; + gboolean installed; +} PreferencesFile; + +static GPtrArray *staged_files; + +static void +preferences_file_free (gpointer data) +{ + PreferencesFile *file = data; + if (file->temporary) g_unlink (file->temporary); + if (file->backup) g_unlink (file->backup); + g_free (file->name); + g_free (file->contents); + g_free (file->temporary); + g_free (file->backup); + g_free (file); +} + +void +preferences_persistence_clear_staged_files (void) +{ + if (staged_files) g_ptr_array_free (staged_files, TRUE); + staged_files = NULL; +} + +void +preferences_persistence_stage_file (const char *name, const char *contents) +{ + PreferencesFile *file; + g_return_if_fail (name && contents && !strchr (name, '/') && !strchr (name, '\\')); + if (!staged_files) + staged_files = g_ptr_array_new_with_free_func (preferences_file_free); + file = g_new0 (PreferencesFile, 1); + file->name = g_strdup (name); + file->contents = g_strdup (contents); + g_ptr_array_add (staged_files, file); +} + +/* A temporary in the configuration directory stays on the same filesystem. + * Restrictive permissions also protect command text and identity settings. */ +static gboolean +preferences_write_temporary (const char *contents, gsize length, char **path) +{ + int fd; + gsize written = 0; + char *temporary = g_build_filename (get_xdir (), ".preferences-XXXXXX", NULL); + fd = g_mkstemp (temporary); + if (fd < 0) { g_free (temporary); return FALSE; } + while (written < length) + { + gssize count = write (fd, contents + written, length - written); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) + { + close (fd); + g_unlink (temporary); + g_free (temporary); + return FALSE; + } + written += count; + } + if (close (fd) != 0) + { + g_unlink (temporary); + g_free (temporary); + return FALSE; + } + *path = temporary; + return TRUE; +} + +static gboolean +preferences_backup (PreferencesFile *file) +{ + char *path = g_build_filename (get_xdir (), file->name, NULL); + char *contents = NULL; + gsize length; + GError *error = NULL; + gboolean success; + if (!g_file_get_contents (path, &contents, &length, &error)) + { + success = g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT); + g_clear_error (&error); + g_free (path); + return success; + } + file->existed = TRUE; + success = preferences_write_temporary (contents, length, &file->backup); + g_free (contents); + g_free (path); + return success; +} + +static gboolean +preferences_install (PreferencesFile *file, const char *temporary) +{ + char *path = g_build_filename (get_xdir (), file->name, NULL); + gboolean success; +#ifdef WIN32 + /* Match the existing config/theme save convention on Windows. The + * backup remains available if replacement fails after unlinking. */ + if (file->existed && g_unlink (path) != 0) + { + g_free (path); + return FALSE; + } + file->installed = TRUE; +#endif + success = g_rename (temporary, path) == 0; + if (success) file->installed = TRUE; + g_free (path); + return success; +} + +static gboolean +preferences_restore (PreferencesFile *file) +{ + char *path; + gboolean success; + if (!file->installed) return TRUE; + path = g_build_filename (get_xdir (), file->name, NULL); + if (file->existed) + { +#ifdef WIN32 + g_unlink (path); +#endif + success = g_rename (file->backup, path) == 0; + /* Retain the recovery copy if restoration itself fails. */ + if (!success) g_clear_pointer (&file->backup, g_free); + } + else success = g_unlink (path) == 0 || errno == ENOENT; + g_free (path); + return success; +} + PreferencesPersistenceResult preferences_persistence_save_all (void) { - PreferencesPersistenceResult result; - char *config_temp; - char *theme_temp; - - result.success = FALSE; - result.retry_possible = TRUE; - result.partial_failure = FALSE; - result.config_failed = FALSE; - result.theme_failed = FALSE; - result.failed_file = NULL; - config_temp = NULL; - theme_temp = NULL; - - if (!save_config_prepare (&config_temp)) - { - result.config_failed = TRUE; - goto done; - } - - if (!theme_runtime_save_prepare (&theme_temp)) - { - result.theme_failed = TRUE; - goto done; - } - - if (!save_config_finalize (config_temp)) - { - result.config_failed = TRUE; - goto done; - } - - if (!theme_runtime_save_finalize (theme_temp)) - { - result.theme_failed = TRUE; - result.partial_failure = TRUE; - goto done; - } - - result.success = TRUE; - + PreferencesPersistenceResult result = {0}; + GPtrArray *files = g_ptr_array_new_with_free_func (preferences_file_free); + PreferencesFile *config = g_new0 (PreferencesFile, 1); + PreferencesFile *theme = g_new0 (PreferencesFile, 1); + guint i; + result.retry_possible = TRUE; + config->name = g_strdup ("zoitechat.conf"); + theme->name = g_strdup ("colors.conf"); + g_ptr_array_add (files, config); + g_ptr_array_add (files, theme); + if (!save_config_prepare (&config->temporary)) + { + result.config_failed = TRUE; + result.failed_file = "zoitechat.conf"; + goto done; + } + if (!theme_runtime_save_prepare (&theme->temporary)) + { + result.theme_failed = TRUE; + result.failed_file = "colors.conf"; + goto done; + } + for (i = 0; staged_files && i < staged_files->len; i++) + { + PreferencesFile *queued = g_ptr_array_index (staged_files, i); + PreferencesFile *file = g_new0 (PreferencesFile, 1); + file->name = g_strdup (queued->name); + g_ptr_array_add (files, file); + if (!preferences_write_temporary (queued->contents, strlen (queued->contents), &file->temporary)) + { + result.failed_file = queued->name; + goto done; + } + } + /* Prepare every file and recovery copy before replacing anything. */ + for (i = 0; i < files->len; i++) + { + PreferencesFile *file = g_ptr_array_index (files, i); + if (!preferences_backup (file)) + { + result.failed_file = i < 2 ? (i ? "colors.conf" : "zoitechat.conf") + : ((PreferencesFile *)g_ptr_array_index (staged_files, i - 2))->name; + result.config_failed = i == 0; + result.theme_failed = i == 1; + goto done; + } + } + for (i = 0; i < files->len; i++) + { + PreferencesFile *file = g_ptr_array_index (files, i); + if (!preferences_install (file, file->temporary)) + { + result.failed_file = i < 2 ? (i ? "colors.conf" : "zoitechat.conf") + : ((PreferencesFile *)g_ptr_array_index (staged_files, i - 2))->name; + result.config_failed = i == 0; + result.theme_failed = i == 1; + goto rollback; + } + } + result.success = TRUE; + goto done; + rollback: + for (i = files->len; i > 0; i--) + if (!preferences_restore (g_ptr_array_index (files, i - 1))) + result.partial_failure = TRUE; done: - if (!result.success) - { - if (result.config_failed && result.theme_failed) - result.failed_file = "zoitechat.conf and colors.conf"; - else if (result.config_failed) - result.failed_file = "zoitechat.conf"; - else if (result.theme_failed) - result.failed_file = "colors.conf"; - } - - save_config_discard (config_temp); - theme_runtime_save_discard (theme_temp); - g_free (config_temp); - g_free (theme_temp); - - return result; + g_ptr_array_free (files, TRUE); + return result; } diff --git a/src/fe-gtk/preferences-persistence.h b/src/fe-gtk/preferences-persistence.h index 10cd28ce..f0560919 100644 --- a/src/fe-gtk/preferences-persistence.h +++ b/src/fe-gtk/preferences-persistence.h @@ -34,5 +34,9 @@ typedef struct } PreferencesPersistenceResult; PreferencesPersistenceResult preferences_persistence_save_all (void); +/* Editors queue serialized settings; runtime state is committed only after + * every queued file and the main preferences have been saved successfully. */ +void preferences_persistence_stage_file (const char *name, const char *contents); +void preferences_persistence_clear_staged_files (void); #endif diff --git a/src/fe-gtk/servlistgui.c b/src/fe-gtk/servlistgui.c index 54902a60..1edb42dd 100644 --- a/src/fe-gtk/servlistgui.c +++ b/src/fe-gtk/servlistgui.c @@ -70,7 +70,7 @@ static GtkWidget *entry_nick1; static GtkWidget *entry_nick2; static GtkWidget *entry_nick3; static GtkWidget *entry_guser; -/* static GtkWidget *entry_greal; */ +static GtkWidget *entry_greal; enum { SERVER_TREE, @@ -1650,7 +1650,7 @@ servlist_savegui (void) sp = strchr (prefs.hex_irc_user_name, ' '); if (sp) sp[0] = 0; /* spaces will break the login */ - /* strcpy (prefs.hex_irc_real_name, gtk_entry_get_text (GTK_ENTRY (entry_greal))); */ + safe_strcpy (prefs.hex_irc_real_name, gtk_entry_get_text (GTK_ENTRY (entry_greal)), sizeof(prefs.hex_irc_real_name)); servlist_save (); if (!save_config ()) fe_message (_("Could not save zoitechat.conf."), FE_MSG_WARN); @@ -2809,12 +2809,12 @@ servlist_open_networks (void) GtkWidget *label4; GtkWidget *label5; GtkWidget *label6; - /* GtkWidget *label7; */ + GtkWidget *label7; GtkWidget *entry1; GtkWidget *entry2; GtkWidget *entry3; GtkWidget *entry4; - /* GtkWidget *entry5; */ + GtkWidget *entry5; GtkWidget *vbox2; GtkWidget *label1; GtkWidget *table4; @@ -2900,12 +2900,12 @@ servlist_open_networks (void) gtk_widget_set_halign (label6, GTK_ALIGN_START); gtk_widget_set_valign (label6, GTK_ALIGN_CENTER); - /* label7 = gtk_label_new_with_mnemonic (_("Rea_l name:")); + label7 = gtk_label_new_with_mnemonic (_("Rea_l name:")); gtk_widget_show (label7); - gtk_table_attach (GTK_TABLE (table1), label7, 0, 1, 4, 5, - (GtkAttachOptions) (GTK_FILL), - (GtkAttachOptions) (0), 0, 0); - */ + servlist_table_attach (table1, label7, 0, 1, 4, 5, + FALSE, FALSE, + SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, + 0, 0); entry_nick1 = entry1 = gtk_entry_new (); gtk_entry_set_text (GTK_ENTRY (entry1), prefs.hex_irc_nick1); @@ -2939,12 +2939,14 @@ servlist_open_networks (void) SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER, 0, 0); - /* entry_greal = entry5 = gtk_entry_new (); + entry_greal = entry5 = gtk_entry_new (); + gtk_entry_set_max_length (GTK_ENTRY (entry5), sizeof prefs.hex_irc_real_name - 1); gtk_entry_set_text (GTK_ENTRY (entry5), prefs.hex_irc_real_name); gtk_widget_show (entry5); - gtk_table_attach (GTK_TABLE (table1), entry5, 1, 2, 4, 5, - (GtkAttachOptions) (GTK_EXPAND | GTK_FILL), - (GtkAttachOptions) (0), 0, 0); */ + servlist_table_attach (table1, entry5, 1, 2, 4, 5, + TRUE, FALSE, + SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER, + 0, 0); vbox2 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); gtk_widget_show (vbox2); @@ -3098,7 +3100,7 @@ button_connect = gtkutil_button (hbuttonbox1, ICON_SERVLIST_CONNECT, NULL, gtk_label_set_mnemonic_widget (GTK_LABEL (label3), entry1); gtk_label_set_mnemonic_widget (GTK_LABEL (label6), entry4); - /* gtk_label_set_mnemonic_widget (GTK_LABEL (label7), entry5); */ + gtk_label_set_mnemonic_widget (GTK_LABEL (label7), entry5); gtk_widget_grab_focus (networks_tree); gtk_widget_grab_default (button_close); diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 5b74013d..90e4f834 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -33,6 +33,7 @@ #include "../common/util.h" #include "../common/zoitechatc.h" #include "../common/outbound.h" +#include "../common/servlist.h" #include "fe-gtk.h" #include "theme/theme-manager.h" #include "theme/theme-preferences.h" @@ -41,6 +42,15 @@ #include "pixmaps.h" #include "menu.h" #include "preferences-persistence.h" +#include "setup.h" +#include "editlist.h" +#include "fkeys.h" +#include "textgui.h" +#ifdef USE_PLUGIN +#include "plugingui.h" +#endif + +GtkWidget *ignore_preferences_create (void); #include "plugin-tray.h" #include "notifications/notification-backend.h" @@ -49,10 +59,9 @@ #endif #include "sexy-spell-entry.h" -#define LABEL_INDENT 12 +#define LABEL_INDENT 0 static GtkWidget *setup_window = NULL; -static int last_selected_page = 0; static int last_selected_row = 0; /* sound row */ static gboolean color_change; static struct zoitechatprefs setup_prefs; @@ -93,11 +102,30 @@ typedef struct typedef GtkWidget *(*setup_page_factory) (const setting *settings); -#define SETUP_MAX_PAGES 12 -static setup_page_factory setup_page_factories[SETUP_MAX_PAGES]; -static const setting *setup_page_settings[SETUP_MAX_PAGES]; -static GtkWidget *setup_page_containers[SETUP_MAX_PAGES]; -static int setup_page_count = 0; +/* A page owns its navigation and search metadata. Adding a settings panel does + * not require another parallel category array or a fixed page limit. */ +typedef struct +{ + const char *id; + const char *group; + const char *title; + const char *description; + setup_page_factory factory; + const setting *settings; + GtkWidget *container; + GtkWidget *body; + GtkWidget *row; + char *search_text; +} setup_page; + +static GArray *setup_pages; +static GtkWidget *setup_book; +static GtkWidget *setup_sidebar; +static GtkWidget *setup_search; +static GtkWidget *setup_empty; +static GtkWidget *setup_error_bar; +static GtkWidget *setup_error_label; +static char *setup_query; #ifdef WIN32 static const char *const langsmenu[] = @@ -341,7 +369,9 @@ static const char *const swtype[] = static const setting tabs_settings[] = { + {ST_HEADER, N_("Channel Switcher"), 0, 0, 0}, {ST_RADIO, N_("Switcher type:"),P_OFFINTNL(hex_gui_tab_layout), 0, swtype, 0}, + {ST_HEADER, N_("Tab Behavior"), 0, 0, 0}, {ST_TOGGLE, N_("Open an extra tab for server messages"), P_OFFINTNL(hex_gui_tab_server), 0, 0, 0}, {ST_TOGGLE, N_("Open a new tab when you receive a private message"), P_OFFINTNL(hex_gui_autoopen_dialog), 0, 0, 0}, {ST_TOGGLE, N_("Sort tabs in alphabetical order"), P_OFFINTNL(hex_gui_tab_sort), 0, 0, 0}, @@ -350,6 +380,7 @@ static const setting tabs_settings[] = {ST_TOGGLE, N_("Scroll mouse-wheel to change tabs"), P_OFFINTNL (hex_gui_tab_scrollchans), 0, 0, 0}, {ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0}, {ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0}, + {ST_HEADER, N_("Fonts & Placement"), 0, 0, 0}, {ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0}, {ST_EFONT, N_("Tab font:"), P_OFFSETNL(hex_gui_tab_font), N_("Font for server and channel tabs in the Tabs layout. Leave blank to use the theme font. Smaller text still applies."), @@ -601,7 +632,6 @@ static const setting advanced_settings[] = "while selecting."), 0, 0}, {ST_HEADER, N_("Miscellaneous"), 0, 0, 0}, - {ST_ENTRY, N_("Real name:"), P_OFFSETNL(hex_irc_real_name), 0, 0, sizeof prefs.hex_irc_real_name}, #ifdef WIN32 {ST_ENTRY, N_("Alternative fonts:"), P_OFFSETNL(hex_text_font_alternative), N_("Separate multiple entries with commas without spaces before or after."), 0, sizeof prefs.hex_text_font_alternative}, #endif @@ -817,6 +847,11 @@ setup_create_3oggle (GtkWidget *tab, int row, const setting *set) wid = gtk_check_button_new (); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wid), setup_get_int3 (&setup_prefs, offsets[0])); + { + char *name = g_strdup_printf ("%s %s", _(set->label), _("Channel Message")); + atk_object_set_name (gtk_widget_get_accessible (wid), name); + g_free (name); + } g_signal_connect (G_OBJECT (wid), "toggled", G_CALLBACK (setup_3oggle_cb), ((int *)&setup_prefs) + offsets[0]); setup_table_attach (tab, wid, 3, 4, row, row + 1, FALSE, FALSE, @@ -825,6 +860,11 @@ setup_create_3oggle (GtkWidget *tab, int row, const setting *set) wid = gtk_check_button_new (); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wid), setup_get_int3 (&setup_prefs, offsets[1])); + { + char *name = g_strdup_printf ("%s %s", _(set->label), _("Private Message")); + atk_object_set_name (gtk_widget_get_accessible (wid), name); + g_free (name); + } g_signal_connect (G_OBJECT (wid), "toggled", G_CALLBACK (setup_3oggle_cb), ((int *)&setup_prefs) + offsets[1]); setup_table_attach (tab, wid, 4, 5, row, row + 1, FALSE, FALSE, @@ -833,6 +873,11 @@ setup_create_3oggle (GtkWidget *tab, int row, const setting *set) wid = gtk_check_button_new (); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wid), setup_get_int3 (&setup_prefs, offsets[2])); + { + char *name = g_strdup_printf ("%s %s", _(set->label), _("Highlighted Message")); + atk_object_set_name (gtk_widget_get_accessible (wid), name); + g_free (name); + } g_signal_connect (G_OBJECT (wid), "toggled", G_CALLBACK (setup_3oggle_cb), ((int *)&setup_prefs) + offsets[2]); setup_table_attach (tab, wid, 5, 6, row, row + 1, FALSE, FALSE, @@ -886,12 +931,15 @@ setup_text_indent_toggled_cb (GtkToggleButton *but, gpointer userdata) gtk_widget_set_sensitive (setup_wrap_align_combo, sens); } -static void -setup_create_toggleR (GtkWidget *tab, int row, const setting *set) +static GtkWidget * +setup_create_toggleL (GtkWidget *tab, int row, const setting *set) { - GtkWidget *wid; + GtkWidget *wid, *label; wid = gtk_check_button_new_with_label (_(set->label)); + label = gtk_bin_get_child (GTK_BIN (wid)); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 60); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wid), setup_get_int (&setup_prefs, set)); g_signal_connect (G_OBJECT (wid), "toggled", @@ -899,22 +947,6 @@ setup_create_toggleR (GtkWidget *tab, int row, const setting *set) if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_text_indent)) g_signal_connect (G_OBJECT (wid), "toggled", G_CALLBACK (setup_text_indent_toggled_cb), NULL); - if (set->tooltip) - gtk_widget_set_tooltip_text (wid, _(set->tooltip)); - setup_table_attach (tab, wid, 4, 5, row, row + 1, TRUE, FALSE, - SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0); -} - -static GtkWidget * -setup_create_toggleL (GtkWidget *tab, int row, const setting *set) -{ - GtkWidget *wid; - - wid = gtk_check_button_new_with_label (_(set->label)); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wid), - setup_get_int (&setup_prefs, set)); - g_signal_connect (G_OBJECT (wid), "toggled", - G_CALLBACK (setup_toggle_cb), (gpointer)set); if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_gui_mode_buttons_inline)) g_signal_connect (G_OBJECT (wid), "toggled", G_CALLBACK (setup_topicbar_inline_toggled_cb), NULL); @@ -925,7 +957,7 @@ setup_create_toggleL (GtkWidget *tab, int row, const setting *set) } if (set->tooltip) gtk_widget_set_tooltip_text (wid, _(set->tooltip)); - setup_table_attach (tab, wid, 2, row==6 ? 6 : 4, row, row + 1, FALSE, FALSE, + setup_table_attach (tab, wid, 2, 6, row, row + 1, FALSE, FALSE, SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, LABEL_INDENT, 0); return wid; @@ -935,12 +967,16 @@ static GtkWidget * setup_create_italic_label (char *text) { GtkWidget *label; - char buf[256]; + char *markup; label = gtk_label_new (NULL); - g_snprintf (buf, sizeof (buf), "%s", text); - gtk_label_set_markup (GTK_LABEL (label), buf); - gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); + markup = g_markup_printf_escaped ("%s", text); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 52); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); return label; } @@ -1033,6 +1069,7 @@ setup_create_spin (GtkWidget *table, int row, const setting *set) wid = gtk_spin_button_new_with_range (0, set->extra, 1); g_object_set_data (G_OBJECT (wid), "lbl", label); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), wid); if (set->tooltip) gtk_widget_set_tooltip_text (wid, _(set->tooltip)); if (setup_is_dcc_speed_setting (set)) @@ -1300,6 +1337,7 @@ setup_create_menu (GtkWidget *table, int row, const setting *set) LABEL_INDENT, 0); cbox = gtk_combo_box_text_new (); + gtk_label_set_mnemonic_widget (GTK_LABEL (wid), cbox); for (i = 0; text[i]; i++) gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (cbox), _(text[i])); @@ -1477,7 +1515,7 @@ static void setup_create_label (GtkWidget *table, int row, const setting *set) { setup_table_attach (table, setup_create_italic_label (_(set->label)), - set->extra ? 1 : 3, 5, row, row + 1, FALSE, FALSE, + 2, 6, row, row + 1, FALSE, FALSE, SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0); } @@ -1496,6 +1534,7 @@ setup_create_entry (GtkWidget *table, int row, const setting *set) wid = gtk_entry_new (); g_object_set_data (G_OBJECT (wid), "lbl", label); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), wid); if (set->list) gtk_entry_set_visibility (GTK_ENTRY (wid), FALSE); if (set->tooltip) @@ -1540,25 +1579,6 @@ setup_create_entry (GtkWidget *table, int row, const setting *set) return wid; } -static void -setup_create_header (GtkWidget *table, int row, char *labeltext) -{ - GtkWidget *label; - char buf[128]; - - if (row == 0) - g_snprintf (buf, sizeof (buf), "%s", _(labeltext)); - else - g_snprintf (buf, sizeof (buf), "\n%s", _(labeltext)); - - label = gtk_label_new (NULL); - gtk_label_set_markup (GTK_LABEL (label), buf); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (table, label, 0, 4, row, row + 1, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, 0, 5); -} - static void setup_create_button (GtkWidget *table, int row, char *label, GCallback callback) { @@ -1574,9 +1594,9 @@ setup_create_frame (void) GtkWidget *tab; tab = gtkutil_grid_new (3, 2, FALSE); - gtk_container_set_border_width (GTK_CONTAINER (tab), 6); - gtk_grid_set_row_spacing (GTK_GRID (tab), 2); - gtk_grid_set_column_spacing (GTK_GRID (tab), 3); + gtk_container_set_border_width (GTK_CONTAINER (tab), 14); + gtk_grid_set_row_spacing (GTK_GRID (tab), 10); + gtk_grid_set_column_spacing (GTK_GRID (tab), 12); return tab; } @@ -1591,20 +1611,31 @@ static GtkWidget * setup_create_page (const setting *set) { int i, row, do_disable; - GtkWidget *tab; + GtkWidget *tab = NULL, *box, *frame, *label; GtkWidget *wid = NULL, *parentwid = NULL; - tab = setup_create_frame (); - gtk_container_set_border_width (GTK_CONTAINER (tab), 6); - + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 18); + gtk_widget_set_valign (box, GTK_ALIGN_START); i = row = do_disable = 0; while (set[i].type != ST_END) { switch (set[i].type) { case ST_HEADER: - setup_create_header (tab, row, set[i].label); + { + char *markup = g_markup_printf_escaped ("%s", _(set[i].label)); + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + gtk_widget_set_margin_start (label, 10); + frame = gtk_frame_new (NULL); + gtk_frame_set_label_widget (GTK_FRAME (frame), label); + tab = setup_create_frame (); + gtk_container_add (GTK_CONTAINER (frame), tab); + gtk_box_pack_start (GTK_BOX (box), frame, FALSE, FALSE, 0); + row = -1; break; + } case ST_EFONT: case ST_ENTRY: case ST_EFILE: @@ -1612,9 +1643,6 @@ setup_create_page (const setting *set) wid = setup_create_entry (tab, row, &set[i]); break; case ST_TOGGLR: - row--; - setup_create_toggleR (tab, row, &set[i]); - break; case ST_TOGGLE: wid = setup_create_toggleL (tab, row, &set[i]); if (set[i].extra) @@ -1627,7 +1655,7 @@ setup_create_page (const setting *set) setup_create_menu (tab, row, &set[i]); break; case ST_RADIO: - row += setup_create_radio (tab, row, &set[i]); + setup_create_radio (tab, row, &set[i]); break; case ST_NUMBER: wid = setup_create_spin (tab, row, &set[i]); @@ -1665,7 +1693,7 @@ setup_create_page (const setting *set) setup_create_button (tab, row, _("Open Data Folder"), G_CALLBACK(open_data_cb)); } - return tab; + return box; } static GtkWidget * @@ -1676,41 +1704,6 @@ setup_create_theme_page (void) &color_change); } -static GtkWidget * -setup_create_appearance_page (void) -{ - GtkWidget *box; - GtkWidget *appearance_page; - GtkWidget *theme_label; - GtkWidget *theme_page; - GtkWidget *advanced_page; - - box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); - appearance_page = setup_create_page (appearance_settings); - theme_label = gtk_label_new (NULL); - theme_page = setup_create_theme_page (); - advanced_page = setup_create_page (appearance_advanced_settings); - - { - char *markup = g_markup_printf_escaped ("%s", _("GTK3 Theme")); - gtk_label_set_markup (GTK_LABEL (theme_label), markup); - g_free (markup); - } - gtk_widget_set_halign (theme_label, GTK_ALIGN_START); - gtk_widget_set_valign (theme_label, GTK_ALIGN_CENTER); - gtk_widget_set_margin_start (theme_label, 2); - gtk_widget_set_margin_end (theme_label, 2); - gtk_widget_set_margin_top (theme_label, 1); - gtk_widget_set_margin_bottom (theme_label, 1); - - gtk_box_pack_start (GTK_BOX (box), appearance_page, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (box), theme_label, FALSE, FALSE, 2); - gtk_box_pack_start (GTK_BOX (box), theme_page, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (box), advanced_page, FALSE, FALSE, 0); - - return box; -} - /* === GLOBALS for sound GUI === */ static GtkWidget *sndfile_entry; @@ -1718,6 +1711,10 @@ static int ignore_changed = FALSE; extern struct text_event te[]; /* text.c */ extern char *sound_files[]; +static char *setup_sound_files[NUM_XP]; +static char *setup_sound_original[NUM_XP]; +static gboolean setup_sound_created; +static gboolean setup_sound_pending; static void setup_snd_populate (GtkTreeView * treeview) @@ -1734,8 +1731,8 @@ setup_snd_populate (GtkTreeView * treeview) for (i = NUM_XP-1; i >= 0; i--) { gtk_list_store_prepend (store, &iter); - if (sound_files[i]) - gtk_list_store_set (store, &iter, 0, te[i].name, 1, sound_files[i], 2, i, -1); + if (setup_sound_files[i]) + gtk_list_store_set (store, &iter, 0, te[i].name, 1, setup_sound_files[i], 2, i, -1); else gtk_list_store_set (store, &iter, 0, te[i].name, 1, "", 2, i, -1); if (i == last_selected_row) @@ -1777,8 +1774,8 @@ setup_snd_row_cb (GtkTreeSelection *sel, gpointer user_data) last_selected_row = n; ignore_changed = TRUE; - if (sound_files[n]) - gtk_entry_set_text (GTK_ENTRY (sndfile_entry), sound_files[n]); + if (setup_sound_files[n]) + gtk_entry_set_text (GTK_ENTRY (sndfile_entry), setup_sound_files[n]); else gtk_entry_set_text (GTK_ENTRY (sndfile_entry), ""); ignore_changed = FALSE; @@ -1808,30 +1805,33 @@ setup_snd_add_columns (GtkTreeView * treeview) } static void -setup_snd_filereq_cb (GtkWidget *entry, char *file) +setup_snd_filereq_cb (GWeakRef *reference, char *file) { - if (file) + GtkWidget *entry; + if (!file) { - if (file[0]) - { - /* Use just the filename if the given sound file is in the default /sounds directory. - * We're comparing absolute paths so this won't work in portable mode which uses a relative path. - */ - if (!strcmp (g_path_get_dirname (file), g_build_filename (get_xdir (), ZOITECHAT_SOUND_DIR, NULL))) - { - gtk_entry_set_text (GTK_ENTRY (entry), g_path_get_basename (file)); - } - else - { - gtk_entry_set_text (GTK_ENTRY (entry), file); - } - } + g_weak_ref_clear (reference); + g_free (reference); + return; } + entry = g_weak_ref_get (reference); + if (entry && entry == sndfile_entry && setup_sound_created && file[0]) + { + char *directory = g_path_get_dirname (file); + char *sounds = g_build_filename (get_xdir (), ZOITECHAT_SOUND_DIR, NULL); + char *basename = g_path_get_basename (file); + gtk_entry_set_text (GTK_ENTRY (entry), !strcmp (directory, sounds) ? basename : file); + g_free (directory); + g_free (sounds); + g_free (basename); + } + if (entry) g_object_unref (entry); } static void setup_snd_browse_cb (GtkWidget *button, GtkEntry *entry) { + GWeakRef *reference = g_new0 (GWeakRef, 1); char *sounds_dir = g_build_filename (get_xdir (), ZOITECHAT_SOUND_DIR, NULL); char *filter = NULL; int filter_type; @@ -1843,7 +1843,8 @@ setup_snd_browse_cb (GtkWidget *button, GtkEntry *entry) filter_type = FRF_MIMETYPES; #endif - gtkutil_file_req (GTK_WINDOW (setup_window), _("Select a sound file"), setup_snd_filereq_cb, entry, + g_weak_ref_init (reference, entry); + gtkutil_file_req (GTK_WINDOW (setup_window), _("Select a sound file"), setup_snd_filereq_cb, reference, sounds_dir, filter, FRF_MODAL|FRF_FILTERISINITIAL|filter_type); g_free (sounds_dir); } @@ -1871,14 +1872,14 @@ setup_snd_changed_cb (GtkEntry *ent, GtkTreeView *tree) return; /* get the new sound file */ - g_free (sound_files[n]); - sound_files[n] = g_strdup (gtk_entry_get_text (GTK_ENTRY (ent))); + g_free (setup_sound_files[n]); + setup_sound_files[n] = g_strdup (gtk_entry_get_text (GTK_ENTRY (ent))); /* update the TreeView list */ store = (GtkListStore *)gtk_tree_view_get_model (tree); - gtk_list_store_set (store, &iter, 1, sound_files[n], -1); + gtk_list_store_set (store, &iter, 1, setup_sound_files[n], -1); + - gtk_widget_set_sensitive (cancel_button, FALSE); } static GtkWidget * @@ -1893,7 +1894,14 @@ setup_create_sound_page (void) GtkWidget *sound_browse; GtkWidget *sound_play; GtkTreeSelection *sel; + int i; + for (i = 0; i < NUM_XP; i++) + { + setup_sound_files[i] = g_strdup (sound_files[i]); + setup_sound_original[i] = g_strdup (sound_files[i]); + } + setup_sound_created = TRUE; vbox1 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); gtk_container_set_border_width (GTK_CONTAINER (vbox1), 6); gtk_widget_show (vbox1); @@ -1965,10 +1973,10 @@ setup_page_from_settings (const setting *settings) } static GtkWidget * -setup_page_from_appearance (const setting *settings) +setup_page_from_theme (const setting *settings) { (void)settings; - return setup_create_appearance_page (); + return setup_create_theme_page (); } static GtkWidget * @@ -1979,271 +1987,482 @@ setup_page_from_sound (const setting *settings) } static void -setup_add_page (const char *title, GtkWidget *book, GtkWidget *tab) +setup_search_append (GString *text, const char *value) { - GtkWidget *label, *vvbox, *viewport; - GtkScrolledWindow *sw; - char buf[128]; - - vvbox = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); - - /* label */ - label = gtk_label_new (NULL); - g_snprintf (buf, sizeof (buf), "%s", _(title)); - gtk_label_set_markup (GTK_LABEL (label), buf); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - gtk_widget_set_margin_start (label, 2); - gtk_widget_set_margin_end (label, 2); - gtk_widget_set_margin_top (label, 1); - gtk_widget_set_margin_bottom (label, 1); - gtk_box_pack_start (GTK_BOX (vvbox), label, FALSE, FALSE, 2); - - gtk_box_pack_start (GTK_BOX (vvbox), tab, TRUE, TRUE, 0); - - sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL)); - gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN); - gtk_scrolled_window_set_policy (sw, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); - gtk_scrolled_window_set_propagate_natural_width (sw, TRUE); - gtk_container_add (GTK_CONTAINER (sw), vvbox); - - viewport = gtk_bin_get_child (GTK_BIN(sw)); - gtk_viewport_set_shadow_type (GTK_VIEWPORT(viewport), GTK_SHADOW_NONE); - - gtk_notebook_append_page (GTK_NOTEBOOK (book), GTK_WIDGET(sw), NULL); -} - -static void -setup_register_page (const char *title, GtkWidget *book, setup_page_factory factory, const setting *settings) -{ - GtkWidget *container; - - if (setup_page_count >= SETUP_MAX_PAGES) - return; - - setup_page_factories[setup_page_count] = factory; - setup_page_settings[setup_page_count] = settings; - container = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); - setup_page_containers[setup_page_count] = container; - setup_add_page (title, book, container); - setup_page_count++; -} - -static void -setup_ensure_page_created (int page) -{ - GList *children; - GtkWidget *page_widget; - GtkWidget *container; - - if (page < 0 || page >= setup_page_count) - return; - - container = setup_page_containers[page]; - children = gtk_container_get_children (GTK_CONTAINER (container)); - if (children != NULL) + if (value) { - g_list_free (children); - return; + g_string_append_c (text, ' '); + g_string_append (text, _(value)); } - - page_widget = setup_page_factories[page] (setup_page_settings[page]); - /* Lazy pages must pass the notebook's spare space to their content, - particularly the independently scrolling Sounds list. */ - gtk_box_pack_start (GTK_BOX (container), page_widget, TRUE, TRUE, 0); - gtk_widget_show_all (container); } -static const char *const cata_interface[] = +static char * +setup_search_normalize (const char *text) { - N_("Appearance"), - N_("Input box"), - N_("User list"), - N_("Channel switcher"), - NULL -}; + char *folded = g_utf8_casefold (text, -1); + char *normalized = g_utf8_normalize (folded, -1, G_NORMALIZE_ALL_COMPOSE); + g_free (folded); + return normalized; +} -static const char *const cata_chatting[] = +static void +setup_register_page (const char *id, const char *group, const char *title, const char *description, + setup_page_factory factory, const setting *settings, + const char *keywords) { - N_("General"), - N_("Alerts"), - N_("Sounds"), - N_("Logging"), - N_("Advanced"), - NULL -}; + setup_page page = {0}; + GtkWidget *box, *label, *sw; + GString *text = g_string_new (""); + char *markup; + int i, j; -static const char *const cata_network[] = + page.id = id; + page.group = group; + page.title = title; + page.description = description; + page.factory = factory; + page.settings = settings; + setup_search_append (text, group); + setup_search_append (text, title); + setup_search_append (text, description); + setup_search_append (text, keywords); + for (i = 0; settings && settings[i].type != ST_END; i++) + { + setup_search_append (text, settings[i].label); + setup_search_append (text, settings[i].tooltip); + if (settings[i].type == ST_MENU || settings[i].type == ST_RADIO) + for (j = 0; settings[i].list && settings[i].list[j]; j++) + setup_search_append (text, settings[i].list[j]); + } + page.search_text = setup_search_normalize (text->str); + g_string_free (text, TRUE); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + gtk_container_set_border_width (GTK_CONTAINER (box), 20); + label = gtk_label_new (NULL); + markup = g_markup_printf_escaped ("%s", _(title)); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); + + label = gtk_label_new (_(description)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 56); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); + page.container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_top (page.container, 8); + gtk_box_pack_start (GTK_BOX (box), page.container, TRUE, TRUE, 0); + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (sw), box); + gtk_notebook_append_page (GTK_NOTEBOOK (setup_book), sw, NULL); + g_array_append_val (setup_pages, page); +} + +static void +setup_ensure_page_created (int index) { - N_("Network setup"), - N_("File transfers"), - N_("Identd"), - NULL -}; + setup_page *page; + GtkWidget *widget; + if (index < 0 || index >= (int)setup_pages->len) + return; + page = &g_array_index (setup_pages, setup_page, index); + if (g_object_get_data (G_OBJECT (page->container), "setup-created")) + return; + widget = page->factory (page->settings); + page->body = widget; + gtk_box_pack_start (GTK_BOX (page->container), widget, TRUE, TRUE, 0); + g_object_set_data (G_OBJECT (page->container), "setup-created", GINT_TO_POINTER (1)); + gtk_widget_show_all (page->container); +} + +static GtkWidget * +setup_page_from_keyboard (const setting *settings) +{ + (void)settings; + return key_preferences_create (); +} + +static GtkWidget * +setup_page_from_events (const setting *settings) +{ + (void)settings; + return pevent_preferences_create (); +} + +static GtkWidget * +setup_page_from_replace (const setting *settings) +{ + (void)settings; + return menu_preferences_create ("replace"); +} + +static GtkWidget * +setup_page_from_menus (const setting *settings) +{ + (void)settings; + return menu_preferences_create ("menus"); +} + +static GtkWidget * +setup_page_from_commands (const setting *settings) +{ + (void)settings; + return menu_preferences_create ("commands"); +} + +static GtkWidget * +setup_page_from_urls (const setting *settings) +{ + (void)settings; + return menu_preferences_create ("urls"); +} + +static GtkWidget * +setup_page_from_ctcp (const setting *settings) +{ + (void)settings; + return menu_preferences_create ("ctcp"); +} + +static GtkWidget * +setup_page_from_ignore (const setting *settings) +{ + (void)settings; + return ignore_preferences_create (); +} + +#ifdef USE_PLUGIN +static GtkWidget * +setup_page_from_plugins (const setting *settings) +{ + (void)settings; + return plugingui_preferences_create (); +} +#endif static GtkWidget * setup_create_pages (GtkWidget *box) { - GtkWidget *book; - GtkWindow *win = GTK_WINDOW (setup_window); + const setting *alerts; + gboolean tray = gtkutil_tray_icon_supported (GTK_WINDOW (setup_window)); + gboolean notifications = notification_backend_supported (); - (void)box; - book = gtk_notebook_new (); + setup_pages = g_array_new (FALSE, TRUE, sizeof (setup_page)); + setup_book = gtk_notebook_new (); + gtk_notebook_set_show_tabs (GTK_NOTEBOOK (setup_book), FALSE); + gtk_notebook_set_show_border (GTK_NOTEBOOK (setup_book), FALSE); + gtk_box_pack_start (GTK_BOX (box), setup_book, TRUE, TRUE, 0); - memset (setup_page_factories, 0, sizeof (setup_page_factories)); - memset (setup_page_settings, 0, sizeof (setup_page_settings)); - memset (setup_page_containers, 0, sizeof (setup_page_containers)); - setup_page_count = 0; + setup_register_page ("text", N_("Interface"), N_("Text & layout"), + N_("Fonts, conversation text, timestamps and window information."), + setup_page_from_settings, appearance_settings, NULL); + setup_register_page ("appearance", N_("Interface"), N_("Appearance"), + N_("Choose a look, preview your conversations and make colours your own."), + setup_page_from_theme, NULL, N_("Themes appearance conversation colors colours palette GTK3 import remove colors.conf dark light contrast nickname text background foreground hex reset")); + setup_register_page ("effects", N_("Interface"), N_("Effects & scrolling"), + N_("Background images, window opacity and mouse wheel behavior."), + setup_page_from_settings, appearance_advanced_settings, NULL); + setup_register_page ("typing", N_("Interface"), N_("Typing & spelling"), + N_("The message input, spell checking and nickname completion."), + setup_page_from_settings, inputbox_settings, NULL); + setup_register_page ("user-list", N_("Interface"), N_("User list"), + N_("How people are displayed and sorted in a channel."), + setup_page_from_settings, userlist_settings, NULL); + setup_register_page ("tabs", N_("Interface"), N_("Channels & tabs"), + N_("Channel navigation, tab placement and activity indicators."), + setup_page_from_settings, tabs_settings, NULL); - setup_register_page (cata_interface[0], book, setup_page_from_appearance, NULL); - setup_register_page (cata_interface[1], book, setup_page_from_settings, inputbox_settings); - setup_register_page (cata_interface[2], book, setup_page_from_settings, userlist_settings); - setup_register_page (cata_interface[3], book, setup_page_from_settings, tabs_settings); + setup_register_page ("keyboard", N_("Interface"), N_("Keyboard shortcuts"), + N_("Customize shortcuts and commands. Changes take effect when you save."), + setup_page_from_keyboard, NULL, N_("Key accelerator action data reset add delete binding")); + setup_register_page ("menus", N_("Interface"), N_("Menus & buttons"), + N_("Customize user list buttons, context menus, dialog buttons and the user menu."), + setup_page_from_menus, NULL, N_("Popup command add delete reorder formatting")); - setup_register_page (cata_chatting[0], book, setup_page_from_settings, general_settings); + setup_register_page ("messages", N_("Chatting"), N_("Messages & away"), + N_("Default messages, away behavior and channel events."), + setup_page_from_settings, general_settings, NULL); + alerts = tray ? (notifications ? alert_settings : alert_settings_nonotifications) + : (notifications ? alert_settings_unity : alert_settings_unityandnonotifications); + setup_register_page ("replace", N_("Chatting"), N_("Text replacements"), + N_("Automatically replace words as you type. Add, edit or reorder your replacements."), + setup_page_from_replace, NULL, N_("Auto replace text expansion")); + setup_register_page ("events", N_("Chatting"), N_("Message formatting"), + N_("Edit text event templates with argument help, import/export and a preview."), + setup_page_from_events, NULL, N_("Text events format message join part quit highlight load save test")); + setup_register_page ("notifications", N_("Chatting"), N_("Notifications"), + N_("Highlights, desktop alerts and system tray behavior."), + setup_page_from_settings, alerts, NULL); + setup_register_page ("sounds", N_("Chatting"), N_("Sounds"), + N_("Assign and preview a sound for each chat event."), + setup_page_from_sound, NULL, N_("Sound file browse play")); + setup_register_page ("logging", N_("Chatting"), N_("History & logging"), + N_("Saved conversations, scrollback and collected links."), + setup_page_from_settings, logging_settings, NULL); + setup_register_page ("advanced", N_("Chatting"), N_("Behavior & timing"), + N_("Clipboard behavior, display options and connection timing."), + setup_page_from_settings, advanced_settings, NULL); - if (!gtkutil_tray_icon_supported (win) && !notification_backend_supported ()) - { - setup_register_page (cata_chatting[1], book, setup_page_from_settings, alert_settings_unityandnonotifications); - } - else if (!gtkutil_tray_icon_supported (win)) - { - setup_register_page (cata_chatting[1], book, setup_page_from_settings, alert_settings_unity); - } - else if (!notification_backend_supported ()) - { - setup_register_page (cata_chatting[1], book, setup_page_from_settings, alert_settings_nonotifications); - } - else - { - setup_register_page (cata_chatting[1], book, setup_page_from_settings, alert_settings); - } - - setup_register_page (cata_chatting[2], book, setup_page_from_sound, NULL); - setup_register_page (cata_chatting[3], book, setup_page_from_settings, logging_settings); - setup_register_page (cata_chatting[4], book, setup_page_from_settings, advanced_settings); - - setup_register_page (cata_network[0], book, setup_page_from_settings, network_settings); - setup_register_page (cata_network[1], book, setup_page_from_settings, filexfer_settings); - setup_register_page (cata_network[2], book, setup_page_from_settings, identd_settings); - - gtk_notebook_set_show_tabs (GTK_NOTEBOOK (book), FALSE); - gtk_notebook_set_show_border (GTK_NOTEBOOK (book), FALSE); - gtk_box_pack_start (GTK_BOX (box), book, TRUE, TRUE, 0); - - return book; + setup_register_page ("connection", N_("Network"), N_("Connection"), + N_("Your address, proxy, connection health and remote media."), + setup_page_from_settings, network_settings, NULL); + setup_register_page ("transfers", N_("Network"), N_("File transfers"), + N_("Download folders, incoming files and transfer speed limits."), + setup_page_from_settings, filexfer_settings, NULL); + setup_register_page ("identd", N_("Network"), N_("Identd"), + N_("The optional identification server and its listening port."), + setup_page_from_settings, identd_settings, NULL); + setup_register_page ("ignore", N_("Privacy"), N_("Ignore rules"), + N_("Manage ignored masks and message types. Ignore changes apply immediately."), + setup_page_from_ignore, NULL, N_("Ignore list mask private channel CTCP DCC notice invite stats")); + setup_register_page ("commands", N_("Advanced"), N_("Commands & aliases"), + N_("Define your own commands and expansions using the documented formatting codes."), + setup_page_from_commands, NULL, N_("User commands name expansion special codes")); + setup_register_page ("urls", N_("Advanced"), N_("Link handling"), + N_("Choose commands and applications used to open links."), + setup_page_from_urls, NULL, N_("URL handlers shell browser command")); + setup_register_page ("ctcp", N_("Advanced"), N_("CTCP replies"), + N_("Customize replies to client-to-client requests."), + setup_page_from_ctcp, NULL, N_("CTCP VERSION PING TIME reply request")); +#ifdef USE_PLUGIN + setup_register_page ("plugins", N_("Extensions"), N_("Plugins & scripts"), + N_("Load, unload and reload extensions. These actions take effect immediately."), + setup_page_from_plugins, NULL, N_("Plugins scripts addons Python Perl Lua load unload reload")); +#endif + return setup_book; } static void -setup_tree_cb (GtkTreeView *treeview, GtkWidget *book) +setup_scroll_selected_row (GtkWidget *widget, gpointer data) { - GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview); - GtkTreeIter iter; - GtkTreeModel *model; - int page; + GtkListBoxRow *row; + GtkWidget *scroll; + GtkAdjustment *adjustment; + int x, y; + (void)widget; + (void)data; + if (!setup_sidebar || !gtk_widget_get_mapped (setup_sidebar)) return; + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (setup_sidebar)); + if (!row || !gtk_widget_get_child_visible (GTK_WIDGET (row))) return; + scroll = gtk_widget_get_ancestor (setup_sidebar, GTK_TYPE_SCROLLED_WINDOW); + if (!scroll || !gtk_widget_translate_coordinates (GTK_WIDGET (row), setup_sidebar, 0, 0, &x, &y)) return; + adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scroll)); + gtk_adjustment_clamp_page (adjustment, y, y + gtk_widget_get_allocated_height (GTK_WIDGET (row))); +} - if (gtk_tree_selection_get_selected (selection, &model, &iter)) +static void +setup_sidebar_size_allocate (GtkWidget *widget, GtkAllocation *allocation, gpointer data) +{ + setup_scroll_selected_row (widget, data); +} + +static void +setup_sidebar_selected (GtkListBox *list, GtkListBoxRow *row, gpointer data) +{ + int page; + (void)list; + (void)data; + if (!row) + return; + page = gtk_list_box_row_get_index (row); + setup_ensure_page_created (page); + gtk_notebook_set_current_page (GTK_NOTEBOOK (setup_book), page); + setup_scroll_selected_row (GTK_WIDGET (list), NULL); +} + +void +setup_reveal_widget (GtkWidget *widget) +{ + guint i; + GtkWidget *child, *parent; + if (!setup_pages || !widget) return; + for (i = 0; i < setup_pages->len; i++) { - gtk_tree_model_get (model, &iter, 1, &page, -1); - if (page != -1) + setup_page *page = &g_array_index (setup_pages, setup_page, i); + if (widget == page->container || gtk_widget_is_ancestor (widget, page->container)) { - setup_ensure_page_created (page); - gtk_notebook_set_current_page (GTK_NOTEBOOK (book), page); - last_selected_page = page; + gtk_entry_set_text (GTK_ENTRY (setup_search), ""); + gtk_list_box_select_row (GTK_LIST_BOX (setup_sidebar), GTK_LIST_BOX_ROW (page->row)); + for (child = widget; (parent = gtk_widget_get_parent (child)); child = parent) + { + if (GTK_IS_STACK (parent)) + { + GtkWidget *selector = g_object_get_data (G_OBJECT (parent), "preferences-selector"); + if (selector) + { + char *name; + gtk_container_child_get (GTK_CONTAINER (parent), child, "name", &name, NULL); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (selector), name); + g_free (name); + } + else gtk_stack_set_visible_child (GTK_STACK (parent), child); + } + } + setup_scroll_selected_row (setup_sidebar, NULL); + return; } } } static gboolean -setup_tree_select_filter (GtkTreeSelection *selection, GtkTreeModel *model, - GtkTreePath *path, gboolean path_selected, - gpointer data) +setup_sidebar_filter (GtkListBoxRow *row, gpointer data) { - if (gtk_tree_path_get_depth (path) > 1) + setup_page *page = &g_array_index (setup_pages, setup_page, gtk_list_box_row_get_index (row)); + char **words; + gboolean matches = TRUE; + int i; + (void)data; + if (!setup_query || !*setup_query) return TRUE; + words = g_strsplit_set (setup_query, " \t\r\n", -1); + for (i = 0; words[i]; i++) + if (*words[i] && !strstr (page->search_text, words[i])) + matches = FALSE; + g_strfreev (words); + return matches; +} + +static void +setup_sidebar_header (GtkListBoxRow *row, GtkListBoxRow *before, gpointer data) +{ + setup_page *page = &g_array_index (setup_pages, setup_page, gtk_list_box_row_get_index (row)); + setup_page *previous = before ? &g_array_index (setup_pages, setup_page, gtk_list_box_row_get_index (before)) : NULL; + GtkWidget *label; + char *markup; + (void)data; + gtk_list_box_row_set_header (row, NULL); + if (previous && !strcmp (page->group, previous->group)) + return; + label = gtk_label_new (NULL); + markup = g_markup_printf_escaped ("%s", _(page->group)); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_widget_set_margin_start (label, 12); + gtk_widget_set_margin_top (label, before ? 18 : 10); + gtk_widget_set_margin_bottom (label, 6); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_widget_show (label); + gtk_list_box_row_set_header (row, label); +} + +static void +setup_search_changed (GtkEditable *entry, gpointer data) +{ + GtkListBoxRow *selected; + GtkListBoxRow *first = NULL; + guint i; + (void)data; + g_free (setup_query); + setup_query = setup_search_normalize (gtk_entry_get_text (GTK_ENTRY (entry))); + gtk_list_box_invalidate_filter (GTK_LIST_BOX (setup_sidebar)); + selected = gtk_list_box_get_selected_row (GTK_LIST_BOX (setup_sidebar)); + for (i = 0; i < setup_pages->len; i++) + { + GtkListBoxRow *row = GTK_LIST_BOX_ROW (g_array_index (setup_pages, setup_page, i).row); + if (setup_sidebar_filter (row, NULL) && !first) + first = row; + } + gtk_widget_set_visible (setup_book, first != NULL); + gtk_widget_set_visible (setup_empty, first == NULL); + if (first && (!selected || !setup_sidebar_filter (selected, NULL))) + gtk_list_box_select_row (GTK_LIST_BOX (setup_sidebar), first); +} + +static void +setup_search_activate (GtkEntry *entry, gpointer data) +{ + GtkListBoxRow *row = gtk_list_box_get_selected_row (GTK_LIST_BOX (setup_sidebar)); + (void)entry; + (void)data; + if (row && setup_sidebar_filter (row, NULL)) + gtk_widget_child_focus (setup_book, GTK_DIR_TAB_FORWARD); +} + +static void +setup_search_clear (GtkButton *button, gpointer data) +{ + (void)button; + (void)data; + gtk_entry_set_text (GTK_ENTRY (setup_search), ""); + gtk_widget_grab_focus (setup_search); +} + +static gboolean +setup_key_press (GtkWidget *win, GdkEventKey *event, gpointer data) +{ + (void)data; + if ((event->state & GDK_CONTROL_MASK) && + (event->keyval == GDK_KEY_f || event->keyval == GDK_KEY_F)) + { + gtk_widget_grab_focus (setup_search); + gtk_editable_select_region (GTK_EDITABLE (setup_search), 0, -1); + return TRUE; + } + if (event->keyval == GDK_KEY_Escape) + { + if (*gtk_entry_get_text (GTK_ENTRY (setup_search))) + setup_search_clear (NULL, NULL); + else if (gtk_widget_get_sensitive (cancel_button)) + gtk_widget_destroy (win); + return TRUE; + } return FALSE; } static void -setup_create_tree (GtkWidget *box, GtkWidget *book) +setup_create_sidebar (GtkWidget *box) { - GtkWidget *tree; - GtkWidget *frame; - GtkTreeStore *model; - GtkTreeIter iter; - GtkTreeIter child_iter; - GtkTreeIter *sel_iter = NULL; - GtkCellRenderer *renderer; - GtkTreeSelection *sel; - int i, page; - - model = gtk_tree_store_new (2, G_TYPE_STRING, G_TYPE_INT); - - page = 0; - - gtk_tree_store_append (model, &iter, NULL); - gtk_tree_store_set (model, &iter, 0, _("Interface"), 1, -1, -1); - for (i = 0; cata_interface[i]; i++) + GtkWidget *sidebar, *sw, *label, *separator; + guint i; + sidebar = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width (GTK_CONTAINER (sidebar), 10); + gtk_widget_set_size_request (sidebar, 220, -1); + setup_search = gtk_search_entry_new (); + gtk_entry_set_placeholder_text (GTK_ENTRY (setup_search), _("Search settings")); + gtk_widget_set_tooltip_text (setup_search, _("Search page names, settings and help text. Ctrl+F to search.")); + atk_object_set_name (gtk_widget_get_accessible (setup_search), _("Search settings")); + gtk_box_pack_start (GTK_BOX (sidebar), setup_search, FALSE, FALSE, 0); + setup_sidebar = gtk_list_box_new (); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (setup_sidebar), GTK_SELECTION_BROWSE); + gtk_list_box_set_header_func (GTK_LIST_BOX (setup_sidebar), setup_sidebar_header, NULL, NULL); + gtk_list_box_set_filter_func (GTK_LIST_BOX (setup_sidebar), setup_sidebar_filter, NULL, NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (setup_sidebar), "sidebar"); + atk_object_set_name (gtk_widget_get_accessible (setup_sidebar), _("Preferences pages")); + for (i = 0; i < setup_pages->len; i++) { - gtk_tree_store_append (model, &child_iter, &iter); - gtk_tree_store_set (model, &child_iter, 0, _(cata_interface[i]), 1, page, -1); - if (page == last_selected_page) - sel_iter = gtk_tree_iter_copy (&child_iter); - page++; - } - - gtk_tree_store_append (model, &iter, NULL); - gtk_tree_store_set (model, &iter, 0, _("Chatting"), 1, -1, -1); - for (i = 0; cata_chatting[i]; i++) - { - gtk_tree_store_append (model, &child_iter, &iter); - gtk_tree_store_set (model, &child_iter, 0, _(cata_chatting[i]), 1, page, -1); - if (page == last_selected_page) - sel_iter = gtk_tree_iter_copy (&child_iter); - page++; - } - - gtk_tree_store_append (model, &iter, NULL); - gtk_tree_store_set (model, &iter, 0, _("Network"), 1, -1, -1); - for (i = 0; cata_network[i]; i++) - { - gtk_tree_store_append (model, &child_iter, &iter); - gtk_tree_store_set (model, &child_iter, 0, _(cata_network[i]), 1, page, -1); - if (page == last_selected_page) - sel_iter = gtk_tree_iter_copy (&child_iter); - page++; - } - - tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model)); - g_object_unref (G_OBJECT (model)); - sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree)); - gtk_tree_selection_set_mode (sel, GTK_SELECTION_BROWSE); - gtk_tree_selection_set_select_function (sel, setup_tree_select_filter, - NULL, NULL); - g_signal_connect (G_OBJECT (tree), "cursor-changed", - G_CALLBACK (setup_tree_cb), book); - - renderer = gtk_cell_renderer_text_new (); - gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree), - -1, _("Categories"), renderer, "text", 0, NULL); - gtk_tree_view_expand_all (GTK_TREE_VIEW (tree)); - - frame = gtk_scrolled_window_new (NULL, NULL); - gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (frame), - GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); - gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (frame), GTK_SHADOW_IN); - gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (frame), TRUE); - gtk_widget_set_vexpand (frame, TRUE); - gtk_container_add (GTK_CONTAINER (frame), tree); - gtk_box_pack_start (GTK_BOX (box), frame, FALSE, TRUE, 0); - gtk_box_reorder_child (GTK_BOX (box), frame, 0); - - if (sel_iter) - { - gtk_tree_selection_select_iter (sel, sel_iter); - gtk_tree_iter_free (sel_iter); - setup_tree_cb (GTK_TREE_VIEW (tree), book); + setup_page *page = &g_array_index (setup_pages, setup_page, i); + page->row = gtk_list_box_row_new (); + label = gtk_label_new (_(page->title)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_widget_set_margin_start (label, 12); + gtk_widget_set_margin_end (label, 12); + gtk_widget_set_margin_top (label, 9); + gtk_widget_set_margin_bottom (label, 9); + gtk_container_add (GTK_CONTAINER (page->row), label); + gtk_container_add (GTK_CONTAINER (setup_sidebar), page->row); } + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (sw), setup_sidebar); + gtk_box_pack_start (GTK_BOX (sidebar), sw, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (box), sidebar, FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (box), sidebar, 0); + separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL); + gtk_box_pack_start (GTK_BOX (box), separator, FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (box), separator, 1); + g_signal_connect (setup_sidebar, "row-selected", G_CALLBACK (setup_sidebar_selected), NULL); + g_signal_connect (setup_sidebar, "map", G_CALLBACK (setup_scroll_selected_row), NULL); + g_signal_connect (setup_sidebar, "size-allocate", G_CALLBACK (setup_sidebar_size_allocate), NULL); + g_signal_connect (setup_search, "changed", G_CALLBACK (setup_search_changed), NULL); + g_signal_connect (setup_search, "activate", G_CALLBACK (setup_search_activate), NULL); + gtk_list_box_select_row (GTK_LIST_BOX (setup_sidebar), + gtk_list_box_get_row_at_index (GTK_LIST_BOX (setup_sidebar), 0)); } static void @@ -2472,12 +2691,120 @@ setup_apply (struct zoitechatprefs *pr) #endif } +static gboolean +setup_sounds_stage (GError **error) +{ + GString *text; + int i; + setup_sound_pending = FALSE; + if (!setup_sound_created) return TRUE; + text = g_string_new (""); + for (i = 0; i < NUM_XP; i++) + { + if (setup_sound_files[i] && strpbrk (setup_sound_files[i], "\r\n")) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Sound filenames must stay on one line.")); + g_string_free (text, TRUE); + return FALSE; + } + if (g_strcmp0 (setup_sound_files[i], setup_sound_original[i]) != 0) setup_sound_pending = TRUE; + if (setup_sound_files[i] && *setup_sound_files[i]) + g_string_append_printf (text, "event=%s\nsound=%s\n\n", te[i].name, setup_sound_files[i]); + } + if (setup_sound_pending) + { + for (i = 0; i < NUM_XP; i++) + if (g_strcmp0 (sound_files[i], setup_sound_original[i]) != 0) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_AGAIN, _("Sounds changed outside Preferences. Reopen Preferences before editing them.")); + g_string_free (text, TRUE); + return FALSE; + } + preferences_persistence_stage_file ("sound.conf", text->str); + } + g_string_free (text, TRUE); + return TRUE; +} + +/* Cell editors can still be active when Save is activated from the keyboard. + * Finish the GtkCellEditable before serializing any list model. */ +static void +setup_finish_cell_edits (GtkWidget *widget) +{ + GList *children, *entry; + if (!GTK_IS_CONTAINER (widget)) return; + children = gtk_container_get_children (GTK_CONTAINER (widget)); + for (entry = children; entry; entry = entry->next) + { + GtkWidget *child = entry->data; + if (GTK_IS_TREE_VIEW (widget) && GTK_IS_CELL_EDITABLE (child)) + { + g_object_ref (child); + gtk_cell_editable_editing_done (GTK_CELL_EDITABLE (child)); + gtk_cell_editable_remove_widget (GTK_CELL_EDITABLE (child)); + g_object_unref (child); + } + else setup_finish_cell_edits (child); + } + g_list_free (children); +} + +static void +setup_show_error (const char *message) +{ + gtk_label_set_text (GTK_LABEL (setup_error_label), message); + gtk_widget_show (setup_error_bar); +} + +static void +setup_error_dismiss (GtkInfoBar *bar, int response, gpointer data) +{ + gtk_widget_hide (GTK_WIDGET (bar)); +} + static void setup_ok_cb (GtkWidget *but, GtkWidget *win) { PreferencesPersistenceResult save_result; struct zoitechatprefs old_prefs; char buffer[192]; + GError *error = NULL; + guint page_index; + + gtk_widget_hide (setup_error_bar); + setup_finish_cell_edits (win); + for (page_index = 0; page_index < setup_pages->len; page_index++) + { + setup_page *page = &g_array_index (setup_pages, setup_page, page_index); + if (!strcmp (page->id, "appearance") && page->body && + !theme_preferences_color_editor_stage (page->body, &error)) + { + setup_show_error (error->message); + g_clear_error (&error); + return; + } + } + + /* Move focus out of cell editors before serializing their models. */ + gtk_widget_grab_focus (but ? but : cancel_button); + preferences_persistence_clear_staged_files (); + if (!editlist_preferences_stage (&error) || + !key_preferences_stage (&error) || !pevent_preferences_stage (&error) || !setup_sounds_stage (&error)) + { + setup_show_error (error ? error->message : _("Could not validate preferences.")); + g_clear_error (&error); + return; + } + + /* Network List owns identity and list state independently of this window. */ + g_strlcpy (setup_prefs.hex_irc_nick1, prefs.hex_irc_nick1, sizeof setup_prefs.hex_irc_nick1); + g_strlcpy (setup_prefs.hex_irc_nick2, prefs.hex_irc_nick2, sizeof setup_prefs.hex_irc_nick2); + g_strlcpy (setup_prefs.hex_irc_nick3, prefs.hex_irc_nick3, sizeof setup_prefs.hex_irc_nick3); + g_strlcpy (setup_prefs.hex_irc_user_name, prefs.hex_irc_user_name, sizeof setup_prefs.hex_irc_user_name); + g_strlcpy (setup_prefs.hex_irc_real_name, prefs.hex_irc_real_name, sizeof setup_prefs.hex_irc_real_name); + setup_prefs.hex_gui_slist_select = prefs.hex_gui_slist_select; + setup_prefs.hex_gui_slist_fav = prefs.hex_gui_slist_fav; + setup_prefs.hex_gui_slist_skip = prefs.hex_gui_slist_skip; memcpy (&old_prefs, &prefs, sizeof (prefs)); setup_apply (&setup_prefs); @@ -2485,6 +2812,16 @@ setup_ok_cb (GtkWidget *but, GtkWidget *win) save_result = preferences_persistence_save_all (); if (save_result.success) { + int i; + editlist_preferences_commit (); + key_preferences_commit (); + pevent_preferences_commit (); + if (setup_sound_pending) + for (i = 0; i < NUM_XP; i++) + { + g_free (sound_files[i]); + sound_files[i] = g_strdup (setup_sound_files[i]); + } theme_preferences_stage_commit (); /* the previewed values were accepted, so the close handler must not revert them */ @@ -2493,42 +2830,84 @@ setup_ok_cb (GtkWidget *but, GtkWidget *win) return; } - memcpy (&prefs, &old_prefs, sizeof (prefs)); - theme_preferences_stage_discard (); + setup_apply (&old_prefs); + theme_preferences_stage_revert_runtime (); if (save_result.partial_failure) { - fe_message (_("Preferences were partially saved. zoitechat.conf succeeded, colors.conf failed. Retry is possible."), FE_MSG_ERROR); + setup_show_error (_("Some preference files could not be restored after a save failure. Recovery copies remain in the configuration folder. Retry is possible.")); return; } g_snprintf (buffer, sizeof (buffer), _("Could not save preferences (%s). Retry is possible."), save_result.failed_file ? save_result.failed_file : _("unknown file")); - fe_message (buffer, FE_MSG_ERROR); + setup_show_error (buffer); } static GtkWidget * setup_window_open (void) { GtkWidget *wid, *win, *vbox, *hbox, *hbbox; + GdkDisplay *display; + GdkMonitor *monitor; char buf[128]; g_snprintf(buf, sizeof(buf), _("Preferences - %s"), _(DISPLAY_NAME)); - win = gtkutil_window_new (buf, "prefs", 0, 600, 2); + win = gtkutil_window_new (buf, "prefs", 940, 700, 2); setup_window = win; + display = gtk_widget_get_display (win); + monitor = parent_window && gtk_widget_get_window (parent_window) + ? gdk_display_get_monitor_at_window (display, gtk_widget_get_window (parent_window)) + : gdk_display_get_primary_monitor (display); + if (!monitor) + monitor = gdk_display_get_monitor (display, 0); + if (monitor) + { + GdkRectangle area; + gdk_monitor_get_workarea (monitor, &area); + gtk_window_set_default_size (GTK_WINDOW (win), + MIN (940, MAX (1, area.width - 48)), + MIN (700, MAX (1, area.height - 48))); + } vbox = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 5); - gtk_container_set_border_width (GTK_CONTAINER (vbox), 6); + gtk_container_set_border_width (GTK_CONTAINER (vbox), 0); gtk_container_add (GTK_CONTAINER (win), vbox); hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 4); gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0); - setup_create_tree (hbox, setup_create_pages (hbox)); + setup_create_pages (hbox); + setup_empty = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign (setup_empty, GTK_ALIGN_CENTER); + gtk_widget_set_valign (setup_empty, GTK_ALIGN_CENTER); + gtk_box_pack_start (GTK_BOX (setup_empty), gtk_label_new (_("No matching settings")), FALSE, FALSE, 0); + wid = gtk_button_new_with_mnemonic (_("_Clear search")); + g_signal_connect (wid, "clicked", G_CALLBACK (setup_search_clear), NULL); + gtk_box_pack_start (GTK_BOX (setup_empty), wid, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox), setup_empty, TRUE, TRUE, 0); + setup_create_sidebar (hbox); + g_signal_connect (win, "key-press-event", G_CALLBACK (setup_key_press), NULL); + + setup_error_bar = gtk_info_bar_new (); + gtk_info_bar_set_message_type (GTK_INFO_BAR (setup_error_bar), GTK_MESSAGE_ERROR); + gtk_info_bar_set_show_close_button (GTK_INFO_BAR (setup_error_bar), TRUE); + setup_error_label = gtk_label_new (NULL); + gtk_label_set_xalign (GTK_LABEL (setup_error_label), 0); + gtk_label_set_line_wrap (GTK_LABEL (setup_error_label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (setup_error_label), 72); + gtk_label_set_selectable (GTK_LABEL (setup_error_label), TRUE); + gtk_widget_set_hexpand (setup_error_label, TRUE); + gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (setup_error_bar))), setup_error_label); + g_signal_connect (setup_error_bar, "response", G_CALLBACK (setup_error_dismiss), NULL); + gtk_box_pack_start (GTK_BOX (vbox), setup_error_bar, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (vbox), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0); /* prepare the button box */ hbbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); gtk_button_box_set_layout (GTK_BUTTON_BOX (hbbox), GTK_BUTTONBOX_END); - gtk_box_set_spacing (GTK_BOX (hbbox), 4); + gtk_box_set_spacing (GTK_BOX (hbbox), 8); + gtk_container_set_border_width (GTK_CONTAINER (hbbox), 12); gtk_box_pack_end (GTK_BOX (vbox), hbbox, FALSE, FALSE, 0); cancel_button = wid = gtkutil_button_new_from_stock ("gtk-cancel", _("_Cancel")); @@ -2536,12 +2915,15 @@ setup_window_open (void) G_CALLBACK (gtkutil_destroy), win); gtk_box_pack_start (GTK_BOX (hbbox), wid, FALSE, FALSE, 0); - wid = gtkutil_button_new_from_stock ("gtk-ok", _("_OK")); + wid = gtkutil_button_new_from_stock ("gtk-ok", _("_Save")); + gtk_style_context_add_class (gtk_widget_get_style_context (wid), "suggested-action"); g_signal_connect (G_OBJECT (wid), "clicked", G_CALLBACK (setup_ok_cb), win); gtk_box_pack_start (GTK_BOX (hbbox), wid, FALSE, FALSE, 0); gtk_widget_show_all (win); + gtk_widget_hide (setup_empty); + gtk_widget_hide (setup_error_bar); return win; } @@ -2549,7 +2931,25 @@ setup_window_open (void) static void setup_close_cb (GtkWidget *win, GtkWidget **swin) { + guint i; *swin = NULL; + for (i = 0; setup_pages && i < setup_pages->len; i++) + g_free (g_array_index (setup_pages, setup_page, i).search_text); + if (setup_pages) + g_array_free (setup_pages, TRUE); + setup_pages = NULL; + g_clear_pointer (&setup_query, g_free); + setup_book = setup_sidebar = setup_search = setup_empty = NULL; + setup_error_bar = setup_error_label = NULL; + setup_topicbar_multiline_toggle = NULL; + for (i = 0; i < NUM_XP; i++) + { + g_clear_pointer (&setup_sound_files[i], g_free); + g_clear_pointer (&setup_sound_original[i], g_free); + } + setup_sound_created = setup_sound_pending = FALSE; + sndfile_entry = NULL; + preferences_persistence_clear_staged_files (); /* drop any pending transparency preview and undo the previewed opacity when the dialog is cancelled; after OK the snapshot @@ -2597,3 +2997,31 @@ setup_open (void) g_signal_connect (G_OBJECT (setup_window), "destroy", G_CALLBACK (setup_close_cb), &setup_window); } + +void +setup_open_page (const char *id) +{ + guint i; + const char *page_id = id; + gboolean menu_editor; + if (!id) { setup_open (); return; } + if (!strcmp (id, "themes") || !strcmp (id, "colors")) page_id = "appearance"; + menu_editor = !strcmp (id, "buttons") || !strcmp (id, "popup") || + !strcmp (id, "dlgbuttons") || !strcmp (id, "usermenu"); + if (menu_editor) page_id = "menus"; + setup_open (); + gtk_entry_set_text (GTK_ENTRY (setup_search), ""); + for (i = 0; i < setup_pages->len; i++) + { + setup_page *page = &g_array_index (setup_pages, setup_page, i); + if (strcmp (page->id, page_id)) continue; + gtk_list_box_select_row (GTK_LIST_BOX (setup_sidebar), GTK_LIST_BOX_ROW (page->row)); + if (menu_editor) + { + GtkWidget *selector = g_object_get_data (G_OBJECT (page->body), "menus-selector"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (selector), id); + } + break; + } + gtk_window_present (GTK_WINDOW (setup_window)); +} diff --git a/src/fe-gtk/setup.h b/src/fe-gtk/setup.h index af8b8466..790eff5d 100644 --- a/src/fe-gtk/setup.h +++ b/src/fe-gtk/setup.h @@ -24,5 +24,9 @@ void setup_apply_real (const ThemeChangedEvent *event); void setup_open (void); +/* Stable, untranslated page IDs allow existing menu entries to deep-link. */ +void setup_open_page (const char *id); +/* Reveal an existing editor, including any nested menu selector, on errors. */ +void setup_reveal_widget (GtkWidget *widget); #endif diff --git a/src/fe-gtk/tests/test-preferences-persistence.c b/src/fe-gtk/tests/test-preferences-persistence.c new file mode 100644 index 00000000..6cd3d18e --- /dev/null +++ b/src/fe-gtk/tests/test-preferences-persistence.c @@ -0,0 +1,226 @@ +/* ZoiteChat + * Copyright (C) 1998-2010 Peter Zelezny. + * Copyright (C) 2009-2013 Berke Viktor. + * Copyright (C) 2026 deepend-tildeclub. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Regression coverage for saving multiple Preferences editors together. */ +#include "../preferences-persistence.h" +#include "../../common/cfgfiles.h" +#include "../theme/theme-runtime.h" +#include +#include +#include + +static char *test_directory; +static gboolean fail_theme_prepare; +static int rename_count, fail_install, fail_restore; + +char *get_xdir (void) { return test_directory; } + +static gboolean +prepare (const char *name, const char *contents, char **path) +{ + *path = g_build_filename (test_directory, name, NULL); + return g_file_set_contents (*path, contents, -1, NULL); +} + +int save_config_prepare (char **path) { return prepare ("config.new", "new config", path); } +gboolean theme_runtime_save_prepare (char **path) +{ + if (fail_theme_prepare) return FALSE; + return prepare ("theme.new", "new theme", path); +} + +static int +checked_rename (const char *source, const char *destination) +{ + rename_count++; + if (rename_count == fail_install || rename_count == fail_restore) + { + errno = EACCES; + return -1; + } + return g_rename (source, destination); +} + +/* Exercise replacement failures without relying on filesystem permissions. */ +#undef g_rename +#define g_rename checked_rename +#include "../preferences-persistence.c" +#undef g_rename + +static void +put (const char *name, const char *contents) +{ + char *path = g_build_filename (test_directory, name, NULL); + g_assert_true (g_file_set_contents (path, contents, -1, NULL)); + g_free (path); +} + +static void +assert_contents (const char *name, const char *expected) +{ + char *path = g_build_filename (test_directory, name, NULL), *contents = NULL; + g_assert_true (g_file_get_contents (path, &contents, NULL, NULL)); + g_assert_cmpstr (contents, ==, expected); + g_free (contents); + g_free (path); +} + +static void +begin (void) +{ + test_directory = g_dir_make_tmp ("zoitechat-preferences-XXXXXX", NULL); + g_assert_nonnull (test_directory); + rename_count = fail_install = fail_restore = 0; + fail_theme_prepare = FALSE; + put ("zoitechat.conf", "old config"); + put ("colors.conf", "old theme"); + put ("commands.conf", "old commands"); + preferences_persistence_stage_file ("commands.conf", "new commands"); + preferences_persistence_stage_file ("replace.conf", "new replacements"); +} + +static int +temporary_count (void) +{ + GDir *directory = g_dir_open (test_directory, 0, NULL); + const char *name; + int count = 0; + while ((name = g_dir_read_name (directory))) + if (g_str_has_prefix (name, ".preferences-")) count++; + g_dir_close (directory); + return count; +} + +static void +end (void) +{ + GDir *directory = g_dir_open (test_directory, 0, NULL); + const char *name; + preferences_persistence_clear_staged_files (); + while ((name = g_dir_read_name (directory))) + { + char *path = g_build_filename (test_directory, name, NULL); + if (g_file_test (path, G_FILE_TEST_IS_DIR)) g_rmdir (path); + else g_unlink (path); + g_free (path); + } + g_dir_close (directory); + g_rmdir (test_directory); + g_free (test_directory); +} + +static void +test_success (void) +{ + PreferencesPersistenceResult result; + begin (); + result = preferences_persistence_save_all (); + g_assert_true (result.success); + assert_contents ("zoitechat.conf", "new config"); + assert_contents ("colors.conf", "new theme"); + assert_contents ("commands.conf", "new commands"); + assert_contents ("replace.conf", "new replacements"); + g_assert_cmpint (temporary_count (), ==, 0); + end (); +} + +static void +test_prepare_failure (void) +{ + PreferencesPersistenceResult result; + begin (); + fail_theme_prepare = TRUE; + result = preferences_persistence_save_all (); + g_assert_false (result.success); + g_assert_false (result.partial_failure); + g_assert_true (result.theme_failed); + assert_contents ("zoitechat.conf", "old config"); + assert_contents ("commands.conf", "old commands"); + g_assert_cmpint (rename_count, ==, 0); + g_assert_cmpint (temporary_count (), ==, 0); + end (); +} + +static void +test_backup_failure (void) +{ + PreferencesPersistenceResult result; + char *path; + begin (); + path = g_build_filename (test_directory, "replace.conf", NULL); + g_assert_cmpint (g_mkdir (path, 0700), ==, 0); + g_free (path); + result = preferences_persistence_save_all (); + g_assert_false (result.success); + g_assert_cmpstr (result.failed_file, ==, "replace.conf"); + assert_contents ("zoitechat.conf", "old config"); + assert_contents ("commands.conf", "old commands"); + g_assert_cmpint (rename_count, ==, 0); + g_assert_cmpint (temporary_count (), ==, 0); + end (); +} + +static void +test_rollback_and_retry (void) +{ + PreferencesPersistenceResult result; + begin (); + fail_install = 4; + result = preferences_persistence_save_all (); + g_assert_false (result.success); + g_assert_false (result.partial_failure); + g_assert_true (result.retry_possible); + assert_contents ("zoitechat.conf", "old config"); + assert_contents ("colors.conf", "old theme"); + assert_contents ("commands.conf", "old commands"); + g_assert_cmpint (temporary_count (), ==, 0); + fail_install = 0; + result = preferences_persistence_save_all (); + g_assert_true (result.success); + assert_contents ("commands.conf", "new commands"); + end (); +} + +static void +test_recovery_copy (void) +{ + PreferencesPersistenceResult result; + begin (); + fail_install = 4; + fail_restore = 5; + result = preferences_persistence_save_all (); + g_assert_false (result.success); + g_assert_true (result.partial_failure); + g_assert_cmpint (temporary_count (), ==, 1); + assert_contents ("zoitechat.conf", "old config"); + assert_contents ("colors.conf", "old theme"); + end (); +} + +int main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/preferences/save/success", test_success); + g_test_add_func ("/preferences/save/prepare-failure", test_prepare_failure); + g_test_add_func ("/preferences/save/backup-failure", test_backup_failure); + g_test_add_func ("/preferences/save/rollback-retry", test_rollback_and_retry); + g_test_add_func ("/preferences/save/recovery-copy", test_recovery_copy); + return g_test_run (); +} diff --git a/src/fe-gtk/textgui.c b/src/fe-gtk/textgui.c index 4abd7235..b0abaebb 100644 --- a/src/fe-gtk/textgui.c +++ b/src/fe-gtk/textgui.c @@ -38,6 +38,8 @@ #include "theme/theme-access.h" #include "theme/theme-manager.h" #include "textgui.h" +#include "setup.h" +#include "preferences-persistence.h" #define ICON_TEXTEVENT_SAVE_AS "document-save-as" #define ICON_TEXTEVENT_OPEN "document-open" @@ -50,6 +52,10 @@ extern char *pntevts[]; static GtkWidget *pevent_dialog = NULL, *pevent_dialog_twid, *pevent_dialog_list, *pevent_dialog_hlist; +static char *pevent_staged[NUM_XP]; +static char *pevent_original[NUM_XP]; +static gboolean pevent_pending; + #define PEVENT_THEME_LISTENER_ID_KEY "textgui.theme-listener-id" enum @@ -148,10 +154,17 @@ PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp) } static void -pevent_dialog_close (GtkWidget *wid, gpointer arg) +pevent_preferences_destroy (GtkWidget *widget, gpointer data) { - pevent_dialog = NULL; - pevent_save (NULL); + int i; + (void)widget; (void)data; + pevent_dialog = pevent_dialog_list = pevent_dialog_hlist = pevent_dialog_twid = NULL; + for (i = 0; i < NUM_XP; i++) + { + g_clear_pointer (&pevent_staged[i], g_free); + g_clear_pointer (&pevent_original[i], g_free); + } + pevent_pending = FALSE; } static void @@ -211,9 +224,14 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo char *out; int sig; - if (!gtkutil_treeview_get_selected (GTK_TREE_VIEW (pevent_dialog_list), - &iter, ROW_COLUMN, &sig, -1)) + path = gtk_tree_path_new_from_string (pathstr); + if (!gtk_tree_model_get_iter (model, &iter, path)) + { + gtk_tree_path_free (path); return; + } + gtk_tree_path_free (path); + gtk_tree_model_get (model, &iter, ROW_COLUMN, &sig, -1); text = new_text; len = strlen (new_text); @@ -234,16 +252,11 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo return; } - path = gtk_tree_path_new_from_string (pathstr); - gtk_tree_model_get_iter (model, &iter, path); gtk_list_store_set (GTK_LIST_STORE (model), &iter, TEXT_COLUMN, new_text, -1); - gtk_tree_path_free (path); - g_free (pntevts_text[sig]); - g_free (pntevts[sig]); - - pntevts_text[sig] = g_strdup (text); - pntevts[sig] = out; + g_free (pevent_staged[sig]); + pevent_staged[sig] = g_strdup (text); + g_free (out); out = g_malloc (len + 2); memcpy (out, text, len + 1); @@ -257,8 +270,7 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo /* Scroll to bottom */ gtk_adjustment_set_value (xtext->adj, gtk_adjustment_get_upper (xtext->adj)); - /* save this when we exit */ - prefs.save_pevents = 1; + /* The Preferences Save action commits this staged template. */ } static void @@ -316,53 +328,222 @@ pevent_dialog_fill (GtkWidget *list) i--; gtk_list_store_insert_with_values (store, &iter, 0, EVENT_COLUMN, te[i].name, - TEXT_COLUMN, pntevts_text[i], + TEXT_COLUMN, pevent_staged[i], ROW_COLUMN, i, -1); } while (i != 0); } -static void -pevent_save_req_cb (void *arg1, char *file) +static char * +pevent_preferences_serialize (void) { - if (file) - pevent_save (file); + GString *text = g_string_new (""); + int i; + for (i = 0; i < NUM_XP; i++) + g_string_append_printf (text, "event_name=%s\nevent_text=%s\n\n", te[i].name, pevent_staged[i]); + return g_string_free (text, FALSE); } -static void -pevent_save_cb (GtkWidget * wid, void *data) +static gboolean +pevent_preferences_validate (const char *text, int event, GError **error) { - if (data) + char *compiled = NULL; + int maximum; + if (strpbrk (text, "\r\n") || pevt_build_string (text, &compiled, &maximum) != 0) { - gtkutil_file_req (NULL, _("Print Texts File"), pevent_save_req_cb, NULL, - NULL, NULL, FRF_WRITE); - return; + g_free (compiled); + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Invalid message format for %s."), te[event].name); + return FALSE; } - pevent_save (NULL); -} - -static void -pevent_load_req_cb (void *arg1, char *file) -{ - if (file) + g_free (compiled); + if (maximum > (te[event].num_args & 0x7f)) { - pevent_load (file); - pevent_make_pntevts (); - pevent_dialog_fill (pevent_dialog_list); - prefs.save_pevents = 1; + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("%s only supports %d arguments."), te[event].name, te[event].num_args & 0x7f); + return FALSE; } + return TRUE; +} + +gboolean +pevent_preferences_import_text (const char *contents, GError **error) +{ + char **lines; + char *values[NUM_XP] = {0}; + int i, event = -1, matched = 0; + gboolean success = FALSE; + if (!pevent_dialog_list) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Open message formatting before importing templates.")); + return FALSE; + } + lines = g_strsplit (contents, "\n", -1); + for (i = 0; lines[i]; i++) + { + char *line = lines[i]; + gsize length = strlen (line); + int n; + if (length && line[length - 1] == '\r') line[length - 1] = 0; + if (g_str_has_prefix (line, "event_name=")) + { + event = -1; + for (n = 0; n < NUM_XP; n++) + if (!strcmp (te[n].name, line + 11)) { event = n; break; } + } + else if (event >= 0 && g_str_has_prefix (line, "event_text=")) + { + if (!pevent_preferences_validate (line + 11, event, error)) goto done; + g_free (values[event]); + values[event] = g_strdup (line + 11); + matched++; + } + } + if (!matched) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("No recognized text events were found in this file.")); + goto done; + } + for (i = 0; i < NUM_XP; i++) + if (values[i]) + { + g_free (pevent_staged[i]); + pevent_staged[i] = values[i]; + values[i] = NULL; + } + pevent_dialog_fill (pevent_dialog_list); + success = TRUE; + done: + for (i = 0; i < NUM_XP; i++) g_free (values[i]); + g_strfreev (lines); + return success; +} + +static gboolean +pevent_preferences_import (const char *file, GError **error) +{ + char *contents; + if (!g_file_get_contents (file, &contents, NULL, error)) return FALSE; + gboolean result = pevent_preferences_import_text (contents, error); + g_free (contents); + return result; +} + +typedef struct +{ + GWeakRef page; + gboolean exporting; +} PeEventFileRequest; + +static void +pevent_file_request_free (gpointer data) +{ + PeEventFileRequest *request = data; + g_weak_ref_clear (&request->page); + g_free (request); } static void -pevent_load_cb (GtkWidget * wid, void *data) +pevent_file_response (GtkNativeDialog *dialog, gint response, PeEventFileRequest *request) { - gtkutil_file_req (NULL, _("Print Texts File"), pevent_load_req_cb, NULL, NULL, NULL, 0); + GtkWidget *page = g_weak_ref_get (&request->page); + if (page && page == pevent_dialog && response == GTK_RESPONSE_ACCEPT) + { + char *file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); + GError *error = NULL; + if (file) + { + if (request->exporting) + { + char *contents = pevent_preferences_serialize (); + g_file_set_contents (file, contents, -1, &error); + g_free (contents); + } + else pevent_preferences_import (file, &error); + if (error) { fe_message (error->message, FE_MSG_ERROR); g_clear_error (&error); } + g_free (file); + } + } + if (page) g_object_unref (page); + g_object_unref (dialog); } static void -pevent_ok_cb (GtkWidget * wid, void *data) +pevent_file_open (GtkWidget *button, gpointer exporting) { - gtk_widget_destroy (pevent_dialog); + GtkFileChooserNative *dialog; + PeEventFileRequest *request = g_new0 (PeEventFileRequest, 1); + (void)button; + request->exporting = GPOINTER_TO_INT (exporting); + g_weak_ref_init (&request->page, pevent_dialog); + dialog = gtk_file_chooser_native_new (_("Text events file"), + GTK_WINDOW (gtk_widget_get_toplevel (pevent_dialog)), + request->exporting ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN, + request->exporting ? _("_Export") : _("_Import"), _("_Cancel")); + gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (dialog), TRUE); + g_object_set_data_full (G_OBJECT (dialog), "request", request, pevent_file_request_free); + g_signal_connect (dialog, "response", G_CALLBACK (pevent_file_response), request); + gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog)); +} + +gboolean +pevent_preferences_stage (GError **error) +{ + int i; + char *contents; + pevent_pending = FALSE; + if (!pevent_dialog) return TRUE; + for (i = 0; i < NUM_XP; i++) + { + if (!pevent_preferences_validate (pevent_staged[i], i, error)) + { + GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (pevent_dialog_list)); + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + setup_reveal_widget (pevent_dialog_list); + while (valid) + { + int event; + gtk_tree_model_get (model, &iter, ROW_COLUMN, &event, -1); + if (event == i) + { + GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + gtk_widget_grab_focus (pevent_dialog_list); + gtk_tree_view_set_cursor (GTK_TREE_VIEW (pevent_dialog_list), path, NULL, FALSE); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (pevent_dialog_list), path, NULL, FALSE, 0, 0); + gtk_tree_path_free (path); + break; + } + valid = gtk_tree_model_iter_next (model, &iter); + } + return FALSE; + } + if (g_strcmp0 (pevent_staged[i], pevent_original[i]) != 0) pevent_pending = TRUE; + } + if (!pevent_pending) return TRUE; + for (i = 0; i < NUM_XP; i++) + if (g_strcmp0 (pntevts_text[i], pevent_original[i]) != 0) + { + setup_reveal_widget (pevent_dialog_list); + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_AGAIN, _("Text events changed outside Preferences. Reopen Preferences before editing them.")); + return FALSE; + } + contents = pevent_preferences_serialize (); + preferences_persistence_stage_file ("pevents.conf", contents); + g_free (contents); + return TRUE; +} + +void +pevent_preferences_commit (void) +{ + int i; + if (!pevent_pending) return; + for (i = 0; i < NUM_XP; i++) + { + g_free (pntevts_text[i]); + pntevts_text[i] = g_strdup (pevent_staged[i]); + } + pevent_make_pntevts (); + prefs.save_pevents = 1; } static void @@ -373,7 +554,7 @@ pevent_test_cb (GtkWidget * wid, GtkWidget * twid) for (n = 0; n < NUM_XP; n++) { - text = _(pntevts_text[n]); + text = pevent_staged[n]; len = strlen (text); out = g_malloc (len + 2); @@ -485,22 +666,27 @@ pevent_hlist_treeview_new (GtkWidget *box) return view; } -void -pevent_dialog_show (void) +static void +pevent_preferences_preview_realize (GtkWidget *widget, gpointer data) +{ + gtk_xtext_set_font (GTK_XTEXT (widget), prefs.hex_text_font); +} + +GtkWidget * +pevent_preferences_create (void) { GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane; XTextColor xtext_palette[XTEXT_COLS]; - if (pevent_dialog) + int i; + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + pevent_dialog = vbox; + for (i = 0; i < NUM_XP; i++) { - mg_bring_tofront (pevent_dialog); - return; + pevent_original[i] = g_strdup (pntevts_text[i]); + pevent_staged[i] = g_strdup (pntevts_text[i]); } - - pevent_dialog = - mg_create_generic_tab ("edit events", _("Edit Events"), - TRUE, FALSE, pevent_dialog_close, NULL, - 600, 455, &vbox, 0); + g_signal_connect (vbox, "destroy", G_CALLBACK (pevent_preferences_destroy), NULL); preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL); gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0); @@ -523,7 +709,7 @@ pevent_dialog_show (void) pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0); gtk_widget_set_sensitive (pevent_dialog_twid, FALSE); gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid); - gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font); + g_signal_connect (pevent_dialog_twid, "realize", G_CALLBACK (pevent_preferences_preview_realize), NULL); g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid); g_object_set_data (G_OBJECT (pevent_dialog), PEVENT_THEME_LISTENER_ID_KEY, GUINT_TO_POINTER (theme_listener_register ("textgui.events", pevent_dialog_theme_changed, pevent_dialog))); @@ -532,15 +718,20 @@ pevent_dialog_show (void) hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_SPREAD); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 4); - gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_save_cb, - (void *) 1, _("Save As...")); - gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_load_cb, - NULL, _("Load From...")); + gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_file_open, + (void *) 1, _("Export...")); + gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_file_open, + NULL, _("Import...")); gtkutil_button (hbox, NULL, NULL, pevent_test_cb, pevent_dialog_twid, _("Test All")); - gtkutil_button (hbox, ICON_TEXTEVENT_OK, NULL, pevent_ok_cb, - NULL, _("OK")); gtk_widget_show_all (pevent_dialog); pevent_dialog_theme_apply (pevent_dialog); + return vbox; +} + +void +pevent_dialog_show (void) +{ + setup_open_page ("events"); } diff --git a/src/fe-gtk/textgui.h b/src/fe-gtk/textgui.h index 88dbd6e9..c7087272 100644 --- a/src/fe-gtk/textgui.h +++ b/src/fe-gtk/textgui.h @@ -22,5 +22,9 @@ void PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp); void pevent_dialog_show (void); +GtkWidget *pevent_preferences_create (void); +gboolean pevent_preferences_import_text (const char *contents, GError **error); +gboolean pevent_preferences_stage (GError **error); +void pevent_preferences_commit (void); #endif diff --git a/src/fe-gtk/theme/tests/test-theme-access-routing.c b/src/fe-gtk/theme/tests/test-theme-access-routing.c index 1c69d502..eee2a9a6 100644 --- a/src/fe-gtk/theme/tests/test-theme-access-routing.c +++ b/src/fe-gtk/theme/tests/test-theme-access-routing.c @@ -112,6 +112,14 @@ theme_runtime_mode_has_user_colors (gboolean dark_mode) return FALSE; } +gboolean +theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color) +{ + (void)map; + *color = stub_light_colors[token]; + return TRUE; +} + gboolean theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) { diff --git a/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c b/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c index 9723a5ca..11020add 100644 --- a/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c +++ b/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c @@ -571,6 +571,31 @@ test_stale_theme_symlink_is_replaced (void) theme_gtk3_disable (); } +static void +test_builtin_appearances_restore_desktop_theme (void) +{ + if (!gtk_available) return; + GtkSettings *settings = gtk_settings_get_default (); + char *original, *name; + gboolean dark; + theme_gtk3_disable (); + g_object_get (settings, "gtk-theme-name", &original, NULL); + g_strlcpy (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK, sizeof prefs.hex_gui_gtk3_theme); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + g_assert_true (theme_gtk3_apply_current (NULL)); + g_object_get (settings, "gtk-theme-name", &name, "gtk-application-prefer-dark-theme", &dark, NULL); + g_assert_cmpstr (name, ==, "Adwaita"); g_assert_true (dark); g_free (name); + g_strlcpy (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_LIGHT, sizeof prefs.hex_gui_gtk3_theme); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_LIGHT; + g_assert_true (theme_gtk3_apply_current (NULL)); + g_object_get (settings, "gtk-application-prefer-dark-theme", &dark, NULL); + g_assert_false (dark); + prefs.hex_gui_gtk3_theme[0] = '\0'; prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_AUTO; + g_assert_true (theme_gtk3_apply_current (NULL)); + g_object_get (settings, "gtk-theme-name", &name, NULL); + g_assert_cmpstr (name, ==, original); g_free (name); g_free (original); +} + int main (int argc, char **argv) { @@ -596,6 +621,7 @@ main (int argc, char **argv) if (!gtk_available) g_test_message ("Skipping GTK3 settings tests because GTK initialization failed"); + g_test_add_func ("/theme/gtk3/builtin_appearances_restore_desktop", test_builtin_appearances_restore_desktop_theme); rc = g_test_run (); theme_gtk3_disable (); teardown_themes (); diff --git a/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c index 56441a7e..ecd47e46 100644 --- a/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c +++ b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c @@ -25,6 +25,7 @@ #include "../../../common/gtk3-theme-service.h" #include "../theme-gtk3.h" #include "../theme-manager.h" +#include "../theme-runtime.h" struct session *current_sess; struct session *current_tab; @@ -38,6 +39,61 @@ static char applied_theme_id[256]; static ThemeGtk3Variant applied_variant; static gboolean removed_selected; +static gboolean custom_tokens[THEME_TOKEN_COUNT]; +static GdkRGBA custom_colors[THEME_TOKEN_COUNT]; + +gboolean +theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color) +{ + if (!custom_tokens[token]) return FALSE; + *color = custom_colors[token]; + return TRUE; +} + +gboolean +theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color) +{ + (void)map; + return gdk_rgba_parse (color, token == THEME_TOKEN_TEXT_BACKGROUND || token == THEME_TOKEN_SELECTION_FOREGROUND ? "#ffffff" : + token == THEME_TOKEN_SELECTION_BACKGROUND ? "#336699" : "#000000"); +} + +gboolean +theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color) +{ + (void)widget; + theme_runtime_get_default_color (token, NULL, color); + if (token == THEME_TOKEN_TEXT_BACKGROUND && prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK) + gdk_rgba_parse (color, "#181818"); + return TRUE; +} + +void theme_runtime_reset_user_color (ThemeSemanticToken token) { custom_tokens[token] = FALSE; } +void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ custom_tokens[token] = TRUE; custom_colors[token] = *color; } + +gboolean theme_manager_apply_mode (unsigned int mode, gboolean *changed) +{ (void)mode; if (changed) *changed = FALSE; return FALSE; } +void theme_manager_apply_and_dispatch (unsigned int mode, ThemeChangedReason reasons, gboolean *changed) +{ (void)mode; (void)reasons; if (changed) *changed = FALSE; } +guint theme_listener_register (const char *id, ThemeChangedCallback callback, gpointer data) +{ (void)id; (void)callback; (void)data; return 0; } +void theme_listener_unregister (guint id) { (void)id; } +gboolean pevent_preferences_import_text (const char *contents, GError **error) +{ (void)contents; (void)error; return TRUE; } + +void +setup_open_page (const char *id) +{ + (void)id; +} + +void +setup_reveal_widget (GtkWidget *widget) +{ + (void)widget; +} + GtkWidget * gtkutil_box_new (GtkOrientation orientation, gboolean homogeneous, gint spacing) { @@ -106,6 +162,7 @@ theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token, (void)token; (void)color; set_token_color_calls++; + theme_runtime_user_set_color (token, color); if (changed) *changed = FALSE; } @@ -458,6 +515,154 @@ test_select_theme_does_not_pin_palette_tokens (void) gtk_widget_destroy (page); } +static void +test_embedded_color_editor (void) +{ + GtkWidget *window, *editor; + theme_color_manager_ui *ui; + theme_color_manager_row *row; + GError *error = NULL; + if (!gtk_available) + { + g_test_message ("GTK display not available"); + return; + } + memset (&prefs, 0, sizeof prefs); + theme_preferences_stage_begin (); + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + editor = theme_preferences_create_color_editor (GTK_WINDOW (window), NULL); + gtk_container_add (GTK_CONTAINER (window), editor); + ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager"); + row = g_ptr_array_index (ui->rows, 0); + set_token_color_calls = 0; + theme_preferences_manager_entry_commit (row); + g_assert_true (theme_preferences_color_editor_stage (editor, &error)); + g_assert_no_error (error); + theme_preferences_stage_apply (); + g_assert_cmpint (set_token_color_calls, ==, 0); + gtk_entry_set_text (GTK_ENTRY (row->entry), "#123456"); + g_assert_true (theme_preferences_color_editor_stage (editor, &error)); + g_assert_no_error (error); + /* A typed edit previews its token, then Save reapplies that token. */ + g_assert_cmpint (set_token_color_calls, ==, 1); + theme_preferences_stage_apply (); + g_assert_cmpint (set_token_color_calls, ==, 2); + gtk_entry_set_text (GTK_ENTRY (row->entry), "invalid"); + g_assert_false (theme_preferences_color_editor_stage (editor, &error)); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); + g_clear_error (&error); + gtk_widget_show_all (window); + gtk_button_clicked (GTK_BUTTON (row->button)); + gtk_widget_destroy (window); + theme_preferences_stage_discard (); +} + +static void +test_appearance_schemes_and_cancel (void) +{ + if (!gtk_available) { g_test_message ("GTK display not available"); return; } + memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens); + GdkRGBA original, restored; + gdk_rgba_parse (&original, "#345678"); + theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &original); + theme_preferences_stage_begin (); + struct zoitechatprefs staged = prefs; + GtkWidget *page = theme_preferences_create_page (NULL, &staged, NULL); + theme_preferences_ui *appearance = g_object_get_data (G_OBJECT (page), "theme-preferences-ui"); + theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (appearance->color_editor), "zoitechat-theme-color-manager"); + gtk_button_clicked (GTK_BUTTON (ui->scheme_buttons[2])); + g_assert_true (theme_preferences_stage.changed); + g_assert_true (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_BACKGROUND]); + gtk_button_clicked (GTK_BUTTON (appearance->style_buttons[2])); + g_assert_cmpint (prefs.hex_gui_dark_mode, ==, ZOITECHAT_DARK_MODE_DARK); + theme_preferences_stage_discard (); + g_assert_cmpint (prefs.hex_gui_dark_mode, ==, ZOITECHAT_DARK_MODE_AUTO); + g_assert_true (theme_runtime_get_user_color (THEME_TOKEN_TEXT_FOREGROUND, &restored)); + g_assert_true (gdk_rgba_equal (&original, &restored)); + for (int token = 0; token < THEME_TOKEN_COUNT; token++) + if (token != THEME_TOKEN_TEXT_FOREGROUND) g_assert_false (custom_tokens[token]); + gtk_widget_destroy (page); +} + +static void +test_individual_reset_preserves_other_overrides (void) +{ + if (!gtk_available) { g_test_message ("GTK display not available"); return; } + memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens); + theme_preferences_stage_begin (); + GtkWidget *editor = theme_preferences_create_color_editor (NULL, NULL); + theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager"); + gtk_button_clicked (GTK_BUTTON (ui->scheme_buttons[2])); + theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_TEXT_BACKGROUND); + gtk_button_clicked (GTK_BUTTON (row->reset)); + g_assert_false (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_BACKGROUND]); + g_assert_true (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_FOREGROUND]); + g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (row->status)), ==, "Theme"); + GdkRGBA light, dark; + theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &light); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &dark); + g_assert_false (gdk_rgba_equal (&light, &dark)); + theme_preferences_stage_commit (); + g_assert_false (custom_tokens[THEME_TOKEN_TEXT_BACKGROUND]); + g_assert_true (custom_tokens[THEME_TOKEN_TEXT_FOREGROUND]); + gtk_widget_destroy (editor); +} + +static void +test_contrast_suggestion_and_preview_navigation (void) +{ + if (!gtk_available) { g_test_message ("GTK display not available"); return; } + memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens); + theme_preferences_stage_begin (); + GtkWidget *editor = theme_preferences_create_color_editor (NULL, NULL); + theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager"); + theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_TEXT_FOREGROUND); + GdkRGBA white, black, gray; + gdk_rgba_parse (&white, "#ffffff"); gdk_rgba_parse (&black, "#000000"); gdk_rgba_parse (&gray, "#dddddd"); + g_assert_cmpfloat_with_epsilon (theme_preferences_contrast_ratio (&white, &black), 21.0, 0.001); + theme_preferences_manager_row_commit (row, &gray); + g_assert_true (gtk_widget_get_visible (ui->contrast_fix)); + gtk_button_clicked (GTK_BUTTON (ui->contrast_fix)); + theme_preferences_staged_get_color (THEME_TOKEN_TEXT_FOREGROUND, &gray); + g_assert_cmpfloat (theme_preferences_contrast_ratio (&gray, &white), >=, 4.5); + theme_preferences_manager_focus_color (ui, THEME_TOKEN_MIRC_12); + g_assert_true (gtk_expander_get_expanded (GTK_EXPANDER (ui->advanced))); + gtk_entry_set_text (GTK_ENTRY (row->entry), "invalid"); + theme_preferences_manager_entry_commit (row); + g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (row->entry)), ==, "invalid"); + GError *error = NULL; + g_assert_false (theme_preferences_color_editor_stage (editor, &error)); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_clear_error (&error); + theme_preferences_stage_discard (); gtk_widget_destroy (editor); +} + +static void +test_picker_cancel_restores_inheritance (void) +{ + if (!gtk_available) { g_test_message ("GTK display not available"); return; } + memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens); + theme_preferences_stage_begin (); + GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + GtkWidget *editor = theme_preferences_create_color_editor (GTK_WINDOW (window), NULL); + gtk_container_add (GTK_CONTAINER (window), editor); + theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager"); + theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_MIRC_12); + gtk_button_clicked (GTK_BUTTON (row->button)); + GList *windows = gtk_window_list_toplevels (); + GtkWidget *picker = NULL; + for (GList *i = windows; i; i = i->next) if (GTK_IS_COLOR_CHOOSER_DIALOG (i->data)) picker = i->data; + g_assert_nonnull (picker); + GdkRGBA color; gdk_rgba_parse (&color, "#aabbcc"); + gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (picker), &color); + g_assert_true (theme_preferences_stage.staged_custom[row->token]); + gtk_dialog_response (GTK_DIALOG (picker), GTK_RESPONSE_CANCEL); + g_assert_false (theme_preferences_stage.staged_custom[row->token]); + g_assert_false (custom_tokens[row->token]); + g_list_free (windows); + theme_preferences_stage_discard (); gtk_widget_destroy (window); +} + int main (int argc, char **argv) { @@ -471,5 +676,10 @@ main (int argc, char **argv) test_select_none_resets_theme_and_applies); g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens", test_select_theme_does_not_pin_palette_tokens); + g_test_add_func ("/theme/preferences/embedded_color_editor", test_embedded_color_editor); + g_test_add_func ("/theme/preferences/appearance_schemes_cancel", test_appearance_schemes_and_cancel); + g_test_add_func ("/theme/preferences/individual_reset_inherits", test_individual_reset_preserves_other_overrides); + g_test_add_func ("/theme/preferences/contrast_and_navigation", test_contrast_suggestion_and_preview_navigation); + g_test_add_func ("/theme/preferences/picker_cancel_inherits", test_picker_cancel_restores_inheritance); return g_test_run (); } diff --git a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c index 3050e5fb..b06f7000 100644 --- a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c +++ b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c @@ -430,6 +430,36 @@ test_save_writes_only_custom_token_keys (void) g_free (cfg); } +static void +test_reset_one_colour_restores_inheritance_after_reload (void) +{ + GdkRGBA color; + char *cfg; + setup_temp_home (); + theme_runtime_load (); + gdk_rgba_parse (&color, "#456789"); + theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &color); + theme_runtime_user_set_color (THEME_TOKEN_TEXT_BACKGROUND, &color); + theme_runtime_reset_user_color (THEME_TOKEN_TEXT_BACKGROUND); + theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL); + g_assert_true (theme_runtime_save ()); + cfg = read_colors_conf (); + g_assert_nonnull (strstr (cfg, "theme.mode.light.token.text_foreground")); + g_assert_null (strstr (cfg, "theme.mode.light.token.text_background")); + g_free (cfg); + theme_runtime_load (); + g_assert_false (theme_runtime_get_user_color (THEME_TOKEN_TEXT_BACKGROUND, &color)); + g_assert_true (theme_runtime_get_user_color (THEME_TOKEN_TEXT_FOREGROUND, &color)); + g_assert_cmpfloat_with_epsilon (color.red, 0x45 / 255.0, 0.0001); + ThemeGtkPaletteMap map = { .enabled = TRUE }; + gdk_rgba_parse (&map.text_background, "#111111"); + theme_runtime_get_default_color (THEME_TOKEN_TEXT_BACKGROUND, &map, &color); + g_assert_cmpfloat_with_epsilon (color.red, 0x11 / 255.0, 0.0001); + gdk_rgba_parse (&map.text_background, "#eeeeee"); + theme_runtime_get_default_color (THEME_TOKEN_TEXT_BACKGROUND, &map, &color); + g_assert_cmpfloat_with_epsilon (color.red, 0xee / 255.0, 0.0001); +} + int main (int argc, char **argv) { @@ -450,5 +480,6 @@ main (int argc, char **argv) test_save_discard_unlinks_temp_file); g_test_add_func ("/theme/runtime/save_writes_only_custom_token_keys", test_save_writes_only_custom_token_keys); + g_test_add_func ("/theme/runtime/reset_colour_persists_inheritance", test_reset_one_colour_restores_inheritance_after_reload); return g_test_run (); } diff --git a/src/fe-gtk/theme/theme-access.c b/src/fe-gtk/theme/theme-access.c index a79c6c2b..5942e58b 100644 --- a/src/fe-gtk/theme/theme-access.c +++ b/src/fe-gtk/theme/theme-access.c @@ -206,6 +206,14 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map return TRUE; } +gboolean +theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color) +{ + ThemeGtkPaletteMap map = { 0 }; + theme_access_get_gtk_palette_map (widget, &map); + return theme_runtime_get_default_color (token, &map, color); +} + gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) { diff --git a/src/fe-gtk/theme/theme-access.h b/src/fe-gtk/theme/theme-access.h index 13811466..300480c9 100644 --- a/src/fe-gtk/theme/theme-access.h +++ b/src/fe-gtk/theme/theme-access.h @@ -28,6 +28,7 @@ #include "theme-palette.h" #include "../xtext-color.h" +gboolean theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color); gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba); gboolean theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba); gboolean theme_get_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue); diff --git a/src/fe-gtk/theme/theme-gtk3.c b/src/fe-gtk/theme/theme-gtk3.c index 2aba5353..921777dc 100644 --- a/src/fe-gtk/theme/theme-gtk3.c +++ b/src/fe-gtk/theme/theme-gtk3.c @@ -1223,9 +1223,27 @@ theme_gtk3_init (void) gboolean theme_gtk3_apply_current (GError **error) { + if (!strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_LIGHT) || !strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK)) + { + /* GTK ships Adwaita on every supported platform. Explicit Light/Dark + * choices must not depend on the desktop theme providing both variants. */ + GtkSettings *settings = gtk_settings_get_default (); + theme_gtk3_disable (); + if (settings) + settings_apply_property (settings, "gtk-theme-name", "Adwaita"); + settings_apply_for_variant (!strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK) ? + THEME_GTK3_VARIANT_PREFER_DARK : THEME_GTK3_VARIANT_PREFER_LIGHT); + return TRUE; + } + if (!prefs.hex_gui_gtk3_theme[0]) { theme_gtk3_disable (); + /* Restoring a previous theme's settings must not undo an explicit + * appearance choice, including when a settings notification re-enters. */ + settings_apply_for_variant (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO ? + THEME_GTK3_VARIANT_FOLLOW_SYSTEM : prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK ? + THEME_GTK3_VARIANT_PREFER_DARK : THEME_GTK3_VARIANT_PREFER_LIGHT); return TRUE; } diff --git a/src/fe-gtk/theme/theme-gtk3.h b/src/fe-gtk/theme/theme-gtk3.h index c3ac664d..66b21c03 100644 --- a/src/fe-gtk/theme/theme-gtk3.h +++ b/src/fe-gtk/theme/theme-gtk3.h @@ -24,6 +24,9 @@ #include #include +#define THEME_GTK3_BUILTIN_LIGHT "builtin:light" +#define THEME_GTK3_BUILTIN_DARK "builtin:dark" + typedef enum { THEME_GTK3_VARIANT_FOLLOW_SYSTEM = 0, diff --git a/src/fe-gtk/theme/theme-preferences.c b/src/fe-gtk/theme/theme-preferences.c index 7721454e..538b0100 100644 --- a/src/fe-gtk/theme/theme-preferences.c +++ b/src/fe-gtk/theme/theme-preferences.c @@ -20,6 +20,7 @@ #include #include +#include #include "../../common/zoitechat.h" #include "../../common/zoitechatc.h" @@ -35,7 +36,9 @@ #include "theme-gtk3.h" #include "theme-manager.h" #include "theme-preferences.h" +#include "../setup.h" #include "theme-runtime.h" +#include "../textgui.h" extern void load_text_events (void); extern char **text_color_event_names (int color, int *count); @@ -45,6 +48,11 @@ typedef struct GtkWindow *parent; GtkWidget *gtk3_combo; GtkWidget *gtk3_remove; + GtkWidget *theme_cards; + GtkWidget *theme_search; + GtkWidget *style_buttons[3]; + GtkWidget *color_editor; + GtkWidget *theme_expander; gboolean gtk3_populating; struct zoitechatprefs *setup_prefs; } theme_preferences_ui; @@ -63,6 +71,9 @@ typedef struct GtkWidget *button; GtkWidget *entry; GtkWidget *preview; + GtkWidget *status; + GtkWidget *reset; + GtkWidget *expander; ThemeSemanticToken token; char *search_text; gboolean *color_change_flag; @@ -75,11 +86,24 @@ typedef struct theme_color_manager_row *row; GdkRGBA original; gboolean has_original; + gboolean original_custom; } theme_manager_live_picker_data; typedef struct { GPtrArray *rows; + GPtrArray *preview_parts; + GtkWidget *root; + GtkWidget *source; + GtkWidget *advanced; + GtkWidget *contrast; + GtkWidget *contrast_fix; + GtkWidget *scheme_buttons[4]; + GtkWidget *scheme_status; + GtkWidget *match_swatches[4]; + guint listener; + guint refresh_idle; + gboolean syncing; GtkWidget *search_entry; gboolean *color_change_flag; GtkWidget *preview_window; @@ -104,6 +128,10 @@ typedef struct /* Only explicitly edited tokens are written back to the runtime * palette; untouched tokens keep following the GTK3 theme. */ gboolean staged_dirty[THEME_TOKEN_COUNT]; + gboolean snapshot_custom[THEME_TOKEN_COUNT]; + gboolean staged_custom[THEME_TOKEN_COUNT]; + GtkWidget *source; + int dark_mode_snapshot; GdkRGBA snapshot[THEME_TOKEN_COUNT]; GdkRGBA staged[THEME_TOKEN_COUNT]; /* the GTK3 theme selection is previewed live from the combo, so @@ -123,11 +151,14 @@ theme_preferences_staged_get_color (ThemeSemanticToken token, GdkRGBA *rgba) if (theme_preferences_stage.active && theme_preferences_stage.staged_valid[token]) { - *rgba = theme_preferences_stage.staged[token]; + if (theme_preferences_stage.staged_custom[token]) + *rgba = theme_preferences_stage.staged[token]; + else + return theme_get_default_color_for_widget (theme_preferences_stage.source, token, rgba); return TRUE; } - - return theme_get_color (token, rgba); + if (theme_runtime_get_user_color (token, rgba)) return TRUE; + return theme_get_default_color_for_widget (theme_preferences_stage.source, token, rgba); } static void @@ -140,7 +171,9 @@ theme_preferences_stage_recompute_changed (void) { if (!theme_preferences_stage.snapshot_valid[token] || !theme_preferences_stage.staged_valid[token]) continue; - if (!gdk_rgba_equal (&theme_preferences_stage.snapshot[token], &theme_preferences_stage.staged[token])) + if (theme_preferences_stage.snapshot_custom[token] != theme_preferences_stage.staged_custom[token] || + (theme_preferences_stage.staged_custom[token] && + !gdk_rgba_equal (&theme_preferences_stage.snapshot[token], &theme_preferences_stage.staged[token]))) { theme_preferences_stage.changed = TRUE; return; @@ -157,9 +190,15 @@ theme_preferences_stage_sync_runtime_to_snapshot (void) { if (theme_preferences_stage.snapshot_valid[token] && theme_preferences_stage.staged_dirty[token]) - theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, - &theme_preferences_stage.snapshot[token], NULL); + { + if (theme_preferences_stage.snapshot_custom[token]) + theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, + &theme_preferences_stage.snapshot[token], NULL); + else theme_runtime_reset_user_color (token); + } } + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, + THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); } static void @@ -171,9 +210,15 @@ theme_preferences_stage_sync_runtime_to_staged (void) { if (theme_preferences_stage.staged_valid[token] && theme_preferences_stage.staged_dirty[token]) - theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, - &theme_preferences_stage.staged[token], NULL); + { + if (theme_preferences_stage.staged_custom[token]) + theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, + &theme_preferences_stage.staged[token], NULL); + else theme_runtime_reset_user_color (token); + } } + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, + THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); } static void @@ -190,6 +235,7 @@ theme_preferences_staged_set_color (ThemeSemanticToken token, const GdkRGBA *rgb theme_preferences_stage.staged[token] = *rgba; theme_preferences_stage.staged_valid[token] = TRUE; theme_preferences_stage.staged_dirty[token] = TRUE; + theme_preferences_stage.staged_custom[token] = TRUE; theme_preferences_stage_recompute_changed (); if (color_change_flag) *color_change_flag = theme_preferences_stage.changed; @@ -213,6 +259,7 @@ theme_preferences_stage_begin (void) prefs.hex_gui_gtk3_theme, sizeof (theme_preferences_stage.gtk3_theme_snapshot)); theme_preferences_stage.gtk3_variant_snapshot = prefs.hex_gui_gtk3_variant; + theme_preferences_stage.dark_mode_snapshot = prefs.hex_gui_dark_mode; for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) { @@ -221,6 +268,8 @@ theme_preferences_stage_begin (void) if (!theme_preferences_staged_get_color (token, &rgba)) continue; + theme_preferences_stage.snapshot_custom[token] = theme_runtime_get_user_color (token, &rgba); + theme_preferences_stage.staged_custom[token] = theme_preferences_stage.snapshot_custom[token]; theme_preferences_stage.snapshot[token] = rgba; theme_preferences_stage.staged[token] = rgba; theme_preferences_stage.snapshot_valid[token] = TRUE; @@ -253,15 +302,17 @@ static void theme_preferences_stage_restore_gtk3 (void) { if (g_strcmp0 (prefs.hex_gui_gtk3_theme, theme_preferences_stage.gtk3_theme_snapshot) == 0 && - prefs.hex_gui_gtk3_variant == theme_preferences_stage.gtk3_variant_snapshot) + prefs.hex_gui_gtk3_variant == theme_preferences_stage.gtk3_variant_snapshot && + prefs.hex_gui_dark_mode == theme_preferences_stage.dark_mode_snapshot) return; g_strlcpy (prefs.hex_gui_gtk3_theme, theme_preferences_stage.gtk3_theme_snapshot, sizeof (prefs.hex_gui_gtk3_theme)); prefs.hex_gui_gtk3_variant = theme_preferences_stage.gtk3_variant_snapshot; - + prefs.hex_gui_dark_mode = theme_preferences_stage.dark_mode_snapshot; theme_gtk3_apply_current (NULL); + theme_manager_apply_mode (prefs.hex_gui_dark_mode, NULL); theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK | THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | @@ -269,6 +320,14 @@ theme_preferences_stage_restore_gtk3 (void) THEME_CHANGED_REASON_MODE); } +void +theme_preferences_stage_revert_runtime (void) +{ + if (!theme_preferences_stage.active) return; + theme_preferences_stage_restore_gtk3 (); + theme_preferences_stage_sync_runtime_to_snapshot (); +} + void theme_preferences_stage_discard (void) { @@ -307,166 +366,138 @@ theme_preferences_manager_ui_free (gpointer data) if (!ui) return; - if (ui->rows) - g_ptr_array_unref (ui->rows); + if (ui->listener) theme_listener_unregister (ui->listener); + if (ui->refresh_idle) g_source_remove (ui->refresh_idle); + if (theme_preferences_stage.source == ui->source) theme_preferences_stage.source = NULL; + if (ui->preview_parts) g_ptr_array_unref (ui->preview_parts); + if (ui->rows) g_ptr_array_unref (ui->rows); g_free (ui); } +static void theme_preferences_manager_focus_color (theme_color_manager_ui *ui, ThemeSemanticToken token); +static void theme_preferences_manager_sync_schemes (theme_color_manager_ui *ui); +static void theme_preferences_manager_refresh_rows (theme_color_manager_ui *ui); + +static double +theme_preferences_luminance (const GdkRGBA *color) +{ + double values[] = { color->red, color->green, color->blue }; + for (guint i = 0; i < 3; i++) + values[i] = values[i] <= 0.04045 ? values[i] / 12.92 : pow ((values[i] + 0.055) / 1.055, 2.4); + return values[0] * 0.2126 + values[1] * 0.7152 + values[2] * 0.0722; +} + +static double +theme_preferences_contrast_ratio (const GdkRGBA *foreground, const GdkRGBA *background) +{ + GdkRGBA visible = *foreground; + visible.red = foreground->red * foreground->alpha + background->red * (1 - foreground->alpha); + visible.green = foreground->green * foreground->alpha + background->green * (1 - foreground->alpha); + visible.blue = foreground->blue * foreground->alpha + background->blue * (1 - foreground->alpha); + double first = theme_preferences_luminance (&visible), second = theme_preferences_luminance (background); + return (MAX (first, second) + 0.05) / (MIN (first, second) + 0.05); +} + static void theme_preferences_manager_update_preview (theme_color_manager_ui *ui) { - GdkRGBA text_fg; - GdkRGBA text_bg; - GdkRGBA sel_fg; - GdkRGBA sel_bg; - GdkRGBA marker; - GdkRGBA tab_new_data; - GdkRGBA tab_new_message; - GdkRGBA tab_highlight; - GdkRGBA tab_away; - GdkRGBA spell; - GtkWidget *label; + GdkRGBA fg, bg, selected_fg, selected_bg; + if (!ui || !ui->preview_parts) return; + theme_preferences_staged_get_color (THEME_TOKEN_TEXT_FOREGROUND, &fg); + theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &bg); + theme_preferences_staged_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &selected_fg); + theme_preferences_staged_get_color (THEME_TOKEN_SELECTION_BACKGROUND, &selected_bg); + gtkutil_apply_palette (ui->preview_window, &bg, &fg, NULL); + for (guint i = 0; i < ui->preview_parts->len; i++) + { + GtkWidget *part = g_ptr_array_index (ui->preview_parts, i); + GdkRGBA part_fg, part_bg; + ThemeSemanticToken foreground = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (part), "preview-foreground")); + ThemeSemanticToken background = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (part), "preview-background")); + theme_preferences_staged_get_color (foreground, &part_fg); + theme_preferences_staged_get_color (background, &part_bg); + gtkutil_apply_palette (part, &part_bg, &part_fg, NULL); + gtkutil_apply_palette (gtk_bin_get_child (GTK_BIN (part)), NULL, &part_fg, NULL); + } + if (ui->contrast) + { + double normal = theme_preferences_contrast_ratio (&fg, &bg); + double selected = theme_preferences_contrast_ratio (&selected_fg, &selected_bg); + gboolean warning = normal < 4.5 || selected < 4.5; + char *message = warning ? g_strdup_printf (_("Some text may be difficult to read (%.1f:1). Aim for at least 4.5:1. Your choice is still allowed."), MIN (normal, selected)) : + g_strdup (_("Message text and selected text have good contrast.")); + gtk_label_set_text (GTK_LABEL (ui->contrast), message); + gtk_widget_set_visible (ui->contrast_fix, warning); + g_free (message); + } + const ThemeSemanticToken swatches[] = { THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TAB_HIGHLIGHT, THEME_TOKEN_TAB_NEW_MESSAGE }; + for (guint i = 0; i < G_N_ELEMENTS (swatches); i++) + if (ui->match_swatches[i]) + { + GdkRGBA color; + theme_get_default_color_for_widget (theme_preferences_stage.source, swatches[i], &color); + gtkutil_apply_palette (ui->match_swatches[i], &color, NULL, NULL); + } + theme_preferences_manager_sync_schemes (ui); +} - if (!ui) - return; - - if (!theme_preferences_staged_get_color (THEME_TOKEN_TEXT_FOREGROUND, &text_fg) - || !theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &text_bg) - || !theme_preferences_staged_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &sel_fg) - || !theme_preferences_staged_get_color (THEME_TOKEN_SELECTION_BACKGROUND, &sel_bg) - || !theme_preferences_staged_get_color (THEME_TOKEN_MARKER, &marker) - || !theme_preferences_staged_get_color (THEME_TOKEN_TAB_NEW_DATA, &tab_new_data) - || !theme_preferences_staged_get_color (THEME_TOKEN_TAB_NEW_MESSAGE, &tab_new_message) - || !theme_preferences_staged_get_color (THEME_TOKEN_TAB_HIGHLIGHT, &tab_highlight) - || !theme_preferences_staged_get_color (THEME_TOKEN_TAB_AWAY, &tab_away) - || !theme_preferences_staged_get_color (THEME_TOKEN_SPELL, &spell)) - return; - - gtkutil_apply_palette (ui->preview_window, &text_bg, &text_fg, NULL); - gtkutil_apply_palette (ui->preview_chat, &text_bg, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_chat), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_selected, &sel_bg, &sel_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_selected), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &sel_fg, NULL); - - gtkutil_apply_palette (ui->preview_marker, &marker, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_marker), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_tab_new_data, &tab_new_data, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_tab_new_data), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_tab_new_message, &tab_new_message, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_tab_new_message), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_tab_highlight, &tab_highlight, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_tab_highlight), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_tab_away, &tab_away, &text_fg, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_tab_away), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &text_fg, NULL); - - gtkutil_apply_palette (ui->preview_spell, &text_bg, &spell, NULL); - label = g_object_get_data (G_OBJECT (ui->preview_spell), "zoitechat-preview-label"); - if (GTK_IS_WIDGET (label)) - gtkutil_apply_palette (label, NULL, &spell, NULL); +static void +theme_preferences_preview_clicked (GtkButton *button, gpointer data) +{ + ThemeSemanticToken token = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "edit-token")); + theme_preferences_manager_focus_color (data, token); } static GtkWidget * -theme_preferences_manager_preview_item_new (const char *text) +theme_preferences_preview_part (theme_color_manager_ui *ui, const char *text, + ThemeSemanticToken foreground, ThemeSemanticToken background, ThemeSemanticToken edit) { - GtkWidget *box; - GtkWidget *label; - - box = gtk_event_box_new (); - gtk_event_box_set_visible_window (GTK_EVENT_BOX (box), TRUE); - gtk_container_set_border_width (GTK_CONTAINER (box), 3); - - label = gtk_label_new (text); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_container_add (GTK_CONTAINER (box), label); - g_object_set_data (G_OBJECT (box), "zoitechat-preview-label", label); - - return box; + GtkWidget *button = gtk_button_new_with_label (text); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_widget_set_halign (button, GTK_ALIGN_FILL); + gtk_label_set_xalign (GTK_LABEL (gtk_bin_get_child (GTK_BIN (button))), 0); + gtk_label_set_line_wrap (GTK_LABEL (gtk_bin_get_child (GTK_BIN (button))), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (gtk_bin_get_child (GTK_BIN (button))), 25); + g_object_set_data (G_OBJECT (button), "preview-foreground", GINT_TO_POINTER (foreground)); + g_object_set_data (G_OBJECT (button), "preview-background", GINT_TO_POINTER (background)); + g_object_set_data (G_OBJECT (button), "edit-token", GINT_TO_POINTER (edit)); + g_signal_connect (button, "clicked", G_CALLBACK (theme_preferences_preview_clicked), ui); + gtk_widget_set_tooltip_text (button, _("Select to edit this colour. The same setting is available below the preview.")); + g_ptr_array_add (ui->preview_parts, button); + return button; } static GtkWidget * theme_preferences_manager_create_preview (theme_color_manager_ui *ui) { - GtkWidget *frame; - GtkWidget *vbox; - GtkWidget *header; - GtkWidget *chat_box; - GtkWidget *tabs_box; - GtkWidget *label; - - frame = gtk_frame_new (_("Live preview")); - vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); - gtk_container_set_border_width (GTK_CONTAINER (vbox), 8); - gtk_container_add (GTK_CONTAINER (frame), vbox); - + GtkWidget *frame = gtk_frame_new (_("Chat preview — select a part to change its colour")); + GtkWidget *inside = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *body = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + GtkWidget *channels = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *chat = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *line = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + ui->preview_parts = g_ptr_array_new (); ui->preview_window = gtk_event_box_new (); - gtk_event_box_set_visible_window (GTK_EVENT_BOX (ui->preview_window), TRUE); gtk_container_set_border_width (GTK_CONTAINER (ui->preview_window), 8); - gtk_box_pack_start (GTK_BOX (vbox), ui->preview_window, TRUE, TRUE, 0); - - chat_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); - gtk_container_add (GTK_CONTAINER (ui->preview_window), chat_box); - - header = gtk_label_new (_("#zoitechat-preview")); - gtk_widget_set_halign (header, GTK_ALIGN_START); - gtk_box_pack_start (GTK_BOX (chat_box), header, FALSE, FALSE, 0); - - ui->preview_chat = theme_preferences_manager_preview_item_new (_(" Example chat message")); - gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_chat, FALSE, FALSE, 0); - - ui->preview_selected = theme_preferences_manager_preview_item_new (_("Selected text example")); - gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_selected, FALSE, FALSE, 0); - - ui->preview_marker = theme_preferences_manager_preview_item_new (_("Marker line")); - gtk_widget_set_hexpand (ui->preview_marker, TRUE); - gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_marker, FALSE, FALSE, 0); - - ui->preview_spell = theme_preferences_manager_preview_item_new (_("mispelled wrd")); - gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_spell, FALSE, FALSE, 0); - - label = gtk_label_new (_("Tab states")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0); - - tabs_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); - gtk_box_pack_start (GTK_BOX (vbox), tabs_box, FALSE, FALSE, 0); - - ui->preview_tab_new_data = theme_preferences_manager_preview_item_new (_("New data")); - gtk_widget_set_hexpand (ui->preview_tab_new_data, TRUE); - gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_new_data, TRUE, TRUE, 0); - - ui->preview_tab_new_message = theme_preferences_manager_preview_item_new (_("New message")); - gtk_widget_set_hexpand (ui->preview_tab_new_message, TRUE); - gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_new_message, TRUE, TRUE, 0); - - ui->preview_tab_highlight = theme_preferences_manager_preview_item_new (_("Highlight")); - gtk_widget_set_hexpand (ui->preview_tab_highlight, TRUE); - gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_highlight, TRUE, TRUE, 0); - - ui->preview_tab_away = theme_preferences_manager_preview_item_new (_("Away")); - gtk_widget_set_hexpand (ui->preview_tab_away, TRUE); - gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_away, TRUE, TRUE, 0); - - theme_preferences_manager_update_preview (ui); - + gtk_container_add (GTK_CONTAINER (frame), ui->preview_window); + gtk_container_add (GTK_CONTAINER (ui->preview_window), inside); + gtk_box_pack_start (GTK_BOX (inside), theme_preferences_preview_part (ui, _("#zoitechat · Chat background"), THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_BACKGROUND), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (inside), body, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (body), channels, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (body), gtk_separator_new (GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (body), chat, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (channels), theme_preferences_preview_part (ui, _("#new messages"), THEME_TOKEN_TAB_NEW_MESSAGE, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TAB_NEW_MESSAGE), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (channels), theme_preferences_preview_part (ui, _("#mentions"), THEME_TOKEN_TAB_HIGHLIGHT, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TAB_HIGHLIGHT), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (channels), theme_preferences_preview_part (ui, _("#activity"), THEME_TOKEN_TAB_NEW_DATA, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TAB_NEW_DATA), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (channels), theme_preferences_preview_part (ui, _("alice (away)"), THEME_TOKEN_TAB_AWAY, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TAB_AWAY), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (chat), line, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (line), theme_preferences_preview_part (ui, "12:34 ", THEME_TOKEN_MIRC_12, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_MIRC_12), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (line), theme_preferences_preview_part (ui, _("Hello! Welcome to ZoiteChat."), THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND), TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (chat), theme_preferences_preview_part (ui, _("This is how your messages will look."), THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (chat), theme_preferences_preview_part (ui, _("Selected text · edit text colour"), THEME_TOKEN_SELECTION_FOREGROUND, THEME_TOKEN_SELECTION_BACKGROUND, THEME_TOKEN_SELECTION_FOREGROUND), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (chat), theme_preferences_preview_part (ui, _("Selection background"), THEME_TOKEN_SELECTION_FOREGROUND, THEME_TOKEN_SELECTION_BACKGROUND, THEME_TOKEN_SELECTION_BACKGROUND), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (inside), theme_preferences_preview_part (ui, _("Unread marker line"), THEME_TOKEN_MARKER, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_MARKER), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (inside), theme_preferences_preview_part (ui, _("Spelling warning"), THEME_TOKEN_SPELL, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_SPELL), FALSE, FALSE, 0); return frame; } @@ -572,33 +603,33 @@ static char * theme_preferences_token_display_name (ThemeSemanticToken token) { if (token >= THEME_TOKEN_MIRC_0 && token <= THEME_TOKEN_MIRC_15) - return g_strdup_printf (_("mIRC color %d"), token - THEME_TOKEN_MIRC_0); + return g_strdup_printf (_("IRC colour %d"), token - THEME_TOKEN_MIRC_0); if (token >= THEME_TOKEN_MIRC_16 && token <= THEME_TOKEN_MIRC_31) - return g_strdup_printf (_("Local color %d"), token - THEME_TOKEN_MIRC_0); + return g_strdup_printf (_("Nickname / local colour %d"), token - THEME_TOKEN_MIRC_0); switch (token) { case THEME_TOKEN_SELECTION_FOREGROUND: - return g_strdup (_("Selected text foreground")); + return g_strdup (_("Selected text")); case THEME_TOKEN_SELECTION_BACKGROUND: return g_strdup (_("Selected text background")); case THEME_TOKEN_TEXT_FOREGROUND: - return g_strdup (_("Text foreground")); + return g_strdup (_("Message text")); case THEME_TOKEN_TEXT_BACKGROUND: - return g_strdup (_("Text background")); + return g_strdup (_("Chat background")); case THEME_TOKEN_MARKER: return g_strdup (_("Marker line")); case THEME_TOKEN_TAB_NEW_DATA: - return g_strdup (_("Tab: new data")); + return g_strdup (_("Channel activity")); case THEME_TOKEN_TAB_HIGHLIGHT: - return g_strdup (_("Tab: highlight")); + return g_strdup (_("Mentions")); case THEME_TOKEN_TAB_NEW_MESSAGE: - return g_strdup (_("Tab: new message")); + return g_strdup (_("Unread messages")); case THEME_TOKEN_TAB_AWAY: - return g_strdup (_("Tab: away")); + return g_strdup (_("Away users")); case THEME_TOKEN_SPELL: - return g_strdup (_("Spell checker")); + return g_strdup (_("Spelling warnings")); default: return g_strdup (_("Unknown color")); } @@ -711,17 +742,25 @@ theme_preferences_color_set_tooltip (GtkWidget *widget, ThemeSemanticToken token g_free (markup); } +static gboolean theme_preferences_is_custom (ThemeSemanticToken token); + static void theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA *rgba) { char *hex; + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (row->entry), GTK_ENTRY_ICON_SECONDARY, NULL); theme_preferences_color_button_apply (row->button, rgba); - gtkutil_apply_palette (row->preview, rgba, NULL, NULL); + GdkRGBA black, white; + gdk_rgba_parse (&black, "#000000"); gdk_rgba_parse (&white, "#ffffff"); + const GdkRGBA *ink = theme_preferences_contrast_ratio (&black, rgba) > theme_preferences_contrast_ratio (&white, rgba) ? &black : &white; + gtkutil_apply_palette (row->preview, rgba, ink, NULL); hex = theme_preferences_format_hex (rgba); gtk_entry_set_text (GTK_ENTRY (row->entry), hex); - g_free (hex); + g_object_set_data_full (G_OBJECT (row->entry), "applied-color", hex, g_free); theme_preferences_color_set_tooltip (row->row, row->token); + if (row->status) gtk_label_set_text (GTK_LABEL (row->status), theme_preferences_is_custom (row->token) ? _("Custom") : _("Theme")); + if (row->reset) gtk_widget_set_sensitive (row->reset, theme_preferences_is_custom (row->token)); } static void @@ -738,16 +777,50 @@ theme_preferences_manager_entry_commit (theme_color_manager_row *row) GdkRGBA rgba; const char *text = gtk_entry_get_text (GTK_ENTRY (row->entry)); + if (g_strcmp0 (text, g_object_get_data (G_OBJECT (row->entry), "applied-color")) == 0) + return; + if (!gdk_rgba_parse (&rgba, text)) { - if (theme_preferences_staged_get_color (row->token, &rgba)) - theme_preferences_manager_row_apply (row, &rgba); + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (row->entry), GTK_ENTRY_ICON_SECONDARY, "dialog-warning-symbolic"); + gtk_entry_set_icon_tooltip_text (GTK_ENTRY (row->entry), GTK_ENTRY_ICON_SECONDARY, _("Enter a colour such as #336699, or choose one using the colour button.")); return; } theme_preferences_manager_row_commit (row, &rgba); } +gboolean +theme_preferences_color_editor_stage (GtkWidget *editor, GError **error) +{ + theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager"); + guint i; + if (!ui) + { + GtkWidget *child = g_object_get_data (G_OBJECT (editor), "appearance-color-editor"); + if (child) return theme_preferences_color_editor_stage (child, error); + } + for (i = 0; ui && i < ui->rows->len; i++) + { + theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); + const char *text = gtk_entry_get_text (GTK_ENTRY (row->entry)); + GdkRGBA rgba; + if (g_strcmp0 (text, g_object_get_data (G_OBJECT (row->entry), "applied-color")) == 0) + continue; + if (!gdk_rgba_parse (&rgba, text)) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + _("Enter a valid color value before saving.")); + theme_preferences_manager_focus_color (ui, row->token); + setup_reveal_widget (row->entry); + gtk_widget_grab_focus (row->entry); + return FALSE; + } + theme_preferences_manager_row_commit (row, &rgba); + } + return TRUE; +} + static void theme_preferences_manager_entry_activate_cb (GtkEntry *entry, gpointer user_data) { @@ -778,16 +851,34 @@ theme_preferences_manager_picker_notify_rgba_cb (GObject *object, GParamSpec *ps theme_preferences_manager_row_commit (data->row, &rgba); } +static void theme_preferences_use_theme_color (ThemeSemanticToken token); + static void theme_preferences_manager_picker_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data) { theme_manager_live_picker_data *data = user_data; if (data && data->row && data->has_original && response_id != GTK_RESPONSE_OK) - theme_preferences_manager_row_commit (data->row, &data->original); + { + if (data->original_custom) theme_preferences_manager_row_commit (data->row, &data->original); + else + { + theme_preferences_use_theme_color (data->row->token); + if (data->row->color_change_flag) *data->row->color_change_flag = theme_preferences_stage.changed; + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); + theme_preferences_manager_refresh_rows (data->row->manager_ui); + } + } gtk_widget_destroy (GTK_WIDGET (dialog)); - g_free (data); +} + +static void +theme_preferences_manager_picker_owner_destroyed (GtkWidget *dialog) +{ + theme_manager_live_picker_data *data = g_object_get_data (G_OBJECT (dialog), "picker-data"); + data->row = NULL; + gtk_widget_destroy (dialog); } static void @@ -805,11 +896,17 @@ theme_preferences_manager_pick_cb (GtkWidget *button, gpointer user_data) theme_manager_attach_window (dialog); gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (dialog), &rgba); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE); data = g_new0 (theme_manager_live_picker_data, 1); data->row = row; data->original = rgba; data->has_original = TRUE; + data->original_custom = theme_preferences_is_custom (row->token); + g_object_set_data_full (G_OBJECT (dialog), "picker-data", data, g_free); + g_signal_connect_object (row->row, "destroy", + G_CALLBACK (theme_preferences_manager_picker_owner_destroyed), + dialog, G_CONNECT_SWAPPED); g_signal_connect (G_OBJECT (dialog), "notify::rgba", G_CALLBACK (theme_preferences_manager_picker_notify_rgba_cb), data); @@ -839,7 +936,10 @@ theme_preferences_manager_search_changed_cb (GtkEditable *editable, gpointer use for (i = 0; i < ui->rows->len; i++) { theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); - gtk_widget_set_visible (row->row, theme_preferences_manager_row_matches (row, needle_lower)); + gboolean matches = theme_preferences_manager_row_matches (row, needle_lower); + gtk_widget_set_visible (row->row, matches); + if (matches && needle_lower[0] && row->expander) + gtk_expander_set_expanded (GTK_EXPANDER (row->expander), TRUE); } g_free (needle_lower); } @@ -857,6 +957,8 @@ theme_preferences_manager_refresh_rows (theme_color_manager_ui *ui) theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); GdkRGBA rgba; + const char *applied = g_object_get_data (G_OBJECT (row->entry), "applied-color"); + if (applied && g_strcmp0 (applied, gtk_entry_get_text (GTK_ENTRY (row->entry)))) continue; if (theme_preferences_staged_get_color (row->token, &rgba)) theme_preferences_manager_row_apply (row, &rgba); } @@ -864,219 +966,401 @@ theme_preferences_manager_refresh_rows (theme_color_manager_ui *ui) theme_preferences_manager_update_preview (ui); } -static void -theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data) +static gboolean +theme_preferences_is_custom (ThemeSemanticToken token) { - theme_color_manager_ui *ui = user_data; - - if (response_id != COLOR_MANAGER_RESPONSE_RESET) - return; + GdkRGBA color; + return theme_preferences_stage.active ? theme_preferences_stage.staged_custom[token] : + theme_runtime_get_user_color (token, &color); +} +static void +theme_preferences_use_theme_color (ThemeSemanticToken token) +{ + if (theme_preferences_stage.active) { - gboolean changed = FALSE; - - theme_manager_reset_mode_colors (ZOITECHAT_DARK_MODE_LIGHT, &changed); - if (theme_preferences_stage.active) - { - ThemeSemanticToken token; - ThemeWidgetStyleValues style_values; - - /* The runtime palette was just reset; drop any - * pending edits so the reset is not re-pinned. */ - memset (theme_preferences_stage.staged_dirty, 0, - sizeof (theme_preferences_stage.staged_dirty)); - for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) - { - GdkRGBA rgba; - - if (!theme_get_color (token, &rgba)) - continue; - theme_preferences_stage.staged[token] = rgba; - theme_preferences_stage.staged_valid[token] = TRUE; - } - theme_get_widget_style_values_for_widget (GTK_WIDGET (dialog), &style_values); - theme_preferences_stage.staged[THEME_TOKEN_TEXT_FOREGROUND] = style_values.foreground; - theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE; - theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background; - theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE; - theme_preferences_stage_recompute_changed (); - if (ui->color_change_flag) - *ui->color_change_flag = theme_preferences_stage.changed; - } - else if (ui->color_change_flag) - *ui->color_change_flag = *ui->color_change_flag || changed; + theme_preferences_stage.staged_custom[token] = FALSE; + theme_preferences_stage.staged_dirty[token] = TRUE; + theme_preferences_stage_recompute_changed (); } + theme_runtime_reset_user_color (token); +} +static void +theme_preferences_manager_row_reset (GtkButton *button, gpointer data) +{ + theme_color_manager_row *row = data; + GdkRGBA color; + (void)button; + theme_preferences_use_theme_color (row->token); + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, + THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); + if (row->color_change_flag) *row->color_change_flag = theme_preferences_stage.changed; + theme_preferences_staged_get_color (row->token, &color); + theme_preferences_manager_row_apply (row, &color); + theme_preferences_manager_update_preview (row->manager_ui); +} + +static void +theme_preferences_manager_focus_color (theme_color_manager_ui *ui, ThemeSemanticToken token) +{ + gtk_entry_set_text (GTK_ENTRY (ui->search_entry), ""); + for (guint i = 0; i < ui->rows->len; i++) + { + theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); + if (row->token != token) continue; + if (row->expander) gtk_expander_set_expanded (GTK_EXPANDER (row->expander), TRUE); + gtk_widget_grab_focus (row->button); + return; + } +} + +/* Schemes are editable templates. Their identity is derived from the palette, + * so existing colours.conf files require no new configuration format. */ +static gboolean +theme_preferences_scheme_color (int scheme, ThemeSemanticToken token, GdkRGBA *color) +{ + static const char *soft[] = { + "#e6edf3", "#17212b", "#ffffff", "#315c82", "#79c0ff", + "#a5d6ff", "#ffa657", "#7ee787", "#a6adb4", "#ff7b72" + }; + static const char *contrast[] = { + "#ffffff", "#000000", "#000000", "#ffff00", "#ffffff", + "#00ffff", "#ffff00", "#00ff00", "#c0c0c0", "#ff8080" + }; + static const ThemeSemanticToken targets[] = { + THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TEXT_BACKGROUND, + THEME_TOKEN_SELECTION_FOREGROUND, THEME_TOKEN_SELECTION_BACKGROUND, + THEME_TOKEN_MARKER, THEME_TOKEN_TAB_NEW_DATA, THEME_TOKEN_TAB_HIGHLIGHT, + THEME_TOKEN_TAB_NEW_MESSAGE, THEME_TOKEN_TAB_AWAY, THEME_TOKEN_SPELL + }; + if (!scheme) return FALSE; + theme_runtime_get_default_color (token, NULL, color); + if (scheme == 1) return TRUE; + for (guint i = 0; i < G_N_ELEMENTS (targets); i++) + if (token == targets[i]) return gdk_rgba_parse (color, scheme == 2 ? soft[i] : contrast[i]); + /* Keep the IRC colour identities while making their dark-theme + * variants readable. Users can still edit every entry individually. */ + color->red = 0.58 + color->red * 0.42; + color->green = 0.58 + color->green * 0.42; + color->blue = 0.58 + color->blue * 0.42; + return TRUE; +} + +static void +theme_preferences_choice_selected (GtkWidget *button, gboolean selected) +{ + GtkWidget *title = g_object_get_data (G_OBJECT (button), "choice-title"); + const char *name = g_object_get_data (G_OBJECT (button), "choice-name"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), selected); + if (title && name) + { + char *text = selected ? g_strdup_printf ("✓ %s", name) : g_strdup (name); + gtk_label_set_text (GTK_LABEL (title), text); + g_free (text); + } +} + +static void +theme_preferences_manager_sync_schemes (theme_color_manager_ui *ui) +{ + if (!ui->scheme_buttons[0]) return; + int matched = -1; + gboolean custom = FALSE; + for (int token = 0; token < THEME_TOKEN_COUNT; token++) custom |= theme_preferences_is_custom (token); + if (!custom) matched = 0; + for (int scheme = 1; matched < 0 && scheme < 4; scheme++) + { + gboolean same = TRUE; + for (int token = 0; same && token < THEME_TOKEN_COUNT; token++) + { + GdkRGBA actual, expected; + theme_preferences_staged_get_color (token, &actual); + theme_preferences_scheme_color (scheme, token, &expected); + same = fabs (actual.red - expected.red) < 0.005 && fabs (actual.green - expected.green) < 0.005 && + fabs (actual.blue - expected.blue) < 0.005; + } + if (same) matched = scheme; + } + ui->syncing = TRUE; + for (int i = 0; i < 4; i++) theme_preferences_choice_selected (ui->scheme_buttons[i], i == matched); + ui->syncing = FALSE; + gtk_label_set_text (GTK_LABEL (ui->scheme_status), matched < 0 ? _("Custom colours · your individual choices are kept") : + matched == 0 ? _("Chat colours follow the application. Individual changes are optional.") : + _("Scheme selected · you can adjust any colour below")); +} + +static void +theme_preferences_manager_scheme_clicked (GtkButton *button, gpointer data) +{ + theme_color_manager_ui *ui = data; + if (ui->syncing) return; + int scheme = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "scheme")); + for (int token = 0; token < THEME_TOKEN_COUNT; token++) + { + GdkRGBA color; + if (!scheme) theme_preferences_use_theme_color (token); + else + { + theme_preferences_scheme_color (scheme, token, &color); + theme_preferences_staged_set_color (token, &color, ui->color_change_flag, FALSE); + theme_runtime_user_set_color (token, &color); + } + } + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, + THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); + if (ui->color_change_flag) *ui->color_change_flag = theme_preferences_stage.changed; + for (guint i = 0; i < ui->rows->len; i++) + { + theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); + GdkRGBA color; + theme_preferences_staged_get_color (row->token, &color); + theme_preferences_manager_row_apply (row, &color); + } + theme_preferences_manager_update_preview (ui); +} + +static void +theme_preferences_manager_reset (GtkWidget *button, gpointer data) +{ + /* Reset is the same inheritance operation as Match application. */ + theme_color_manager_ui *ui = data; + (void)button; + theme_preferences_manager_scheme_clicked (GTK_BUTTON (ui->scheme_buttons[0]), ui); +} + +static void +theme_preferences_contrast_fix (GtkButton *button, gpointer data) +{ + theme_color_manager_ui *ui = data; + const ThemeSemanticToken texts[] = { THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_SELECTION_FOREGROUND }; + const ThemeSemanticToken backgrounds[] = { THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_SELECTION_BACKGROUND }; + (void)button; + for (guint n = 0; n < G_N_ELEMENTS (texts); n++) + { + GdkRGBA text, bg, black, white; + theme_preferences_staged_get_color (texts[n], &text); + theme_preferences_staged_get_color (backgrounds[n], &bg); + if (theme_preferences_contrast_ratio (&text, &bg) >= 4.5) continue; + gdk_rgba_parse (&black, "#000000"); gdk_rgba_parse (&white, "#ffffff"); + for (guint i = 0; i < ui->rows->len; i++) + { + theme_color_manager_row *row = g_ptr_array_index (ui->rows, i); + if (row->token == texts[n]) + theme_preferences_manager_row_commit (row, theme_preferences_contrast_ratio (&black, &bg) > + theme_preferences_contrast_ratio (&white, &bg) ? &black : &white); + } + } +} + +static gboolean +theme_preferences_manager_refresh_idle (gpointer data) +{ + theme_color_manager_ui *ui = data; + ui->refresh_idle = 0; theme_preferences_manager_refresh_rows (ui); - g_signal_stop_emission_by_name (dialog, "response"); + return G_SOURCE_REMOVE; +} + +static void +theme_preferences_manager_theme_changed (const ThemeChangedEvent *event, gpointer data) +{ + theme_color_manager_ui *ui = data; + (void)event; + if (!ui->refresh_idle) ui->refresh_idle = g_idle_add (theme_preferences_manager_refresh_idle, ui); } static GtkWidget * -theme_preferences_create_color_manager_dialog (GtkWindow *parent, gboolean *color_change_flag) +theme_preferences_note (const char *text) { - GtkWidget *dialog; - GtkWidget *content; - GtkWidget *vbox; - GtkWidget *content_hbox; - GtkWidget *left_box; - GtkWidget *search; - GtkWidget *scroller; - GtkWidget *list; - GtkWidget *preview_frame; - theme_color_manager_ui *ui; - ThemeSemanticToken token; + GtkWidget *label = gtk_label_new (text); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 62); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + return label; +} - dialog = gtk_dialog_new_with_buttons (_("Manage client colors"), - parent, - GTK_DIALOG_MODAL, - _("_Reset to GTK3 defaults"), - COLOR_MANAGER_RESPONSE_RESET, - _("_Close"), - GTK_RESPONSE_CLOSE, - NULL); - theme_manager_attach_window (dialog); - gtk_window_set_default_size (GTK_WINDOW (dialog), 760, 560); +static void +theme_preferences_flow_activate (GtkFlowBox *box, GtkFlowBoxChild *child, gpointer data) +{ + GtkWidget *button = gtk_bin_get_child (GTK_BIN (child)); + (void)box; (void)data; + if (GTK_IS_BUTTON (button)) gtk_button_clicked (GTK_BUTTON (button)); +} - ui = g_new0 (theme_color_manager_ui, 1); +GtkWidget * +theme_preferences_create_color_editor (GtkWindow *parent, gboolean *color_change_flag) +{ + theme_color_manager_ui *ui = g_new0 (theme_color_manager_ui, 1); + GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + GtkWidget *schemes = gtk_flow_box_new (); + GtkWidget *common = gtk_list_box_new (), *advanced = gtk_list_box_new (); + const char *scheme_names[] = { N_("Match application"), N_("Classic IRC"), N_("Soft dark"), N_("High contrast") }; + const char *scheme_notes[] = { N_("Follow the window theme"), N_("Traditional IRC palette"), N_("Gentle colours on dark blue"), N_("Bright text on black") }; ui->rows = g_ptr_array_new_with_free_func (theme_preferences_manager_row_free); - ui->color_change_flag = color_change_flag; - g_object_set_data_full (G_OBJECT (dialog), "zoitechat-theme-color-manager", ui, theme_preferences_manager_ui_free); - g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (theme_preferences_manager_dialog_response_cb), ui); - - content = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); - vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); - gtk_container_set_border_width (GTK_CONTAINER (vbox), 8); - gtk_widget_set_hexpand (vbox, TRUE); - gtk_widget_set_vexpand (vbox, TRUE); - gtk_box_pack_start (GTK_BOX (content), vbox, TRUE, TRUE, 0); - - content_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_hexpand (content_hbox, TRUE); - gtk_widget_set_vexpand (content_hbox, TRUE); - gtk_box_pack_start (GTK_BOX (vbox), content_hbox, TRUE, TRUE, 0); - - left_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); - gtk_widget_set_hexpand (left_box, TRUE); - gtk_widget_set_vexpand (left_box, TRUE); - gtk_box_pack_start (GTK_BOX (content_hbox), left_box, TRUE, TRUE, 0); - - search = gtk_search_entry_new (); - gtk_entry_set_placeholder_text (GTK_ENTRY (search), _("Search colors by name")); - gtk_box_pack_start (GTK_BOX (left_box), search, FALSE, FALSE, 0); - ui->search_entry = search; - - scroller = gtk_scrolled_window_new (NULL, NULL); - gtk_widget_set_hexpand (scroller, TRUE); - gtk_widget_set_vexpand (scroller, TRUE); - gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); - gtk_box_pack_start (GTK_BOX (left_box), scroller, TRUE, TRUE, 0); - - list = gtk_list_box_new (); - gtk_widget_set_hexpand (list, TRUE); - gtk_widget_set_vexpand (list, TRUE); - gtk_list_box_set_selection_mode (GTK_LIST_BOX (list), GTK_SELECTION_NONE); - gtk_container_add (GTK_CONTAINER (scroller), list); - - preview_frame = theme_preferences_manager_create_preview (ui); - gtk_widget_set_size_request (preview_frame, 300, -1); - gtk_widget_set_hexpand (preview_frame, FALSE); - gtk_widget_set_vexpand (preview_frame, TRUE); - gtk_box_pack_start (GTK_BOX (content_hbox), preview_frame, FALSE, TRUE, 0); - - for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) + ui->root = box; ui->color_change_flag = color_change_flag; + ui->source = parent ? GTK_WIDGET (parent) : box; + theme_preferences_stage.source = ui->source; + g_object_set_data_full (G_OBJECT (box), "zoitechat-theme-color-manager", ui, theme_preferences_manager_ui_free); + gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (schemes), GTK_SELECTION_NONE); + gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (schemes), FALSE); + g_signal_connect (schemes, "child-activated", G_CALLBACK (theme_preferences_flow_activate), NULL); + gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (schemes), 2); + gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (schemes), 4); + gtk_flow_box_set_column_spacing (GTK_FLOW_BOX (schemes), 8); + gtk_flow_box_set_row_spacing (GTK_FLOW_BOX (schemes), 8); + for (int i = 0; i < 4; i++) { - theme_color_manager_row *row; - GtkWidget *list_row; - GtkWidget *hbox; - GtkWidget *name; - GtkWidget *preview; - GtkWidget *button; - GtkWidget *entry; - GdkRGBA rgba; - char *display; - char *search_text; - char *token_code; - - list_row = gtk_list_box_row_new (); - hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); - gtk_container_set_border_width (GTK_CONTAINER (hbox), 4); - gtk_container_add (GTK_CONTAINER (list_row), hbox); - - display = theme_preferences_token_display_name (token); - name = gtk_label_new (display); - gtk_widget_set_halign (name, GTK_ALIGN_START); - gtk_widget_set_hexpand (name, TRUE); - gtk_box_pack_start (GTK_BOX (hbox), name, TRUE, TRUE, 0); - - preview = gtk_label_new (_("Preview")); - gtk_widget_set_size_request (preview, 90, -1); - gtk_widget_set_halign (preview, GTK_ALIGN_CENTER); - gtk_box_pack_start (GTK_BOX (hbox), preview, FALSE, FALSE, 0); - - button = gtk_button_new_with_label (_("Choose…")); - gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 0); - - entry = gtk_entry_new (); - gtk_entry_set_width_chars (GTK_ENTRY (entry), 9); - gtk_entry_set_max_length (GTK_ENTRY (entry), 9); - gtk_box_pack_start (GTK_BOX (hbox), entry, FALSE, FALSE, 0); - - row = g_new0 (theme_color_manager_row, 1); - row->row = list_row; - row->button = button; - row->entry = entry; - row->preview = preview; - row->token = token; - row->color_change_flag = color_change_flag; - row->parent = GTK_WINDOW (dialog); - row->manager_ui = ui; - - token_code = g_strdup_printf ("token_%d", token); - search_text = g_strconcat (display, " ", token_code, NULL); + GtkWidget *content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4); + GtkWidget *button = gtk_toggle_button_new (); + GtkWidget *title = gtk_label_new (_(scheme_names[i])); + gtk_label_set_line_wrap (GTK_LABEL (title), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (title), 14); + GtkWidget *swatches = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_set_border_width (GTK_CONTAINER (content), 8); + gtk_box_pack_start (GTK_BOX (content), title, FALSE, FALSE, 0); + for (int swatch = 0; swatch < 4; swatch++) + { + GdkRGBA color; + const ThemeSemanticToken tokens[] = { THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND, THEME_TOKEN_TAB_HIGHLIGHT, THEME_TOKEN_TAB_NEW_MESSAGE }; + if (i) theme_preferences_scheme_color (i, tokens[swatch], &color); + else theme_get_default_color_for_widget (GTK_WIDGET (parent), tokens[swatch], &color); + GtkWidget *sample = gtk_event_box_new (); + if (!i) ui->match_swatches[swatch] = sample; + gtk_widget_set_size_request (sample, 26, 12); + gtkutil_apply_palette (sample, &color, NULL, NULL); + gtk_box_pack_start (GTK_BOX (swatches), sample, TRUE, TRUE, 0); + } + gtk_box_pack_start (GTK_BOX (content), swatches, FALSE, FALSE, 2); + gtk_container_add (GTK_CONTAINER (button), content); + g_object_set_data (G_OBJECT (button), "choice-title", title); + g_object_set_data (G_OBJECT (button), "choice-name", (gpointer)_(scheme_names[i])); + gtk_widget_set_tooltip_text (button, _(scheme_notes[i])); + atk_object_set_name (gtk_widget_get_accessible (button), _(scheme_names[i])); + g_object_set_data (G_OBJECT (button), "scheme", GINT_TO_POINTER (i)); + g_signal_connect (button, "clicked", G_CALLBACK (theme_preferences_manager_scheme_clicked), ui); + gtk_flow_box_insert (GTK_FLOW_BOX (schemes), button, -1); + ui->scheme_buttons[i] = button; + } + gtk_box_pack_start (GTK_BOX (box), schemes, FALSE, FALSE, 0); + ui->scheme_status = theme_preferences_note (""); + gtk_box_pack_start (GTK_BOX (box), ui->scheme_status, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), theme_preferences_manager_create_preview (ui), FALSE, FALSE, 0); + GtkWidget *feedback = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4); + ui->contrast = theme_preferences_note (""); + ui->contrast_fix = gtk_button_new_with_label (_("Improve text contrast")); + gtk_widget_set_halign (ui->contrast_fix, GTK_ALIGN_START); + gtk_widget_set_no_show_all (ui->contrast_fix, TRUE); + gtk_box_pack_start (GTK_BOX (feedback), ui->contrast, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (feedback), ui->contrast_fix, FALSE, FALSE, 0); + g_signal_connect (ui->contrast_fix, "clicked", G_CALLBACK (theme_preferences_contrast_fix), ui); + gtk_box_pack_start (GTK_BOX (box), feedback, FALSE, FALSE, 0); + ui->search_entry = gtk_search_entry_new (); + gtk_entry_set_placeholder_text (GTK_ENTRY (ui->search_entry), _("Find a colour to adjust…")); + atk_object_set_name (gtk_widget_get_accessible (ui->search_entry), _("Find a colour to adjust")); + gtk_box_pack_start (GTK_BOX (box), ui->search_entry, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), theme_preferences_note (_("Adjust individual colours · Theme means it follows your application appearance.")), FALSE, FALSE, 0); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (common), GTK_SELECTION_NONE); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (advanced), GTK_SELECTION_NONE); + gtk_box_pack_start (GTK_BOX (box), common, FALSE, FALSE, 0); + ui->advanced = gtk_expander_new (_("Advanced colours · nicknames, IRC palette and other indicators")); + gtk_container_add (GTK_CONTAINER (ui->advanced), advanced); + gtk_box_pack_start (GTK_BOX (box), ui->advanced, FALSE, FALSE, 0); + for (ThemeSemanticToken token = 0; token < THEME_TOKEN_COUNT; token++) + { + theme_color_manager_row *row = g_new0 (theme_color_manager_row, 1); + GtkWidget *line = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); + GtkWidget *names = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + char *display = theme_preferences_token_display_name (token); + GtkWidget *name = gtk_label_new (display); + gtk_label_set_xalign (GTK_LABEL (name), 0); + gtk_label_set_line_wrap (GTK_LABEL (name), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (name), 22); + gtk_container_set_border_width (GTK_CONTAINER (line), 6); + row->row = gtk_list_box_row_new (); + gtk_container_add (GTK_CONTAINER (row->row), line); + row->status = theme_preferences_note (""); + gtk_box_pack_start (GTK_BOX (names), name, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (names), row->status, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (line), names, TRUE, TRUE, 0); + row->button = gtk_button_new (); + row->preview = gtk_event_box_new (); + GtkWidget *swatch = gtk_label_new (" "); + gtk_widget_set_size_request (swatch, 42, 20); + gtk_container_add (GTK_CONTAINER (row->preview), swatch); + gtk_container_add (GTK_CONTAINER (row->button), row->preview); + g_object_set_data (G_OBJECT (row->button), "zoitechat-color-box", row->preview); + char *choose_name = g_strdup_printf (_("Choose colour for %s"), display); + atk_object_set_name (gtk_widget_get_accessible (row->button), choose_name); + gtk_widget_set_tooltip_text (row->button, choose_name); + g_free (choose_name); + row->entry = gtk_entry_new (); + gtk_entry_set_width_chars (GTK_ENTRY (row->entry), 9); + gtk_entry_set_max_width_chars (GTK_ENTRY (row->entry), 9); + gtk_entry_set_max_length (GTK_ENTRY (row->entry), 32); + char *entry_name = g_strdup_printf (_("%s colour value"), display); + atk_object_set_name (gtk_widget_get_accessible (row->entry), entry_name); + g_free (entry_name); + gtk_widget_set_tooltip_text (row->entry, _("Optional: enter a colour name or a value such as #336699.")); + row->reset = gtk_button_new_from_icon_name ("edit-undo-symbolic", GTK_ICON_SIZE_BUTTON); + char *reset_name = g_strdup_printf (_("Use theme colour for %s"), display); + gtk_widget_set_tooltip_text (row->reset, reset_name); + atk_object_set_name (gtk_widget_get_accessible (row->reset), reset_name); + g_free (reset_name); + gtk_box_pack_start (GTK_BOX (line), row->button, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (line), row->entry, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (line), row->reset, FALSE, FALSE, 0); + row->token = token; row->color_change_flag = color_change_flag; + row->manager_ui = ui; row->parent = parent; + char *search_text = g_strdup_printf ("%s token_%d %s", display, token, + token < 32 ? "mIRC IRC palette nickname" : token == THEME_TOKEN_TEXT_FOREGROUND ? "foreground text" : + token == THEME_TOKEN_SELECTION_FOREGROUND ? "foreground selection" : "background tab channel interface"); row->search_text = g_utf8_strdown (search_text, -1); - g_free (token_code); g_free (search_text); - - if (theme_preferences_staged_get_color (token, &rgba)) - theme_preferences_manager_row_apply (row, &rgba); - - theme_preferences_color_set_tooltip (list_row, token); - - g_signal_connect (G_OBJECT (button), "clicked", - G_CALLBACK (theme_preferences_manager_pick_cb), row); - g_object_set_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui", ui); - g_signal_connect (G_OBJECT (entry), "activate", - G_CALLBACK (theme_preferences_manager_entry_activate_cb), row); - g_signal_connect (G_OBJECT (entry), "focus-out-event", - G_CALLBACK (theme_preferences_manager_entry_focus_out_cb), row); - - gtk_container_add (GTK_CONTAINER (list), list_row); + gboolean extra = token < THEME_TOKEN_SELECTION_FOREGROUND || token == THEME_TOKEN_MARKER || token == THEME_TOKEN_SPELL; + row->expander = extra ? ui->advanced : NULL; + gtk_container_add (GTK_CONTAINER (extra ? advanced : common), row->row); g_ptr_array_add (ui->rows, row); + GdkRGBA color; + theme_preferences_staged_get_color (token, &color); + theme_preferences_manager_row_apply (row, &color); + g_signal_connect (row->button, "clicked", G_CALLBACK (theme_preferences_manager_pick_cb), row); + g_signal_connect (row->reset, "clicked", G_CALLBACK (theme_preferences_manager_row_reset), row); + g_signal_connect (row->entry, "activate", G_CALLBACK (theme_preferences_manager_entry_activate_cb), row); + g_signal_connect (row->entry, "focus-out-event", G_CALLBACK (theme_preferences_manager_entry_focus_out_cb), row); g_free (display); } - - g_signal_connect (G_OBJECT (search), "changed", - G_CALLBACK (theme_preferences_manager_search_changed_cb), ui); - + const ThemeSemanticToken common_order[] = { + THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND, + THEME_TOKEN_SELECTION_BACKGROUND, THEME_TOKEN_SELECTION_FOREGROUND, + THEME_TOKEN_TAB_HIGHLIGHT, THEME_TOKEN_TAB_NEW_MESSAGE, THEME_TOKEN_TAB_NEW_DATA, THEME_TOKEN_TAB_AWAY + }; + for (guint i = 0; i < G_N_ELEMENTS (common_order); i++) + { + theme_color_manager_row *row = g_ptr_array_index (ui->rows, common_order[i]); + g_object_ref (row->row); + gtk_container_remove (GTK_CONTAINER (common), row->row); + gtk_list_box_insert (GTK_LIST_BOX (common), row->row, -1); + g_object_unref (row->row); + } + GtkWidget *reset = gtk_button_new_with_label (_("Reset chat colours")); + gtk_widget_set_halign (reset, GTK_ALIGN_START); + gtk_widget_set_tooltip_text (reset, _("Remove all custom chat colours and follow the application again. Cancel restores your previous choices.")); + g_signal_connect (reset, "clicked", G_CALLBACK (theme_preferences_manager_reset), ui); + gtk_box_pack_start (GTK_BOX (box), reset, FALSE, FALSE, 0); + g_object_set_data (G_OBJECT (box), "reset-button", reset); + g_signal_connect (ui->search_entry, "changed", G_CALLBACK (theme_preferences_manager_search_changed_cb), ui); + ui->listener = theme_listener_register ("preferences.appearance", theme_preferences_manager_theme_changed, ui); theme_preferences_manager_update_preview (ui); - - gtk_widget_show_all (dialog); - return dialog; + return box; } static void theme_preferences_manage_colors_cb (GtkWidget *button, gpointer user_data) { - gboolean *color_change_flag = user_data; - GtkWidget *dialog; - - dialog = theme_preferences_create_color_manager_dialog (GTK_WINDOW (gtk_widget_get_toplevel (button)), - color_change_flag); - gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy (dialog); - - if (color_change_flag) - *color_change_flag = theme_preferences_stage.active ? theme_preferences_stage.changed : *color_change_flag; + (void)button; (void)user_data; + setup_open_page ("colors"); } static void @@ -1211,112 +1495,92 @@ theme_preferences_read_import_color (const char *cfg, return TRUE; } +static gboolean +theme_preferences_import_scheme_path (const char *path, gboolean include_formatting, + gboolean *color_change_flag, gboolean *formatting_imported, GError **error) +{ + char *cfg = NULL, *pevents = NULL, *lower = g_ascii_strdown (path, -1); + GdkRGBA colors[THEME_TOKEN_COUNT]; + gboolean present[THEME_TOKEN_COUNT] = { FALSE }, any = FALSE, success = FALSE; + gboolean archive = g_str_has_suffix (lower, ".hct"); + if (formatting_imported) *formatting_imported = FALSE; + if (archive) + { + if (!zoitechat_gtk3_theme_service_read_archive_text_file (path, "colors.conf", &cfg, error)) goto done; + } + else if (!g_file_get_contents (path, &cfg, NULL, error)) goto done; + for (ThemeSemanticToken token = 0; token < THEME_TOKEN_COUNT; token++) + { + present[token] = theme_preferences_read_import_color (cfg, token, &colors[token]); + any |= present[token]; + } + if (!any) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("No recognised colours were found in this scheme.")); + goto done; + } + if (archive && include_formatting) + { + GError *optional_error = NULL; + zoitechat_gtk3_theme_service_read_archive_text_file (path, "pevents.conf", &pevents, &optional_error); + g_clear_error (&optional_error); + } + if (pevents) + { + /* Import into the same staged model as Message formatting. + * Invalid templates leave both models untouched. */ + setup_open_page ("events"); + gboolean imported = pevent_preferences_import_text (pevents, error); + setup_open_page ("appearance"); + if (!imported) goto done; + if (formatting_imported) *formatting_imported = TRUE; + } + for (ThemeSemanticToken token = 0; token < THEME_TOKEN_COUNT; token++) + if (present[token]) + { + theme_preferences_staged_set_color (token, &colors[token], color_change_flag, FALSE); + theme_runtime_user_set_color (token, &colors[token]); + } + theme_manager_apply_and_dispatch (prefs.hex_gui_dark_mode, + THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST, NULL); + success = TRUE; + done: + g_free (pevents); g_free (cfg); g_free (lower); + return success; +} + static void theme_preferences_import_colors_conf_cb (GtkWidget *button, gpointer user_data) { gboolean *color_change_flag = user_data; - GtkFileChooserNative *dialog; - char *path; - char *lower_path; - char *cfg; - char *pevents_cfg = NULL; - GError *error = NULL; - gboolean any_imported = FALSE; - gboolean imported_from_hct = FALSE; - gboolean imported_pevents = FALSE; - ThemeSemanticToken token; - GtkFileFilter *filter; - - dialog = gtk_file_chooser_native_new (_("Import colors.conf colors"), - GTK_WINDOW (gtk_widget_get_toplevel (button)), - GTK_FILE_CHOOSER_ACTION_OPEN, - _("_Import"), - _("_Cancel")); - gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (dialog), FALSE); - filter = gtk_file_filter_new (); - gtk_file_filter_set_name (filter, _("Theme colors (*.conf, *.hct)")); + GtkFileChooserNative *dialog = gtk_file_chooser_native_new (_("Import colour scheme"), + GTK_WINDOW (gtk_widget_get_toplevel (button)), GTK_FILE_CHOOSER_ACTION_OPEN, _("_Import"), _("_Cancel")); + GtkFileFilter *filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("Colour schemes (*.conf, *.hct)")); gtk_file_filter_add_pattern (filter, "*.conf"); gtk_file_filter_add_pattern (filter, "*.hct"); gtk_file_filter_add_pattern (filter, "*.HCT"); gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); - + gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (dialog), FALSE); if (gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog)) != GTK_RESPONSE_ACCEPT) { g_object_unref (dialog); return; } - - path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); + char *path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); g_object_unref (dialog); - if (!path) - return; - - lower_path = g_ascii_strdown (path, -1); - if (g_str_has_suffix (lower_path, ".hct")) - { - imported_from_hct = TRUE; - if (!zoitechat_gtk3_theme_service_read_archive_text_file (path, "colors.conf", &cfg, &error)) - { - theme_preferences_show_import_error (button, _("Failed to read colors.conf from .hct file.")); - g_clear_error (&error); - g_free (lower_path); - g_free (path); - return; - } - if (zoitechat_gtk3_theme_service_read_archive_text_file (path, "pevents.conf", &pevents_cfg, &error)) - { - char *pevents_path = g_build_filename (get_xdir (), "pevents.conf", NULL); - if (g_file_set_contents (pevents_path, pevents_cfg, -1, &error)) - { - load_text_events (); - imported_pevents = TRUE; - } - g_free (pevents_path); - g_clear_error (&error); - } - else - g_clear_error (&error); - } - else if (!g_file_get_contents (path, &cfg, NULL, &error)) - { - theme_preferences_show_import_error (button, _("Failed to read colors.conf file.")); - g_clear_error (&error); - g_free (lower_path); - g_free (path); - return; - } - g_free (lower_path); - - for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) - { - GdkRGBA rgba; - - if (!theme_preferences_read_import_color (cfg, token, &rgba)) - continue; - - theme_preferences_staged_set_color (token, &rgba, color_change_flag, TRUE); - any_imported = TRUE; - } - - if (!any_imported) - theme_preferences_show_import_error (button, _("No importable colors were found in that colors.conf file.")); - else if (imported_from_hct) - { - char *message = g_strdup_printf (imported_pevents ? - _("Imported colors.conf and pevents.conf from %s.") : - _("Imported colors.conf from %s."), - path); - theme_preferences_show_import_info (button, message); - g_free (message); - } - else if (color_change_flag) - *color_change_flag = theme_preferences_stage.active ? theme_preferences_stage.changed : *color_change_flag; - - if (any_imported && color_change_flag) - *color_change_flag = theme_preferences_stage.active ? theme_preferences_stage.changed : *color_change_flag; - - g_free (pevents_cfg); - g_free (cfg); + if (!path) return; + GtkWidget *include = g_object_get_data (G_OBJECT (button), "include-message-formatting"); + gboolean formatting = FALSE; + GError *error = NULL; + if (!theme_preferences_import_scheme_path (path, !include || gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (include)), + color_change_flag, &formatting, &error)) + theme_preferences_show_import_error (button, error ? error->message : _("Could not import this colour scheme.")); + else + theme_preferences_show_import_info (button, formatting ? + _("Colours and message formatting are ready to preview. Save keeps them; Cancel discards them.") : + _("Colours are ready to preview. Save keeps them; Cancel discards them.")); + g_clear_error (&error); g_free (path); } @@ -1577,52 +1841,6 @@ theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui) gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER); } -static void -theme_preferences_stage_rebase_color (ThemeSemanticToken token, const GdkRGBA *rgba) -{ - if (token < 0 || token >= THEME_TOKEN_COUNT || !rgba) - return; - if (!theme_preferences_stage.active) - return; - - /* The theme changed underneath the dialog: shift the baseline so the - * new theme-provided value is neither treated as a user edit nor - * pinned into the runtime palette. */ - theme_preferences_stage.snapshot[token] = *rgba; - theme_preferences_stage.staged[token] = *rgba; - theme_preferences_stage.snapshot_valid[token] = TRUE; - theme_preferences_stage.staged_valid[token] = TRUE; - theme_preferences_stage.staged_dirty[token] = FALSE; - theme_preferences_stage_recompute_changed (); -} - -static void -theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui) -{ - ThemeWidgetStyleValues style_values; - GtkWidget *style_source = NULL; - - if (ui && ui->parent) - style_source = GTK_WIDGET (ui->parent); - else if (ui && ui->gtk3_combo) - style_source = ui->gtk3_combo; - - theme_runtime_clear_gtk_mapped_custom_tokens (); - - theme_get_widget_style_values_for_widget (style_source, &style_values); - - /* Leave the mapped tokens unpinned so the user list, channel tree and - * input box keep resolving their colors from whatever GTK3 theme is - * active (system or in-app selected), and only refresh what the - * dialog shows. Pinning theme-derived values here freezes them into - * the runtime palette (and colors.conf), which left those widgets - * with stale colors until the next restart. */ - theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground); - theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background); - theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground); - theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background); -} - static gboolean theme_preferences_gtk3_apply_and_refresh (theme_preferences_ui *ui, GError **error) { @@ -1632,7 +1850,7 @@ theme_preferences_gtk3_apply_and_refresh (theme_preferences_ui *ui, GError **err if (ui && ui->parent) theme_manager_apply_to_window (GTK_WIDGET (ui->parent)); - theme_preferences_gtk3_sync_runtime_palette (ui); + theme_manager_apply_mode (prefs.hex_gui_dark_mode, NULL); theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK | THEME_CHANGED_REASON_PALETTE | @@ -1642,6 +1860,8 @@ theme_preferences_gtk3_apply_and_refresh (theme_preferences_ui *ui, GError **err return TRUE; } +static void theme_preferences_sync_cards (theme_preferences_ui *ui); + static void theme_preferences_gtk3_changed_cb (GtkComboBox *combo, gpointer user_data) { @@ -1664,6 +1884,9 @@ theme_preferences_gtk3_changed_cb (GtkComboBox *combo, gpointer user_data) variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM; else variant = theme_gtk3_variant_for_theme (id); + char previous_id[sizeof prefs.hex_gui_gtk3_theme]; + g_strlcpy (previous_id, prefs.hex_gui_gtk3_theme, sizeof previous_id); + int previous_variant = prefs.hex_gui_gtk3_variant; selection_changed = g_strcmp0 (prefs.hex_gui_gtk3_theme, id) != 0 || prefs.hex_gui_gtk3_variant != variant; g_strlcpy (prefs.hex_gui_gtk3_theme, id, sizeof (prefs.hex_gui_gtk3_theme)); @@ -1677,14 +1900,40 @@ theme_preferences_gtk3_changed_cb (GtkComboBox *combo, gpointer user_data) if (selection_changed && !theme_preferences_gtk3_apply_and_refresh (ui, &error)) { + g_strlcpy (prefs.hex_gui_gtk3_theme, previous_id, sizeof prefs.hex_gui_gtk3_theme); + prefs.hex_gui_gtk3_variant = previous_variant; + if (ui->setup_prefs) + { + g_strlcpy (ui->setup_prefs->hex_gui_gtk3_theme, previous_id, sizeof ui->setup_prefs->hex_gui_gtk3_theme); + ui->setup_prefs->hex_gui_gtk3_variant = previous_variant; + } + GtkTreeModel *model = gtk_combo_box_get_model (combo); + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + ui->gtk3_populating = TRUE; + while (valid) + { + char *candidate; + gtk_tree_model_get (model, &iter, GTK3_THEME_COL_ID, &candidate, -1); + gboolean match = !g_strcmp0 (candidate, previous_id); + g_free (candidate); + if (match) { gtk_combo_box_set_active_iter (combo, &iter); break; } + valid = gtk_tree_model_iter_next (model, &iter); + } + ui->gtk3_populating = FALSE; + theme_preferences_gtk3_sync_remove_state (ui); theme_preferences_show_message (ui, GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply GTK3 theme.")); g_clear_error (&error); } + theme_preferences_sync_cards (ui); g_free (id); } +static void theme_preferences_rebuild_cards (theme_preferences_ui *ui); +static void theme_preferences_sync_cards (theme_preferences_ui *ui); + static void theme_preferences_populate_gtk3 (theme_preferences_ui *ui) { @@ -1694,7 +1943,8 @@ theme_preferences_populate_gtk3 (theme_preferences_ui *ui) GtkTreeIter iter; int active = -1; gboolean removed_selected_theme = FALSE; - gboolean using_system_default = prefs.hex_gui_gtk3_theme[0] == '\0'; + gboolean using_system_default = prefs.hex_gui_gtk3_theme[0] == '\0' || + !strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_LIGHT) || !strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK); gboolean should_apply = FALSE; char *final_id; ThemeGtk3Variant final_variant = THEME_GTK3_VARIANT_PREFER_LIGHT; @@ -1706,7 +1956,7 @@ theme_preferences_populate_gtk3 (theme_preferences_ui *ui) gtk_tree_store_append (store, &iter, NULL); gtk_tree_store_set (store, &iter, GTK3_THEME_COL_ID, "", - GTK3_THEME_COL_LABEL, _("None"), + GTK3_THEME_COL_LABEL, _("Follow system"), GTK3_THEME_COL_SOURCE, ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM, -1); themes = zoitechat_gtk3_theme_service_discover (); @@ -1772,6 +2022,7 @@ theme_preferences_populate_gtk3 (theme_preferences_ui *ui) } g_ptr_array_unref (themes); + theme_preferences_rebuild_cards (ui); } static void @@ -1779,14 +2030,30 @@ theme_preferences_gtk3_import_path (theme_preferences_ui *ui, char *path) { char *id = NULL; GError *error = NULL; - - if (!zoitechat_gtk3_theme_service_import (path, &id, &error)) + gboolean imported = zoitechat_gtk3_theme_service_import (path, &id, &error); + if (!imported) theme_preferences_show_message (ui, GTK_MESSAGE_ERROR, - error ? error->message : _("Failed to import GTK3 theme.")); + error ? error->message : _("Failed to install theme.")); g_clear_error (&error); - g_free (id); g_free (path); theme_preferences_populate_gtk3 (ui); + if (imported && id) + { + GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo)); + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) + { + char *candidate; + gtk_tree_model_get (model, &iter, GTK3_THEME_COL_ID, &candidate, -1); + gboolean match = !g_strcmp0 (candidate, id); + g_free (candidate); + if (match) { gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->gtk3_combo), &iter); break; } + valid = gtk_tree_model_iter_next (model, &iter); + } + gtk_expander_set_expanded (GTK_EXPANDER (ui->theme_expander), TRUE); + } + g_free (id); } static void @@ -1847,94 +2114,268 @@ theme_preferences_gtk3_remove_cb (GtkWidget *button, gpointer user_data) theme_preferences_populate_gtk3 (ui); } -GtkWidget * -theme_preferences_create_page (GtkWindow *parent, - struct zoitechatprefs *setup_prefs, - gboolean *color_change_flag) +static gboolean +theme_preferences_thumbnail_draw (GtkWidget *widget, cairo_t *cr, gpointer data) { - theme_preferences_ui *ui; - GtkWidget *box; - GtkWidget *label; - GtkWidget *colors_frame; - GtkWidget *colors_box; - GtkWidget *manage_colors_button; - GtkWidget *import_colors_button; - GtkWidget *gtk3_frame; - GtkWidget *gtk3_grid; - GtkWidget *gtk3_button; - GtkTreeStore *gtk3_store; - GtkCellRenderer *renderer; + gboolean dark = GPOINTER_TO_INT (data); + double width = gtk_widget_get_allocated_width (widget), height = gtk_widget_get_allocated_height (widget); + cairo_set_source_rgb (cr, dark ? 0.12 : 0.94, dark ? 0.14 : 0.95, dark ? 0.18 : 0.96); + cairo_paint (cr); + cairo_set_source_rgb (cr, dark ? 0.19 : 0.82, dark ? 0.22 : 0.85, dark ? 0.27 : 0.88); + cairo_rectangle (cr, 0, 0, width * 0.26, height); cairo_fill (cr); + cairo_set_source_rgb (cr, 0.28, 0.55, 0.82); + cairo_rectangle (cr, width * 0.04, height * 0.23, width * 0.17, 5); cairo_fill (cr); + for (int i = 0; i < 3; i++) + { + cairo_set_source_rgb (cr, dark ? 0.75 : 0.30, dark ? 0.80 : 0.34, dark ? 0.87 : 0.40); + cairo_rectangle (cr, width * 0.33, height * (0.25 + i * 0.22), width * (i == 1 ? 0.43 : 0.57), 4); cairo_fill (cr); + } + (void)widget; + return FALSE; +} - ui = g_new0 (theme_preferences_ui, 1); - ui->parent = parent; - ui->setup_prefs = setup_prefs; +static GtkWidget * +theme_preferences_theme_thumbnail (gboolean dark) +{ + GtkWidget *drawing = gtk_drawing_area_new (); + gtk_widget_set_size_request (drawing, 110, 58); + gtk_widget_set_hexpand (drawing, TRUE); + g_signal_connect (drawing, "draw", G_CALLBACK (theme_preferences_thumbnail_draw), GINT_TO_POINTER (dark)); + return drawing; +} - box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6); - gtk_container_set_border_width (GTK_CONTAINER (box), 6); +static void +theme_preferences_sync_cards (theme_preferences_ui *ui) +{ + if (!ui->theme_cards) return; + gboolean populating = ui->gtk3_populating; + ui->gtk3_populating = TRUE; + for (int i = 0; i < 3; i++) + { + int mode = i == 0 ? ZOITECHAT_DARK_MODE_AUTO : i == 1 ? ZOITECHAT_DARK_MODE_LIGHT : ZOITECHAT_DARK_MODE_DARK; + theme_preferences_choice_selected (ui->style_buttons[i], + (!prefs.hex_gui_gtk3_theme[0] || g_str_has_prefix (prefs.hex_gui_gtk3_theme, "builtin:")) && prefs.hex_gui_dark_mode == mode); + } + GList *children = gtk_container_get_children (GTK_CONTAINER (ui->theme_cards)); + for (GList *i = children; i; i = i->next) + { + GtkWidget *button = gtk_bin_get_child (GTK_BIN (i->data)); + const char *id = g_object_get_data (G_OBJECT (button), "theme-id"); + theme_preferences_choice_selected (button, !g_strcmp0 (id, prefs.hex_gui_gtk3_theme)); + } + g_list_free (children); + ui->gtk3_populating = populating; +} - colors_frame = gtk_frame_new (_("Colors")); - gtk_box_pack_start (GTK_BOX (box), colors_frame, FALSE, FALSE, 0); - colors_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); - gtk_container_set_border_width (GTK_CONTAINER (colors_box), 6); - gtk_container_add (GTK_CONTAINER (colors_frame), colors_box); +static void +theme_preferences_style_clicked (GtkButton *button, gpointer data) +{ + theme_preferences_ui *ui = data; + if (ui->gtk3_populating) return; + int mode = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "appearance-mode")); + prefs.hex_gui_dark_mode = mode; + if (ui->setup_prefs) ui->setup_prefs->hex_gui_dark_mode = mode; + ui->gtk3_populating = TRUE; + gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 0); + ui->gtk3_populating = FALSE; + const char *id = mode == ZOITECHAT_DARK_MODE_AUTO ? "" : mode == ZOITECHAT_DARK_MODE_DARK ? THEME_GTK3_BUILTIN_DARK : THEME_GTK3_BUILTIN_LIGHT; + ThemeGtk3Variant variant = mode == ZOITECHAT_DARK_MODE_AUTO ? THEME_GTK3_VARIANT_FOLLOW_SYSTEM : + mode == ZOITECHAT_DARK_MODE_DARK ? THEME_GTK3_VARIANT_PREFER_DARK : THEME_GTK3_VARIANT_PREFER_LIGHT; + g_strlcpy (prefs.hex_gui_gtk3_theme, id, sizeof prefs.hex_gui_gtk3_theme); + prefs.hex_gui_gtk3_variant = variant; + if (ui->setup_prefs) + { + g_strlcpy (ui->setup_prefs->hex_gui_gtk3_theme, id, sizeof ui->setup_prefs->hex_gui_gtk3_theme); + ui->setup_prefs->hex_gui_gtk3_variant = variant; + } + theme_preferences_gtk3_apply_and_refresh (ui, NULL); + theme_preferences_sync_cards (ui); +} - label = gtk_label_new (_("GTK3 theme colors are used by default. Open the color manager to set custom colors.")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_label_set_xalign (GTK_LABEL (label), 0.0f); - gtk_box_pack_start (GTK_BOX (colors_box), label, FALSE, FALSE, 0); +static void +theme_preferences_theme_card_clicked (GtkButton *button, gpointer data) +{ + theme_preferences_ui *ui = data; + if (ui->gtk3_populating) return; + const char *id = g_object_get_data (G_OBJECT (button), "theme-id"); + GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo)); + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) + { + char *candidate; + gtk_tree_model_get (model, &iter, GTK3_THEME_COL_ID, &candidate, -1); + gboolean match = !g_strcmp0 (candidate, id); + g_free (candidate); + if (match) { gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->gtk3_combo), &iter); break; } + valid = gtk_tree_model_iter_next (model, &iter); + } + theme_preferences_sync_cards (ui); +} - manage_colors_button = gtk_button_new_with_label (_("Manage all client colors…")); - gtk_widget_set_halign (manage_colors_button, GTK_ALIGN_START); - gtk_box_pack_start (GTK_BOX (colors_box), manage_colors_button, FALSE, FALSE, 0); - g_signal_connect (G_OBJECT (manage_colors_button), "clicked", - G_CALLBACK (theme_preferences_manage_colors_cb), color_change_flag); +static gboolean +theme_preferences_theme_card_filter (GtkFlowBoxChild *child, gpointer data) +{ + theme_preferences_ui *ui = data; + GtkWidget *button = gtk_bin_get_child (GTK_BIN (child)); + char *needle = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (ui->theme_search)), -1); + const char *name = g_object_get_data (G_OBJECT (button), "theme-search"); + gboolean visible = !needle[0] || (name && strstr (name, needle)); + g_free (needle); + return visible; +} - import_colors_button = gtk_button_new_with_label (_("Import colors.conf colors…")); - gtk_widget_set_halign (import_colors_button, GTK_ALIGN_START); - gtk_box_pack_start (GTK_BOX (colors_box), import_colors_button, FALSE, FALSE, 0); - g_signal_connect (G_OBJECT (import_colors_button), "clicked", - G_CALLBACK (theme_preferences_import_colors_conf_cb), color_change_flag); +static void +theme_preferences_theme_search_changed (GtkEditable *entry, gpointer data) +{ + theme_preferences_ui *ui = data; + (void)entry; + gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (ui->theme_cards)); +} - gtk3_frame = gtk_frame_new (_("GTK3 Theme")); - gtk_box_pack_start (GTK_BOX (box), gtk3_frame, FALSE, FALSE, 0); - gtk3_grid = gtk_grid_new (); - gtk_container_set_border_width (GTK_CONTAINER (gtk3_grid), 6); - gtk_grid_set_row_spacing (GTK_GRID (gtk3_grid), 6); - gtk_grid_set_column_spacing (GTK_GRID (gtk3_grid), 6); - gtk_container_add (GTK_CONTAINER (gtk3_frame), gtk3_grid); - - label = gtk_label_new (_("GTK3 theme:")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_grid_attach (GTK_GRID (gtk3_grid), label, 0, 0, 1, 1); - gtk3_store = gtk_tree_store_new (GTK3_THEME_COL_COUNT, - G_TYPE_STRING, - G_TYPE_STRING, - G_TYPE_INT); - ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (gtk3_store)); - gtk_widget_set_hexpand (ui->gtk3_combo, TRUE); - g_object_unref (gtk3_store); - renderer = gtk_cell_renderer_text_new (); - gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, TRUE); - gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, "text", GTK3_THEME_COL_LABEL); - gtk_grid_attach (GTK_GRID (gtk3_grid), ui->gtk3_combo, 1, 0, 3, 1); - g_signal_connect (G_OBJECT (ui->gtk3_combo), "changed", G_CALLBACK (theme_preferences_gtk3_changed_cb), ui); - - gtk3_button = gtk_button_new_with_mnemonic (_("Import")); - gtk_grid_attach (GTK_GRID (gtk3_grid), gtk3_button, 0, 1, 1, 1); - g_signal_connect (G_OBJECT (gtk3_button), "clicked", G_CALLBACK (theme_preferences_gtk3_import_cb), ui); - - ui->gtk3_remove = gtk_button_new_with_mnemonic (_("Remove")); - gtk_grid_attach (GTK_GRID (gtk3_grid), ui->gtk3_remove, 1, 1, 1, 1); - g_signal_connect (G_OBJECT (ui->gtk3_remove), "clicked", G_CALLBACK (theme_preferences_gtk3_remove_cb), ui); - - gtk3_button = gtk_button_new_with_mnemonic (_("Open theme folder")); - gtk_grid_attach (GTK_GRID (gtk3_grid), gtk3_button, 2, 1, 2, 1); - g_signal_connect (G_OBJECT (gtk3_button), "clicked", G_CALLBACK (theme_preferences_open_gtk3_folder_cb), ui); - - theme_preferences_populate_gtk3 (ui); +static void +theme_preferences_rebuild_cards (theme_preferences_ui *ui) +{ + if (!ui->theme_cards) return; + GList *children = gtk_container_get_children (GTK_CONTAINER (ui->theme_cards)); + for (GList *i = children; i; i = i->next) gtk_widget_destroy (i->data); + g_list_free (children); + GPtrArray *themes = zoitechat_gtk3_theme_service_discover (); + for (guint i = 0; i < themes->len; i++) + { + ZoitechatGtk3Theme *theme = g_ptr_array_index (themes, i); + GtkWidget *button = gtk_toggle_button_new (); + GtkWidget *content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + GtkWidget *picture = NULL; + if (theme->thumbnail_path) + { + GdkPixbuf *image = gdk_pixbuf_new_from_file_at_scale (theme->thumbnail_path, 170, 72, TRUE, NULL); + if (image) { picture = gtk_image_new_from_pixbuf (image); g_object_unref (image); } + } + if (!picture) picture = gtk_image_new_from_icon_name ("preferences-desktop-theme", GTK_ICON_SIZE_DIALOG); + gtk_container_set_border_width (GTK_CONTAINER (content), 8); + gtk_box_pack_start (GTK_BOX (content), picture, FALSE, FALSE, 0); + GtkWidget *label = gtk_label_new (theme->display_name); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (label), 24); + gtk_box_pack_start (GTK_BOX (content), label, FALSE, FALSE, 0); + g_object_set_data (G_OBJECT (button), "choice-title", label); + g_object_set_data_full (G_OBJECT (button), "choice-name", g_strdup (theme->display_name), g_free); + gtk_box_pack_start (GTK_BOX (content), theme_preferences_note (theme->source == ZOITECHAT_GTK3_THEME_SOURCE_USER ? _("Installed by you") : _("Available on this computer")), FALSE, FALSE, 0); + gtk_container_add (GTK_CONTAINER (button), content); + atk_object_set_name (gtk_widget_get_accessible (button), theme->display_name); + g_object_set_data_full (G_OBJECT (button), "theme-id", g_strdup (theme->id), g_free); + g_object_set_data_full (G_OBJECT (button), "theme-search", g_utf8_casefold (theme->display_name, -1), g_free); + g_signal_connect (button, "clicked", G_CALLBACK (theme_preferences_theme_card_clicked), ui); + gtk_flow_box_insert (GTK_FLOW_BOX (ui->theme_cards), button, -1); + } + g_ptr_array_unref (themes); + gtk_widget_show_all (ui->theme_cards); + theme_preferences_sync_cards (ui); +} +GtkWidget * +theme_preferences_create_page (GtkWindow *parent, struct zoitechatprefs *setup_prefs, gboolean *color_change_flag) +{ + theme_preferences_ui *ui = g_new0 (theme_preferences_ui, 1); + GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 16); + ui->parent = parent; ui->setup_prefs = setup_prefs; g_object_set_data_full (G_OBJECT (box), "theme-preferences-ui", ui, g_free); - + GtkWidget *modes = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); + const char *names[] = { N_("Follow system"), N_("Light"), N_("Dark") }; + gtk_box_pack_start (GTK_BOX (box), theme_preferences_note (_("Application appearance · changes window controls, buttons and menus")), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), modes, FALSE, FALSE, 0); + for (int i = 0; i < 3; i++) + { + GtkWidget *button = gtk_toggle_button_new (); + GtkWidget *content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_container_set_border_width (GTK_CONTAINER (content), 6); + gtk_box_pack_start (GTK_BOX (content), theme_preferences_theme_thumbnail (i == 2), FALSE, FALSE, 0); + GtkWidget *title = gtk_label_new (_(names[i])); + gtk_box_pack_start (GTK_BOX (content), title, FALSE, FALSE, 0); + g_object_set_data (G_OBJECT (button), "choice-title", title); + g_object_set_data (G_OBJECT (button), "choice-name", (gpointer)_(names[i])); + gtk_container_add (GTK_CONTAINER (button), content); + atk_object_set_name (gtk_widget_get_accessible (button), _(names[i])); + int mode = i == 0 ? ZOITECHAT_DARK_MODE_AUTO : i == 1 ? ZOITECHAT_DARK_MODE_LIGHT : ZOITECHAT_DARK_MODE_DARK; + g_object_set_data (G_OBJECT (button), "appearance-mode", GINT_TO_POINTER (mode)); + g_signal_connect (button, "clicked", G_CALLBACK (theme_preferences_style_clicked), ui); + gtk_box_pack_start (GTK_BOX (modes), button, TRUE, TRUE, 0); + ui->style_buttons[i] = button; + } + gtk_box_pack_start (GTK_BOX (box), theme_preferences_note (_("Follow system uses your desktop theme. Light and Dark use the built-in appearance, with no theme installation needed.")), FALSE, FALSE, 0); + ui->theme_expander = gtk_expander_new (_("Installed themes")); + GtkWidget *installed = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + gtk_container_add (GTK_CONTAINER (ui->theme_expander), installed); + gtk_expander_set_expanded (GTK_EXPANDER (ui->theme_expander), prefs.hex_gui_gtk3_theme[0] != '\0'); + ui->theme_search = gtk_search_entry_new (); + gtk_entry_set_placeholder_text (GTK_ENTRY (ui->theme_search), _("Find an installed theme…")); + gtk_box_pack_start (GTK_BOX (installed), ui->theme_search, FALSE, FALSE, 0); + GtkWidget *scroller = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroller), 170); + gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (scroller), 260); + gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (scroller), TRUE); + ui->theme_cards = gtk_flow_box_new (); + gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (ui->theme_cards), GTK_SELECTION_NONE); + gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (ui->theme_cards), FALSE); + g_signal_connect (ui->theme_cards, "child-activated", G_CALLBACK (theme_preferences_flow_activate), NULL); + gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (ui->theme_cards), 2); + gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (ui->theme_cards), 2); + gtk_flow_box_set_column_spacing (GTK_FLOW_BOX (ui->theme_cards), 8); + gtk_flow_box_set_row_spacing (GTK_FLOW_BOX (ui->theme_cards), 8); + gtk_flow_box_set_filter_func (GTK_FLOW_BOX (ui->theme_cards), theme_preferences_theme_card_filter, ui, NULL); + gtk_container_add (GTK_CONTAINER (scroller), ui->theme_cards); + gtk_box_pack_start (GTK_BOX (installed), scroller, FALSE, FALSE, 0); + g_signal_connect (ui->theme_search, "changed", G_CALLBACK (theme_preferences_theme_search_changed), ui); + GtkWidget *tools = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8); + GtkWidget *import = gtk_button_new_with_label (_("Install theme…")); + ui->gtk3_remove = gtk_button_new_with_label (_("Remove selected theme")); + GtkWidget *folder = gtk_button_new_with_label (_("Open theme folder")); + gtk_box_pack_start (GTK_BOX (tools), import, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (tools), ui->gtk3_remove, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (tools), folder, FALSE, FALSE, 0); + g_signal_connect (import, "clicked", G_CALLBACK (theme_preferences_gtk3_import_cb), ui); + g_signal_connect (ui->gtk3_remove, "clicked", G_CALLBACK (theme_preferences_gtk3_remove_cb), ui); + g_signal_connect (folder, "clicked", G_CALLBACK (theme_preferences_open_gtk3_folder_cb), ui); + gtk_box_pack_start (GTK_BOX (installed), tools, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (installed), theme_preferences_note (_("Installing or removing theme files happens immediately. Your appearance selection is kept only when you save Preferences.")), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), ui->theme_expander, FALSE, FALSE, 0); + GtkTreeStore *store = gtk_tree_store_new (GTK3_THEME_COL_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT); + /* Keep one selection model shared by import/remove and the visual cards. */ + ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + g_object_unref (store); + g_object_ref_sink (ui->gtk3_combo); + g_object_set_data_full (G_OBJECT (box), "theme-selection-model", ui->gtk3_combo, g_object_unref); + g_signal_connect (ui->gtk3_combo, "changed", G_CALLBACK (theme_preferences_gtk3_changed_cb), ui); + theme_preferences_populate_gtk3 (ui); + theme_preferences_rebuild_cards (ui); + gtk_box_pack_start (GTK_BOX (box), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), theme_preferences_note (_("Chat colours · changes messages, nicknames and channel indicators")), FALSE, FALSE, 0); + ui->color_editor = theme_preferences_create_color_editor (parent, color_change_flag); + gtk_box_pack_start (GTK_BOX (box), ui->color_editor, FALSE, FALSE, 0); + g_object_set_data (G_OBJECT (box), "appearance-color-editor", ui->color_editor); + GtkWidget *files = gtk_expander_new (_("Import colours and message formatting")); + GtkWidget *file_tools = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + gtk_container_add (GTK_CONTAINER (files), file_tools); + import = gtk_button_new_with_label (_("Import colour scheme…")); + gtk_widget_set_halign (import, GTK_ALIGN_START); + GtkWidget *formatting = gtk_check_button_new_with_label (_("Include message formatting when the theme file provides it")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (formatting), TRUE); + g_object_set_data (G_OBJECT (import), "include-message-formatting", formatting); + g_signal_connect (import, "clicked", G_CALLBACK (theme_preferences_import_colors_conf_cb), color_change_flag); + gtk_box_pack_start (GTK_BOX (file_tools), import, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (file_tools), formatting, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (file_tools), theme_preferences_note (_("Supports existing colours.conf / colors.conf files and HexChat theme files (.hct). Imported choices wait for Save; Cancel discards them.")), FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), files, FALSE, FALSE, 0); + GtkWidget *stripping = gtk_expander_new (_("Ignore colours sent by other people")); + GtkWidget *strip_grid = gtk_grid_new (); + gtk_grid_set_row_spacing (GTK_GRID (strip_grid), 6); + theme_preferences_create_strip_toggle (strip_grid, 0, _("Messages"), &setup_prefs->hex_text_stripcolor_msg); + theme_preferences_create_strip_toggle (strip_grid, 1, _("Scrollback"), &setup_prefs->hex_text_stripcolor_replay); + theme_preferences_create_strip_toggle (strip_grid, 2, _("Topic"), &setup_prefs->hex_text_stripcolor_topic); + gtk_container_add (GTK_CONTAINER (stripping), strip_grid); + gtk_box_pack_start (GTK_BOX (box), stripping, FALSE, FALSE, 0); return box; } diff --git a/src/fe-gtk/theme/theme-preferences.h b/src/fe-gtk/theme/theme-preferences.h index 1f56d063..09489cf2 100644 --- a/src/fe-gtk/theme/theme-preferences.h +++ b/src/fe-gtk/theme/theme-preferences.h @@ -37,5 +37,8 @@ void theme_preferences_stage_begin (void); void theme_preferences_stage_apply (void); void theme_preferences_stage_commit (void); void theme_preferences_stage_discard (void); +void theme_preferences_stage_revert_runtime (void); +GtkWidget *theme_preferences_create_color_editor (GtkWindow *parent, gboolean *color_change_flag); +gboolean theme_preferences_color_editor_stage (GtkWidget *editor, GError **error); #endif diff --git a/src/fe-gtk/theme/theme-runtime.c b/src/fe-gtk/theme/theme-runtime.c index 15e13e11..9a7d3713 100644 --- a/src/fe-gtk/theme/theme-runtime.c +++ b/src/fe-gtk/theme/theme-runtime.c @@ -367,6 +367,37 @@ theme_runtime_get_xtext_colors_mapped (const ThemeGtkPaletteMap *gtk_map, XTextC theme_palette_to_xtext_colors (&mapped_palette, palette, palette_len); } +gboolean +theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color) +{ + if (token < 0 || token >= THEME_TOKEN_COUNT || !light_custom_tokens[token]) + return FALSE; + return theme_palette_get_color (&light_palette, token, color); +} + +gboolean +theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color) +{ + ThemePalette defaults; + gboolean custom[THEME_TOKEN_COUNT] = { FALSE }; + theme_palette_from_legacy_colors (&defaults, legacy_light_defaults, G_N_ELEMENTS (legacy_light_defaults)); + theme_runtime_apply_gtk_map (&defaults, map, custom); + return theme_palette_get_color (&defaults, token, color); +} + +void +theme_runtime_reset_user_color (ThemeSemanticToken token) +{ + GdkRGBA color; + if (!theme_runtime_get_default_color (token, NULL, &color)) + return; + if (!user_colors_valid) + light_palette = active_palette; + theme_palette_set_color (&light_palette, token, &color); + light_custom_tokens[token] = FALSE; + user_colors_valid = TRUE; +} + void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col) { diff --git a/src/fe-gtk/theme/theme-runtime.h b/src/fe-gtk/theme/theme-runtime.h index 12923708..d37fc1c1 100644 --- a/src/fe-gtk/theme/theme-runtime.h +++ b/src/fe-gtk/theme/theme-runtime.h @@ -44,6 +44,9 @@ gboolean theme_runtime_save_finalize (const char *temp_path); void theme_runtime_save_discard (const char *temp_path); gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed); gboolean theme_runtime_apply_dark_mode (gboolean enable); +gboolean theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color); +void theme_runtime_reset_user_color (ThemeSemanticToken token); +gboolean theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color); void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col); void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col); void theme_runtime_reset_mode_colors (gboolean dark_mode);