diff --git a/src/common/text.c b/src/common/text.c
index c2b389c4..5cdd4fb7 100644
--- a/src/common/text.c
+++ b/src/common/text.c
@@ -68,7 +68,7 @@ static ca_context *ca_con;
#define SCROLLBACK_MAX 32000
static void mkdir_p (char *filename);
-static char *log_create_filename (char *channame);
+static char *log_create_filename (const char *channame);
static char *
scrollback_get_filename (session *sess)
@@ -396,7 +396,7 @@ mkdir_p (char *filename)
}
static char *
-log_create_filename (char *channame)
+log_create_filename (const char *channame)
{
char *tmp, *ret;
int mbl;
@@ -453,7 +453,7 @@ log_escape_strcpy (char *dest, char *src, char *end)
/* substitutes %c %n %s into buffer */
static void
-log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
+log_insert_vars (char *buf, int bufsize, const char *fmt, char *c, char *n, char *s)
{
char *end = buf + bufsize;
@@ -466,6 +466,14 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
return;
case '%':
+ /* A partially typed custom format can end in a literal percent. */
+ if (!fmt[1])
+ {
+ buf[0] = '%';
+ if (buf + 1 < end) buf[1] = 0;
+ else buf[0] = 0;
+ return;
+ }
fmt++;
switch (fmt[0])
{
@@ -479,6 +487,11 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
buf = log_escape_strcpy (buf, s, end);
break;
default:
+ if (buf + 1 == end)
+ {
+ buf[0] = 0;
+ return;
+ }
buf[0] = '%';
buf++;
buf[0] = fmt[0];
@@ -500,61 +513,74 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
}
}
-static char *
-log_create_pathname (char *servname, char *channame, char *netname)
+char *
+log_build_pathname (const char *mask, const char *config_dir,
+ const char *servname, const char *channame,
+ const char *netname, time_t now)
{
char fname[384];
char fnametime[384];
- time_t now;
+ char *safe_channel, *safe_network, *safe_server;
+
+ if (!mask || !*mask || !config_dir || !servname || !channame ||
+ !g_utf8_validate (mask, -1, NULL))
+ return NULL;
if (!netname)
{
- netname = g_strdup ("NETWORK");
+ safe_network = g_strdup ("NETWORK");
}
else
{
- netname = log_create_filename (netname);
+ safe_network = log_create_filename (netname);
}
/* first, everything is in UTF-8 */
if (!rfc_casecmp (channame, servname))
{
- channame = g_strdup ("server");
+ safe_channel = g_strdup ("server");
}
else
{
- channame = log_create_filename (channame);
+ safe_channel = log_create_filename (channame);
}
- servname = log_create_filename (servname);
+ safe_server = log_create_filename (servname);
- log_insert_vars (fname, sizeof (fname), prefs.hex_irc_logmask, channame, netname, servname);
- g_free (channame);
- g_free (netname);
- g_free (servname);
+ log_insert_vars (fname, sizeof (fname), mask, safe_channel, safe_network, safe_server);
+ g_free (safe_channel);
+ g_free (safe_network);
+ g_free (safe_server);
/* insert time/date */
- now = time (NULL);
- strftime_utf8 (fnametime, sizeof (fnametime), fname, now);
+ if (!g_utf8_validate (fname, -1, NULL) ||
+ !strftime_utf8 (fnametime, sizeof (fnametime), fname, now))
+ return NULL;
/* If one uses log mask variables, such as "%c/...", %c will be empty upon
* connecting since there's no channel name yet, so we have to make sure
* we won't try to write to the FS root. */
- if (g_path_is_absolute (prefs.hex_irc_logmask))
+ if (g_path_is_absolute (mask))
{
g_snprintf (fname, sizeof (fname), "%s", fnametime);
}
else /* relative path */
{
- g_snprintf (fname, sizeof (fname), "%s" G_DIR_SEPARATOR_S "logs" G_DIR_SEPARATOR_S "%s", get_xdir (), fnametime);
+ g_snprintf (fname, sizeof (fname), "%s" G_DIR_SEPARATOR_S "logs" G_DIR_SEPARATOR_S "%s", config_dir, fnametime);
}
- /* create all the subdirectories */
- mkdir_p (fname);
-
return g_strdup (fname);
}
+static char *
+log_create_pathname (char *servname, char *channame, char *netname)
+{
+ char *filename = log_build_pathname (prefs.hex_irc_logmask, get_xdir (),
+ servname, channame, netname, time (NULL));
+ if (filename) mkdir_p (filename);
+ return filename;
+}
+
static int
log_open_file (char *servname, char *channame, char *netname)
{
diff --git a/src/common/text.h b/src/common/text.h
index f164263e..1c4f0c66 100644
--- a/src/common/text.h
+++ b/src/common/text.h
@@ -47,6 +47,11 @@ void PrintTextf (session *sess, const char *format, ...) G_GNUC_PRINTF (2, 3);
void PrintTextTimeStampf (session *sess, time_t timestamp, const char *format, ...) G_GNUC_PRINTF (3, 4);
void log_close (session *sess);
void log_open_or_close (session *sess);
+/* Resolve a log filename without creating directories or changing preferences.
+ * The caller owns the UTF-8 result; NULL means the format could not be expanded. */
+char *log_build_pathname (const char *mask, const char *config_dir,
+ const char *servname, const char *channame,
+ const char *netname, time_t now);
void load_text_events (void);
void pevent_save (char *fn);
int pevt_build_string (const char *input, char **output, int *max_arg);
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/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj
index d1fa5bf6..8d132060 100644
--- a/src/fe-gtk/fe-gtk.vcxproj
+++ b/src/fe-gtk/fe-gtk.vcxproj
@@ -63,6 +63,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
+
@@ -113,6 +114,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
+
diff --git a/src/fe-gtk/fe-gtk.vcxproj.filters b/src/fe-gtk/fe-gtk.vcxproj.filters
index 0c218a8a..d2317452 100644
--- a/src/fe-gtk/fe-gtk.vcxproj.filters
+++ b/src/fe-gtk/fe-gtk.vcxproj.filters
@@ -57,6 +57,9 @@
Header Files
+
+ Header Files
+
Header Files
@@ -194,6 +197,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/src/fe-gtk/fkeys.c b/src/fe-gtk/fkeys.c
index a5b5b888..e8dcf4ee 100644
--- a/src/fe-gtk/fkeys.c
+++ b/src/fe-gtk/fkeys.c
@@ -58,6 +58,9 @@
#ifdef __APPLE__
#include "macos-shortcuts.h"
#endif
+#include "setup.h"
+#include "preferences-persistence.h"
+#include "preferences-pages.h"
#define ICON_FKEYS_NEW "document-new"
#define ICON_FKEYS_DELETE "edit-delete"
@@ -110,7 +113,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,
@@ -159,53 +162,6 @@ static int key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt,
static GSList *keybind_list = NULL;
-#define KEY_DIALOG_THEME_LISTENER_ID_KEY "fkeys.theme-listener-id"
-
-static void
-key_dialog_theme_apply (GtkWidget *window)
-{
- GtkWidget *xtext;
- XTextColor xtext_palette[XTEXT_COLS];
-
- if (!window)
- return;
-
- xtext = g_object_get_data (G_OBJECT (window), "xtext");
- if (!xtext)
- return;
-
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
- gtk_xtext_set_palette (GTK_XTEXT (xtext), xtext_palette);
-}
-
-static void
-key_dialog_theme_changed (const ThemeChangedEvent *event, gpointer userdata)
-{
- GtkWidget *window = userdata;
-
- if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) &&
- !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) &&
- !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE))
- return;
-
- key_dialog_theme_apply (window);
-}
-
-static void
-key_dialog_theme_destroy_cb (GtkWidget *widget, gpointer userdata)
-{
- guint listener_id;
-
- (void) userdata;
-
- listener_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY));
- if (listener_id)
- {
- theme_listener_unregister (listener_id);
- g_object_set_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY, NULL);
- }
-}
-
static gsize
key_action_decode_escapes (const char *input, char *output)
{
@@ -365,69 +321,6 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
"ACCEL=F1\nMenu Shortcut\nD1:contents\nD2!\n\n"\
"ACCEL=T\nReopen Closed Tab\nD1!\nD2!\n\n"
-static gboolean
-key_builtin_data_match (char *line, char *data)
-{
- if (line[2] == '!')
- return data == NULL || data[0] == 0;
-
- if (line[2] == ':')
- return !strcmp (&line[3], data ? data : "");
-
- return FALSE;
-}
-
-static gboolean
-key_binding_is_builtin (struct key_binding *kb)
-{
- char *buf, *ibuf;
- char *action;
- int pnt = 0;
- int state = 0;
- gboolean match = FALSE;
- gboolean d1_match = FALSE;
- off_t size;
-
- if (kb->action < 0 || kb->action > KEY_MAX_ACTIONS)
- return FALSE;
-
- action = key_actions[kb->action].name;
- ibuf = g_strdup (default_kb_cfg);
- size = strlen (default_kb_cfg);
-
- while (buf_get_line (ibuf, &buf, &pnt, size))
- {
- if (strlen (buf) == 0)
- continue;
-
- switch (state)
- {
- case 0:
- state = 1;
- break;
- case 1:
- match = !strcmp (buf, action);
- state = 2;
- break;
- case 2:
- d1_match = match && key_builtin_data_match (buf, kb->data1);
- state = 3;
- break;
- case 3:
- if (d1_match && key_builtin_data_match (buf, kb->data2))
- {
- g_free (ibuf);
- return TRUE;
- }
- state = 0;
- break;
- }
- }
-
- g_free (ibuf);
- return FALSE;
-}
-
void
key_init (void)
{
@@ -619,13 +512,13 @@ key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod)
enum
{
- KEY_COLUMN,
- ACCEL_COLUMN,
- ACTION_COLUMN,
- D1_COLUMN,
- D2_COLUMN,
- CUSTOM_COLUMN,
- N_COLUMNS
+ KEY_COLUMN = PREFERENCES_SHORTCUT_LABEL,
+ ACCEL_COLUMN = PREFERENCES_SHORTCUT_ACCEL,
+ ACTION_COLUMN = PREFERENCES_SHORTCUT_ACTION,
+ D1_COLUMN = PREFERENCES_SHORTCUT_DATA1,
+ D2_COLUMN = PREFERENCES_SHORTCUT_DATA2,
+ CUSTOM_COLUMN = PREFERENCES_SHORTCUT_CUSTOM,
+ N_COLUMNS = PREFERENCES_SHORTCUT_N_COLUMNS
};
static GtkWidget *key_dialog = NULL;
@@ -633,280 +526,15 @@ static GtkWidget *key_dialog = NULL;
static inline GtkTreeModel *
get_store (void)
{
- return gtk_tree_view_get_model (g_object_get_data (G_OBJECT (key_dialog), "view"));
-}
-
-static void
-key_dialog_print_text (GtkXText *xtext, char *text)
-{
- unsigned int old = prefs.hex_stamp_text;
- prefs.hex_stamp_text = 0; /* temporarily disable stamps */
- gtk_xtext_clear (GTK_XTEXT (xtext)->buffer, 0);
- PrintTextRaw (GTK_XTEXT (xtext)->buffer, text, 0, 0);
- prefs.hex_stamp_text = old;
-}
-
-static void
-key_dialog_set_key (GtkCellRendererAccel *accel, gchar *pathstr, guint accel_key,
- GdkModifierType accel_mods, guint hardware_keycode, gpointer userdata)
-{
- GtkTreeModel *model = get_store ();
- GtkTreePath *path = gtk_tree_path_new_from_string (pathstr);
- GtkTreeIter iter;
- gchar *label_name, *accel_name;
-
- /* Shift tab requires an exception, hopefully that list ends here.. */
- if (accel_key == GDK_KEY_Tab && accel_mods & GDK_SHIFT_MASK)
- accel_key = GDK_KEY_ISO_Left_Tab;
-
- label_name = gtk_accelerator_get_label (accel_key, key_modifier_get_valid (accel_mods));
- accel_name = gtk_accelerator_name (accel_key, key_modifier_get_valid (accel_mods));
-
- gtk_tree_model_get_iter (model, &iter, path);
- gtk_list_store_set (GTK_LIST_STORE (model), &iter, KEY_COLUMN, label_name,
- ACCEL_COLUMN, accel_name, -1);
-
- gtk_tree_path_free (path);
- g_free (label_name);
- g_free (accel_name);
-}
-
-static void
-key_dialog_combo_changed (GtkCellRendererCombo *combo, gchar *pathstr,
- GtkTreeIter *new_iter, gpointer data)
-{
- GtkTreeModel *model;
- GtkXText *xtext;
- gchar *actiontext = NULL;
- gint action;
-
- xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
- model = GTK_TREE_MODEL (data);
-
- gtk_tree_model_get (model, new_iter, 0, &actiontext, -1);
-
- if (actiontext)
- {
-#ifdef WIN32
- /* We need to manually update the store */
- GtkTreePath *path;
- GtkTreeIter iter;
-
- path = gtk_tree_path_new_from_string (pathstr);
- model = get_store ();
-
- gtk_tree_model_get_iter (model, &iter, path);
- gtk_list_store_set (GTK_LIST_STORE (model), &iter, ACTION_COLUMN, actiontext, -1);
-
- gtk_tree_path_free (path);
-#endif
-
- action = key_get_action_from_string (actiontext);
- key_dialog_print_text (xtext, key_actions[action].help);
-
- g_free (actiontext);
- }
-}
-
-static void
-key_dialog_entry_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpointer data)
-{
- GtkTreeModel *model = get_store ();
- GtkTreePath *path = gtk_tree_path_new_from_string (pathstr);
- GtkTreeIter iter;
- gint column = GPOINTER_TO_INT (data);
-
- gtk_tree_model_get_iter (model, &iter, path);
- gtk_list_store_set (GTK_LIST_STORE (model), &iter, column, new_text, -1);
-
- gtk_tree_path_free (path);
-}
-
-static gboolean
-key_dialog_keypress (GtkWidget *wid, GdkEventKey *evt, gpointer userdata)
-{
- GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
- GtkTreeModel *store;
- GtkTreeIter iter1, iter2;
- GtkTreeSelection *sel;
- GtkTreePath *path;
- gboolean handled = FALSE;
- int delta;
-
- if (evt->state & GDK_SHIFT_MASK)
- {
- if (evt->keyval == GDK_KEY_Up)
- {
- handled = TRUE;
- delta = -1;
- }
- else if (evt->keyval == GDK_KEY_Down)
- {
- handled = TRUE;
- delta = 1;
- }
- }
-
- if (handled)
- {
- gboolean custom1, custom2;
-
- sel = gtk_tree_view_get_selection (view);
- if (!gtk_tree_selection_get_selected (sel, &store, &iter1))
- return FALSE;
-
- path = gtk_tree_model_get_path (store, &iter1);
- if (delta == 1)
- gtk_tree_path_next (path);
- else if (!gtk_tree_path_prev (path))
- {
- gtk_tree_path_free (path);
- return FALSE;
- }
-
- if (!gtk_tree_model_get_iter (store, &iter2, path))
- {
- gtk_tree_path_free (path);
- return FALSE;
- }
-
- gtk_tree_path_free (path);
- gtk_tree_model_get (store, &iter1, CUSTOM_COLUMN, &custom1, -1);
- gtk_tree_model_get (store, &iter2, CUSTOM_COLUMN, &custom2, -1);
- if (custom1 && custom2)
- gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
- }
-
- return handled;
-}
-
-static void
-key_dialog_selection_changed (GtkTreeSelection *sel, gpointer userdata)
-{
- GtkTreeModel *model;
- GtkTreeIter iter;
- GtkXText *xtext;
- GtkWidget *delete_button;
- char *actiontext;
- gboolean custom;
- int action;
-
- delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
- if (!gtk_tree_selection_get_selected (sel, &model, &iter) || model == NULL)
- {
- if (delete_button)
- gtk_widget_set_sensitive (delete_button, FALSE);
- return;
- }
-
- xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
- gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, CUSTOM_COLUMN, &custom, -1);
- if (delete_button)
- gtk_widget_set_sensitive (delete_button, custom);
-
- if (actiontext)
- {
- action = key_get_action_from_string (actiontext);
- key_dialog_print_text (xtext, key_actions[action].help);
- g_free (actiontext);
- }
- else
- 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)
-{
- GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
- GtkTreeViewColumn *col;
- GtkListStore *store = GTK_LIST_STORE (get_store ());
- GtkTreeIter iter;
- GtkTreePath *path;
-
- gtk_list_store_append (store, &iter);
- gtk_list_store_set (store, &iter, CUSTOM_COLUMN, TRUE, -1);
-
- /* make sure the new row is visible and selected */
- path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
- col = gtk_tree_view_get_column (view, ACTION_COLUMN);
- gtk_tree_view_scroll_to_cell (view, path, NULL, FALSE, 0.0, 0.0);
- gtk_tree_view_set_cursor (view, path, col, TRUE);
- gtk_tree_path_free (path);
+ return g_object_get_data (G_OBJECT (key_dialog), "bindings-store");
}
static char *
key_binding_signature (const char *action, const char *data1, const char *data2)
{
- return g_strdup_printf ("%s\\n%s\\n%s", action ? action : "", data1 ? data1 : "", data2 ? data2 : "");
+ const char *a = action ? action : "", *b = data1 ? data1 : "", *c = data2 ? data2 : "";
+ return g_strdup_printf ("%" G_GSIZE_FORMAT ":%s%" G_GSIZE_FORMAT ":%s%" G_GSIZE_FORMAT ":%s",
+ strlen (a), a, strlen (b), b, strlen (c), c);
}
static int
@@ -1031,309 +659,152 @@ key_dialog_reset (GtkWidget *wid, gpointer userdata)
g_slist_free_full (list, key_free);
}
-static void
-key_dialog_delete (GtkWidget *wid, gpointer userdata)
-{
- GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
- GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view));
- GtkTreeIter iter;
- GtkTreePath *path;
- gboolean custom;
-
- if (gtkutil_treeview_get_selected (view, &iter, -1))
- {
- gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, CUSTOM_COLUMN, &custom, -1);
- if (!custom)
- return;
-
- /* delete this row, select next one */
- if (gtk_list_store_remove (store, &iter))
- {
- path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
- gtk_tree_view_scroll_to_cell (view, path, NULL, TRUE, 1.0, 0.0);
- gtk_tree_view_set_cursor (view, path, NULL, FALSE);
- gtk_tree_path_free (path);
- }
- }
-}
-
-static GtkWidget *
-key_dialog_treeview_new (GtkWidget *box)
-{
- GtkWidget *scroll;
- GtkListStore *store, *combostore;
- GtkTreeViewColumn *col;
- GtkWidget *view;
- GtkCellRenderer *render;
- int i;
-
- 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_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
-
- store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
- G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
- g_return_val_if_fail (store != NULL, NULL);
-
- view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
- gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (view), TRUE);
- gtk_tree_view_set_enable_search (GTK_TREE_VIEW (view), FALSE);
- gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), FALSE);
-
- g_signal_connect (G_OBJECT (view), "key-press-event",
- G_CALLBACK (key_dialog_keypress), NULL);
- g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW(view))),
- "changed", G_CALLBACK (key_dialog_selection_changed), NULL);
-
- gtk_widget_set_name (view, "fkeys-treeview");
- if (!theme_gtk3_is_active ())
- {
- GtkCssProvider *provider = gtk_css_provider_new ();
-
- gtk_css_provider_load_from_data (
- provider,
- "treeview#fkeys-treeview row:nth-child(odd) {"
- " background-color: @theme_base_color;"
- "}"
- "treeview#fkeys-treeview row:nth-child(even) {"
- " background-color: shade(@theme_base_color, 0.96);"
- "}",
- -1,
- NULL);
- theme_css_apply_widget_provider (view, GTK_STYLE_PROVIDER (provider));
- g_object_unref (provider);
- }
-
- render = gtk_cell_renderer_accel_new ();
- g_object_set (render, "editable", TRUE,
-#ifndef WIN32
- "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER,
-#endif
- NULL);
- g_signal_connect (G_OBJECT (render), "accel-edited",
- G_CALLBACK (key_dialog_set_key), NULL);
- gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), KEY_COLUMN,
- "Key", render,
- "text", KEY_COLUMN,
- NULL);
-
- render = gtk_cell_renderer_text_new ();
- gtk_tree_view_insert_column_with_attributes (
- GTK_TREE_VIEW (view), ACCEL_COLUMN,
- "Accel", render,
- "text", ACCEL_COLUMN,
- NULL);
-
- combostore = gtk_list_store_new (1, G_TYPE_STRING);
- for (i = 0; i <= KEY_MAX_ACTIONS; i++)
- {
- GtkTreeIter iter;
-
- if (key_actions[i].name[0])
- {
- gtk_list_store_append (combostore, &iter);
- gtk_list_store_set (combostore, &iter, 0, key_actions[i].name, -1);
- }
- }
-
- render = gtk_cell_renderer_combo_new ();
- g_object_set (G_OBJECT (render), "model", combostore,
- "has-entry", FALSE,
- "editable", TRUE,
- "text-column", 0,
- NULL);
- g_signal_connect (G_OBJECT (render), "edited",
- G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (ACTION_COLUMN));
- g_signal_connect (G_OBJECT (render), "changed",
- G_CALLBACK (key_dialog_combo_changed), combostore);
- gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), ACTION_COLUMN,
- "Action", render,
- "text", ACTION_COLUMN,
- "editable", CUSTOM_COLUMN,
- NULL);
-
- render = gtk_cell_renderer_text_new ();
- g_object_set (render, "editable", TRUE, NULL);
- g_signal_connect (G_OBJECT (render), "edited",
- G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (D1_COLUMN));
- gtk_tree_view_insert_column_with_attributes (
- GTK_TREE_VIEW (view), D1_COLUMN,
- "Data1", render,
- "text", D1_COLUMN,
- "editable", CUSTOM_COLUMN,
- NULL);
-
- render = gtk_cell_renderer_text_new ();
- g_object_set (render, "editable", TRUE, NULL);
- g_signal_connect (G_OBJECT (render), "edited",
- G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (D2_COLUMN));
- gtk_tree_view_insert_column_with_attributes (
- GTK_TREE_VIEW (view), D2_COLUMN,
- "Data2", render,
- "text", D2_COLUMN,
- "editable", CUSTOM_COLUMN,
- NULL);
-
- col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), KEY_COLUMN);
- gtk_tree_view_column_set_fixed_width (col, 200);
- gtk_tree_view_column_set_resizable (col, TRUE);
- col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), ACCEL_COLUMN);
- gtk_tree_view_column_set_visible (col, FALSE);
- col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), ACTION_COLUMN);
- gtk_tree_view_column_set_fixed_width (col, 160);
- col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), D1_COLUMN);
- gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
- gtk_tree_view_column_set_min_width (col, 80);
- gtk_tree_view_column_set_resizable (col, TRUE);
- col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), D2_COLUMN);
- gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
- gtk_tree_view_column_set_min_width (col, 80);
- gtk_tree_view_column_set_resizable (col, TRUE);
-
- gtk_container_add (GTK_CONTAINER (scroll), view);
- gtk_box_pack_start (GTK_BOX (box), scroll, TRUE, TRUE, 0);
-
- return view;
-}
-
static void
key_dialog_load (GtkListStore *store)
{
- struct key_binding *kb = NULL;
- char *label_text, *accel_text;
- GtkTreeIter iter;
- GSList *list = keybind_list;
+ GHashTable *defaults = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ GHashTable *seen = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ GSList *reference = NULL;
+ key_load_kbs_from_buffer (g_strdup (default_kb_cfg), strlen (default_kb_cfg), &reference);
+ for (GSList *item = reference; item; item = item->next)
+ {
+ struct key_binding *kb = item->data;
+ key_dialog_reset_increment (defaults, key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2));
+ }
+ /* Protect only the built-in number of occurrences of an action and its
+ * parameters. Extra copies remain custom after save/reload, including
+ * copies whose only difference is their key. No runtime order changes. */
+ for (GSList *item = keybind_list; item; item = item->next)
+ {
+ struct key_binding *kb = item->data;
+ GtkTreeIter iter;
+ char *signature = key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2);
+ gboolean custom = key_dialog_reset_count (seen, signature) >= key_dialog_reset_count (defaults, signature);
+ key_dialog_reset_increment (seen, signature);
+ char *label = gtk_accelerator_get_label (kb->keyval, kb->mod);
+ char *accel = gtk_accelerator_name (kb->keyval, kb->mod);
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, KEY_COLUMN, label, ACCEL_COLUMN, accel,
+ ACTION_COLUMN, key_actions[kb->action].name, D1_COLUMN, kb->data1,
+ D2_COLUMN, kb->data2, CUSTOM_COLUMN, custom, -1);
+ g_free (label); g_free (accel);
+ }
+ g_slist_free_full (reference, key_free);
+ g_hash_table_destroy (defaults); g_hash_table_destroy (seen);
+}
- while (list)
- {
- kb = (struct key_binding*)list->data;
+static char *key_preferences_serialize (GtkTreeModel *model, GError **error);
- label_text = gtk_accelerator_get_label (kb->keyval, kb->mod);
- accel_text = gtk_accelerator_name (kb->keyval, kb->mod);
+static void
+key_preferences_restore (gpointer unused)
+{
+ key_dialog_reset (NULL, NULL);
+}
- gtk_list_store_append (store, &iter);
- gtk_list_store_set (store, &iter,
- KEY_COLUMN, label_text,
- ACCEL_COLUMN, accel_text,
- ACTION_COLUMN, key_actions[kb->action].name,
- D1_COLUMN, kb->data1,
- D2_COLUMN, kb->data2,
- CUSTOM_COLUMN, !key_binding_is_builtin (kb), -1);
-
- g_free (accel_text);
- g_free (label_text);
-
- list = g_slist_next (list);
- }
+GtkWidget *
+key_preferences_create (void)
+{
+ GtkListStore *store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING,
+ G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ PreferencesShortcutAction actions[KEY_MAX_ACTIONS + 1];
+ for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+ {
+ actions[i].id = key_actions[i].name;
+ actions[i].help = key_actions[i].help;
+ }
+ key_dialog_load (store);
+ key_dialog = preferences_shortcuts_page_new (store, actions, G_N_ELEMENTS (actions),
+ key_modifier_get_valid, key_preferences_restore, NULL);
+ g_object_add_weak_pointer (G_OBJECT (key_dialog), (gpointer *)&key_dialog);
+ g_object_set_data_full (G_OBJECT (key_dialog), "original-bindings",
+ key_preferences_serialize (GTK_TREE_MODEL (store), NULL), g_free);
+ g_object_unref (store);
+ return key_dialog;
}
void
key_dialog_show (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);
-
- 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);
- gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
- view = key_dialog_treeview_new (box);
- scroll = gtk_scrolled_window_new (NULL, NULL);
- gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
- GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
- gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
- gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
- gtk_paned_set_position (GTK_PANED (pane), 220);
- 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_object_set_data (G_OBJECT (key_dialog), "view", view);
- g_object_set_data (G_OBJECT (key_dialog), "xtext", xtext);
- g_object_set_data (G_OBJECT (key_dialog), KEY_DIALOG_THEME_LISTENER_ID_KEY,
- GUINT_TO_POINTER (theme_listener_register ("fkeys.window", key_dialog_theme_changed, key_dialog)));
- g_signal_connect (G_OBJECT (key_dialog), "destroy", G_CALLBACK (key_dialog_theme_destroy_cb), NULL);
-
- box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
- gtk_button_box_set_layout (GTK_BUTTON_BOX (box), GTK_BUTTONBOX_SPREAD);
- gtk_box_pack_start (GTK_BOX (vbox), box, FALSE, FALSE, 2);
- gtk_container_set_border_width (GTK_CONTAINER (box), 5);
-
- gtkutil_button (box, ICON_FKEYS_NEW, NULL, key_dialog_add,
- NULL, _("Add"));
- delete_button = gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
- NULL, _("Delete"));
- g_object_set_data (G_OBJECT (key_dialog), "delete_button", delete_button);
- 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);
-
- gtk_widget_show_all (key_dialog);
+ setup_open_page ("keyboard");
}
-static int
-key_save_kbs (void)
+static char *
+key_preferences_serialize (GtkTreeModel *model, GError **error)
{
- int fd;
- char buf[512];
- char *accel_text;
- GSList *list = keybind_list;
- struct key_binding *kb;
-
- 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)
+ 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));
+ preferences_shortcuts_reveal_path (key_dialog, path);
+ gtk_widget_grab_focus (GTK_WIDGET (view));
+ 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..342e0f6c 100644
--- a/src/fe-gtk/meson.build
+++ b/src/fe-gtk/meson.build
@@ -32,6 +32,7 @@ zoitechat_gtk_sources = [
'notifygui.c',
'pixmaps.c',
'preferences-persistence.c',
+ 'preferences-pages.c',
'plugin-tray.c',
'plugin-notification.c',
'rawlog.c',
@@ -62,7 +63,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 +245,50 @@ 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,
+)
+
+# g_test_skip() was added in GLib 2.38, below GTK 3.22's minimum GLib
+# requirement. Allow it in this test target without changing the app's API
+# ceiling or lowering macOS's existing 2.60 ceiling.
+preferences_pages_test_cflags = []
+if host_machine.system() != 'darwin'
+ preferences_pages_test_cflags = [
+ '-UGLIB_VERSION_MAX_ALLOWED',
+ '-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_38',
+ ]
+endif
+
+preferences_pages_tests = executable('preferences_pages_tests',
+ [textevents, 'tests/test-preferences-pages.c', 'preferences-pages.c'],
+ include_directories: [config_h_include, include_directories('../common')],
+ dependencies: [gtk_dep],
+ c_args: preferences_pages_test_cflags,
+)
+test('Preferences Pages Tests', preferences_pages_tests,
+ protocol: 'tap',
+ timeout: 120,
+)
+
+spell_entry_tests = executable('spell_entry_tests',
+ [marshal, 'tests/test-spell-entry.c'],
+ include_directories: [config_h_include, include_directories('../common')],
+ dependencies: [gtk_dep, libgmodule_dep],
+ c_args: preferences_pages_test_cflags,
+)
+test('Spell Entry Tests', spell_entry_tests,
+ protocol: 'tap',
+ timeout: 120,
+)
+
test('Theme Runtime Persistence Tests', theme_runtime_tests,
protocol: 'tap',
timeout: 120,
@@ -277,7 +323,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-pages.c b/src/fe-gtk/preferences-pages.c
new file mode 100644
index 00000000..09330cc0
--- /dev/null
+++ b/src/fe-gtk/preferences-pages.c
@@ -0,0 +1,2388 @@
+/* ZoiteChat
+ * Copyright (C) 2026 deepend-tildeclub.
+ * Licensed under the GNU General Public License, version 2 or later.
+ */
+
+#include "../common/zoitechat.h"
+#include "../common/fe.h"
+#include "../common/text.h"
+#include "../common/util.h"
+#include "preferences-pages.h"
+#include "setup.h"
+#include "theme/theme-access.h"
+#include "notifications/notification-backend.h"
+
+typedef enum { PAGE_TEXT, PAGE_TABS, PAGE_NOTIFICATIONS, PAGE_HISTORY, PAGE_TYPING } PageKind;
+
+/* Stored gui_tab_pos values, shared with the placement menu and maingui. */
+typedef enum
+{
+ POS_TOPLEFT = 1,
+ POS_BOTTOMLEFT,
+ POS_TOPRIGHT,
+ POS_BOTTOMRIGHT,
+ POS_TOP,
+ POS_BOTTOM,
+ POS_HIDDEN
+} PreferencesSwitcherPosition;
+
+typedef struct
+{
+ PageKind kind;
+ struct zoitechatprefs *prefs;
+ GtkWidget *root;
+ GtkWidget *preview, *preview_title, *preview_topic;
+ GtkWidget *stamp_combo, *stamp_entry, *stamp_options, *stamp_example;
+ GtkWidget *wrap_options, *topic_multiline;
+ GtkWidget *tree_options, *tab_options, *placement;
+ GtkWidget *layout_drawings[2];
+ GtkWidget *tray_options, *sound_entry;
+ GtkWidget *quiet_hidden, *tray_alerts[3];
+ GtkWidget *test_event, *test_status;
+ char *custom_stamp;
+ char **beep_file;
+ PreferencesSoundChanged sound_changed;
+ gpointer sound_data;
+ PreferencesAlertCapabilities capabilities;
+ gboolean refreshing;
+ int tree_position;
+ GtkWidget *log_options, *log_style, *log_entry, *log_custom;
+ GtkWidget *log_folder, *log_open, *log_file, *log_status, *log_line;
+ GtkWidget *history_open, *links_limit;
+ char *config_dir, *custom_logmask;
+ gboolean log_dirty;
+ GtkWidget *typing_preview, *typing_nick, *typing_mode, *spelling_options;
+ GtkWidget *language_search, *language_list, *language_entry, *language_status;
+ GtkWidget *suffix_entry, *completion_input, *completion_result;
+ GPtrArray *language_buttons;
+ PreferencesTypingPreview update_typing_preview;
+} PreferencesPage;
+
+typedef struct
+{
+ PreferencesPage *page;
+ int *integer;
+ char *string;
+ gsize capacity;
+ int base;
+} PreferenceBinding;
+
+static void page_refresh (PreferencesPage *page);
+
+static void
+page_mapped (GtkWidget *widget, PreferencesPage *page)
+{
+ page_refresh (page);
+}
+
+static GtkWidget *
+page_label (const char *text, gboolean secondary)
+{
+ 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), 58);
+ if (secondary)
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ return label;
+}
+
+static void
+page_pack (GtkWidget *box, GtkWidget *child)
+{
+ gtk_box_pack_start (GTK_BOX (box), child, FALSE, FALSE, 0);
+}
+
+static void
+page_status (GtkWidget *label, const char *text)
+{
+ gtk_widget_set_no_show_all (label, TRUE);
+ gtk_label_set_text (GTK_LABEL (label), text);
+ gtk_widget_set_visible (label, *text != '\0');
+}
+
+static GtkWidget *
+page_section (GtkWidget *parent, const char *title, const char *description)
+{
+ GtkWidget *section = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ GtkWidget *heading = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ GtkWidget *label = page_label (title, FALSE);
+ GtkWidget *line = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ char *markup = g_markup_printf_escaped ("%s", title);
+ gtk_label_set_markup (GTK_LABEL (label), markup);
+ g_free (markup);
+ page_pack (heading, label);
+ gtk_widget_set_valign (line, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (heading), line, TRUE, TRUE, 0);
+ page_pack (section, heading);
+ /* Layout-specific sections manage their own visibility, so their
+ * heading children must already be visible when the section is shown. */
+ gtk_widget_show_all (heading);
+ gtk_widget_set_margin_start (box, 12);
+ page_pack (section, box);
+ page_pack (parent, section);
+ if (description) gtk_widget_set_tooltip_text (label, description);
+ return box;
+}
+
+static void
+page_row (GtkWidget *box, const char *text, GtkWidget *control)
+{
+ GtkWidget *row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ GtkWidget *label = page_label (text, FALSE);
+ GtkSizeGroup *labels = g_object_get_data (G_OBJECT (box), "row-labels");
+ if (!labels)
+ {
+ labels = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+ g_object_set_data_full (G_OBJECT (box), "row-labels", labels, g_object_unref);
+ }
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 26);
+ gtk_size_group_add_widget (labels, label);
+ gtk_label_set_mnemonic_widget (GTK_LABEL (label), control);
+ gtk_box_pack_start (GTK_BOX (row), label, FALSE, FALSE, 0);
+ gboolean expand = (GTK_IS_ENTRY (control) && !GTK_IS_SPIN_BUTTON (control) && gtk_entry_get_max_width_chars (GTK_ENTRY (control)) < 0) || gtk_widget_get_hexpand (control);
+ gtk_box_pack_start (GTK_BOX (row), control, expand, expand, 0);
+ page_pack (box, row);
+}
+
+static PreferenceBinding *
+page_binding (PreferencesPage *page, GtkWidget *widget, const char *name)
+{
+ PreferenceBinding *binding = g_new0 (PreferenceBinding, 1);
+ binding->page = page;
+ gtk_widget_set_name (widget, name);
+ g_object_set_data_full (G_OBJECT (widget), "preference-binding", binding, g_free);
+ return binding;
+}
+
+static void
+page_toggle_changed (GtkToggleButton *button, PreferenceBinding *binding)
+{
+ *binding->integer = gtk_toggle_button_get_active (button);
+ page_refresh (binding->page);
+}
+
+static GtkWidget *
+page_toggle (PreferencesPage *page, GtkWidget *box, const char *name,
+ const char *text, int *value)
+{
+ GtkWidget *button = gtk_check_button_new_with_label (text);
+ GtkWidget *label = gtk_bin_get_child (GTK_BIN (button));
+ PreferenceBinding *binding = page_binding (page, button, name);
+ gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 56);
+ binding->integer = value;
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), *value);
+ g_signal_connect (button, "toggled", G_CALLBACK (page_toggle_changed), binding);
+ if (GTK_IS_FLOW_BOX (box)) gtk_container_add (GTK_CONTAINER (box), button);
+ else if (box) page_pack (box, button);
+ return button;
+}
+
+static void
+page_entry_changed (GtkEntry *entry, PreferenceBinding *binding)
+{
+ g_strlcpy (binding->string, gtk_entry_get_text (entry), binding->capacity);
+ page_refresh (binding->page);
+}
+
+static GtkWidget *
+page_entry (PreferencesPage *page, GtkWidget *box, const char *name,
+ const char *text, char *value, gsize capacity)
+{
+ GtkWidget *entry = gtk_entry_new ();
+ PreferenceBinding *binding = page_binding (page, entry, name);
+ binding->string = value;
+ binding->capacity = capacity;
+ gtk_entry_set_max_length (GTK_ENTRY (entry), capacity - 1);
+ gtk_entry_set_text (GTK_ENTRY (entry), value);
+ atk_object_set_name (gtk_widget_get_accessible (entry), text);
+ if (text && *text) page_row (box, text, entry);
+ else page_pack (box, entry);
+ g_signal_connect (entry, "changed", G_CALLBACK (page_entry_changed), binding);
+ return entry;
+}
+
+static void
+page_combo_changed (GtkComboBox *combo, PreferenceBinding *binding)
+{
+ int selected = gtk_combo_box_get_active (combo);
+ if (selected >= 0) *binding->integer = selected + binding->base;
+ page_refresh (binding->page);
+}
+
+static GtkWidget *
+page_combo (PreferencesPage *page, GtkWidget *box, const char *name,
+ const char *text, int *value, int base, const char *const *choices)
+{
+ GtkWidget *combo = gtk_combo_box_text_new ();
+ PreferenceBinding *binding = page_binding (page, combo, name);
+ binding->integer = value;
+ binding->base = base;
+ for (int i = 0; choices[i]; i++)
+ gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), _(choices[i]));
+ gtk_combo_box_set_active (GTK_COMBO_BOX (combo), *value - base);
+ page_row (box, text, combo);
+ g_signal_connect (combo, "changed", G_CALLBACK (page_combo_changed), binding);
+ return combo;
+}
+
+static void
+page_spin_changed (GtkSpinButton *spin, PreferenceBinding *binding)
+{
+ *binding->integer = gtk_spin_button_get_value_as_int (spin);
+ page_refresh (binding->page);
+}
+
+static GtkWidget *
+page_spin (PreferencesPage *page, GtkWidget *box, const char *name,
+ const char *text, int *value, int maximum)
+{
+ /* Preserve an existing out-of-range value on opening, just as with
+ * unknown combo choices. Only a user's edit changes the stored value. */
+ GtkWidget *spin = gtk_spin_button_new_with_range (0, MAX (maximum, *value), 1);
+ PreferenceBinding *binding = page_binding (page, spin, name);
+ binding->integer = value;
+ gtk_spin_button_set_value (GTK_SPIN_BUTTON (spin), *value);
+ page_row (box, text, spin);
+ g_signal_connect (spin, "value-changed", G_CALLBACK (page_spin_changed), binding);
+ return spin;
+}
+
+static void
+page_free (gpointer data)
+{
+ PreferencesPage *page = data;
+ g_free (page->custom_stamp);
+ g_free (page->config_dir);
+ g_free (page->custom_logmask);
+ if (page->language_buttons) g_ptr_array_unref (page->language_buttons);
+ g_free (page);
+}
+
+static PreferencesPage *
+page_new (PageKind kind, struct zoitechatprefs *prefs)
+{
+ PreferencesPage *page = g_new0 (PreferencesPage, 1);
+ page->kind = kind;
+ page->prefs = prefs;
+ page->root = gtk_box_new (GTK_ORIENTATION_VERTICAL, 20);
+ gtk_widget_set_valign (page->root, GTK_ALIGN_START);
+ g_object_set_data_full (G_OBJECT (page->root), "preferences-page", page, page_free);
+ g_signal_connect (page->root, "map", G_CALLBACK (page_mapped), page);
+ return page;
+}
+
+static GtkWindow *
+page_window (PreferencesPage *page)
+{
+ GtkWidget *window = gtk_widget_get_toplevel (page->root);
+ return GTK_IS_WINDOW (window) ? GTK_WINDOW (window) : NULL;
+}
+
+static void
+page_font_response (GtkDialog *dialog, int response, GtkWidget *entry)
+{
+ if (response == GTK_RESPONSE_OK)
+ {
+ char *font = gtk_font_chooser_get_font (GTK_FONT_CHOOSER (dialog));
+ if (font) gtk_entry_set_text (GTK_ENTRY (entry), font);
+ g_free (font);
+ }
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+page_choose_font (GtkButton *button, GtkWidget *entry)
+{
+ PreferenceBinding *binding = g_object_get_data (G_OBJECT (entry), "preference-binding");
+ GtkWidget *dialog = gtk_font_chooser_dialog_new (_("Choose a font"), page_window (binding->page));
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+ gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE);
+ gtk_font_chooser_set_font (GTK_FONT_CHOOSER (dialog), gtk_entry_get_text (GTK_ENTRY (entry)));
+ g_signal_connect_object (dialog, "response", G_CALLBACK (page_font_response), entry, 0);
+ gtk_widget_show (dialog);
+}
+
+static void
+page_font (PreferencesPage *page, GtkWidget *box, const char *name,
+ const char *text, char *value, gsize capacity)
+{
+ GtkWidget *row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ GtkWidget *entry = page_entry (page, row, name, "", value, capacity);
+ GtkWidget *button = gtk_button_new_with_label (_("Choose font…"));
+ atk_object_set_name (gtk_widget_get_accessible (entry), text);
+ gtk_box_set_child_packing (GTK_BOX (row), entry, TRUE, TRUE, 0, GTK_PACK_START);
+ page_pack (row, button);
+ gtk_widget_set_hexpand (row, TRUE);
+ page_row (box, text, row);
+ g_signal_connect (button, "clicked", G_CALLBACK (page_choose_font), entry);
+}
+
+static void
+page_timestamp (PreferencesPage *page, char *text, gsize size)
+{
+ GDateTime *today = g_date_time_new_now_local ();
+ GDateTime *date = page->kind == PAGE_HISTORY ?
+ g_date_time_new_local (g_date_time_get_year (today), g_date_time_get_month (today),
+ g_date_time_get_day_of_month (today), 14, 35, 8) :
+ g_date_time_new_local (2026, 1, 15, 14, 35, 8);
+ time_t stamp = g_date_time_to_unix (date);
+ char *formatted = NULL;
+ char *format = page->kind == PAGE_HISTORY ? page->prefs->hex_stamp_log_format : page->prefs->hex_stamp_text_format;
+ char *locale_format = g_locale_from_utf8 (format, -1, NULL, NULL, NULL);
+ text[0] = 0;
+ if (locale_format && get_stamp_str (format, stamp, &formatted) > 0)
+ g_strlcpy (text, formatted, size);
+ g_free (locale_format);
+ g_free (formatted);
+ g_date_time_unref (date);
+ g_date_time_unref (today);
+}
+
+static void
+page_text_preview (PreferencesPage *page)
+{
+ struct zoitechatprefs *prefs = page->prefs;
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (page->preview));
+ GtkTextTagTable *table = gtk_text_buffer_get_tag_table (buffer);
+ GtkTextIter iter;
+ GdkRGBA foreground, background, nick, marker;
+ char timestamp[256], *title;
+ int stamp_width = 0, nick_width = 0, space_width = 1;
+ PangoLayout *layout;
+ const char *font;
+#ifdef WIN32
+ font = prefs->hex_text_font_main;
+#else
+ font = prefs->hex_text_font;
+#endif
+ PangoFontDescription *description = pango_font_description_from_string (*font ? font : "Sans 11");
+ page_timestamp (page, timestamp, sizeof timestamp);
+ if (!prefs->hex_stamp_text) timestamp[0] = 0;
+ layout = gtk_widget_create_pango_layout (page->preview, timestamp);
+ pango_layout_set_font_description (layout, description);
+ pango_layout_get_pixel_size (layout, &stamp_width, NULL);
+ pango_layout_set_text (layout, "longnickname ", -1);
+ pango_layout_get_pixel_size (layout, &nick_width, NULL);
+ pango_layout_set_text (layout, " ", -1);
+ pango_layout_get_pixel_size (layout, &space_width, NULL);
+ theme_get_color (THEME_TOKEN_TEXT_FOREGROUND, &foreground);
+ theme_get_color (THEME_TOKEN_TEXT_BACKGROUND, &background);
+ theme_get_color (THEME_TOKEN_MIRC_12, &nick);
+ theme_get_color (THEME_TOKEN_MARKER, &marker);
+ {
+ GtkCssProvider *provider = g_object_get_data (G_OBJECT (page->preview), "preview-palette");
+ char *bg = gdk_rgba_to_string (&background), *fg = gdk_rgba_to_string (&foreground);
+ char *css = g_strdup_printf ("textview, textview text { background-color: %s; color: %s; }", bg, fg);
+ if (!provider)
+ {
+ provider = gtk_css_provider_new ();
+ gtk_style_context_add_provider (gtk_widget_get_style_context (page->preview),
+ GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
+ g_object_set_data_full (G_OBJECT (page->preview), "preview-palette", provider, g_object_unref);
+ }
+ gtk_css_provider_load_from_data (provider, css, -1, NULL);
+ g_free (css); g_free (bg); g_free (fg);
+ }
+ g_object_set (gtk_text_tag_table_lookup (table, "body"), "font-desc", description,
+ "foreground-rgba", &foreground, "background-rgba", &background, NULL);
+ g_object_set (gtk_text_tag_table_lookup (table, "nickname0"), "foreground-rgba",
+ prefs->hex_text_color_nicks ? &nick : &foreground, NULL);
+ theme_get_color (THEME_TOKEN_MIRC_9, &nick);
+ g_object_set (gtk_text_tag_table_lookup (table, "nickname1"), "foreground-rgba",
+ prefs->hex_text_color_nicks ? &nick : &foreground, NULL);
+ g_object_set (gtk_text_tag_table_lookup (table, "marker"), "foreground-rgba", &marker, NULL);
+ gtk_text_buffer_set_text (buffer, "", 0);
+ for (int i = 0; i < 3; i++)
+ {
+ GtkTextIter start, end;
+ const char *name = i == 1 ? "longnickname" : "alice";
+ const char *message = i == 0 ? _("Hello! Welcome to #zoitechat.") :
+ i == 1 ? _("A longer message wraps onto another line so you can see where the next line begins.") :
+ _("This message arrived after you last read the conversation.");
+ int start_offset, wrap_margin, name_width;
+ gtk_text_buffer_get_end_iter (buffer, &iter);
+ if (i == 2 && prefs->hex_text_show_marker)
+ {
+ GtkTextMark *mark = gtk_text_buffer_get_mark (buffer, "unread-preview");
+ if (mark) gtk_text_buffer_move_mark (buffer, mark, &iter);
+ else gtk_text_buffer_create_mark (buffer, "unread-preview", &iter, TRUE);
+ gtk_text_buffer_insert_with_tags_by_name (buffer, &iter, _("Unread messages\n"), -1, "marker", NULL);
+ }
+ start_offset = gtk_text_iter_get_offset (&iter);
+ gtk_text_buffer_insert (buffer, &iter, timestamp, -1);
+ pango_layout_set_text (layout, name, -1);
+ pango_layout_get_pixel_size (layout, &name_width, NULL);
+ if (prefs->hex_text_indent)
+ {
+ char *padding = g_strnfill (MAX (0, (nick_width - name_width - 2 * space_width) / MAX (1, space_width)), ' ');
+ gtk_text_buffer_insert (buffer, &iter, padding, -1);
+ g_free (padding);
+ }
+ gtk_text_buffer_insert_with_tags_by_name (buffer, &iter, name, -1, i == 1 ? "nickname1" : "nickname0", NULL);
+ gtk_text_buffer_insert (buffer, &iter, prefs->hex_text_indent ? "\t" : " ", -1);
+ gtk_text_buffer_insert (buffer, &iter, message, -1);
+ gtk_text_buffer_insert (buffer, &iter, "\n", 1);
+ gtk_text_buffer_get_iter_at_offset (buffer, &start, start_offset);
+ gtk_text_buffer_get_end_iter (buffer, &end);
+ wrap_margin = prefs->hex_text_indent ? stamp_width + nick_width :
+ prefs->hex_text_wrap_align == 2 ? stamp_width + name_width + 2 * space_width :
+ prefs->hex_text_wrap_align == 1 ? stamp_width : 0;
+ GtkTextTag *paragraph = gtk_text_tag_table_lookup (table, i == 0 ? "line0" : i == 1 ? "line1" : "line2");
+ /* GtkTextView's negative indent supplies the hanging margin;
+ * adding left-margin as well would offset the first line too. */
+ g_object_set (paragraph, "left-margin", 10,
+ "indent", -wrap_margin,
+ "pixels-below-lines", 8, NULL);
+ gtk_text_buffer_apply_tag (buffer, paragraph, &start, &end);
+ if (prefs->hex_text_indent)
+ {
+ /* Use a tab stop for the message column, so differently
+ * sized nicknames share the same alignment. */
+ PangoTabArray *tabs = pango_tab_array_new (1, TRUE);
+ pango_tab_array_set_tab (tabs, 0, PANGO_TAB_LEFT, stamp_width + nick_width);
+ g_object_set (paragraph, "tabs", tabs, NULL);
+ pango_tab_array_free (tabs);
+ }
+ }
+ g_object_unref (layout);
+ pango_font_description_free (description);
+ {
+ GtkTextIter end;
+ gtk_text_buffer_get_bounds (buffer, &iter, &end);
+ gtk_text_buffer_apply_tag_by_name (buffer, "body", &iter, &end);
+ }
+ title = g_strdup_printf ("ZoiteChat — #zoitechat%s%s%s",
+ prefs->hex_gui_win_nick ? " · alice" : "",
+ prefs->hex_gui_win_ucount ? _(" · 42 users") : "",
+ prefs->hex_gui_win_modes ? " · +nt" : "");
+ gtk_label_set_text (GTK_LABEL (page->preview_title), title);
+ g_free (title);
+ gtk_label_set_text (GTK_LABEL (page->preview_topic), prefs->hex_gui_mode_buttons_inline ?
+ _("Topic: Welcome to ZoiteChat [ +n ] [ +t ]") :
+ prefs->hex_gui_topicbar_multiline ? _("Topic: Welcome to ZoiteChat\nA longer topic can use another line.") :
+ _("Topic: Welcome to ZoiteChat"));
+ gtk_widget_set_sensitive (page->stamp_options, prefs->hex_stamp_text);
+ gtk_widget_set_visible (gtk_widget_get_parent (page->stamp_entry),
+ !g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->stamp_combo)), "custom"));
+ gtk_widget_set_sensitive (page->stamp_entry,
+ !g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->stamp_combo)), "custom"));
+ gtk_widget_set_sensitive (page->wrap_options, !prefs->hex_text_indent);
+ gtk_widget_set_sensitive (page->topic_multiline, !prefs->hex_gui_mode_buttons_inline);
+ page_timestamp (page, timestamp, sizeof timestamp);
+ title = g_strdup_printf (_("Example: %s"), *timestamp ? timestamp : _("(no timestamp text)"));
+ gtk_label_set_text (GTK_LABEL (page->stamp_example), title);
+ g_free (title);
+}
+
+static gboolean
+page_preview_marker (GtkWidget *widget, cairo_t *cr, PreferencesPage *page)
+{
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+ GtkTextMark *mark = gtk_text_buffer_get_mark (buffer, "unread-preview");
+ GtkTextIter iter;
+ GdkRectangle line;
+ GdkRGBA color;
+ int x, y;
+ if (!page->prefs->hex_text_show_marker || !mark) return FALSE;
+ gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
+ gtk_text_view_get_iter_location (GTK_TEXT_VIEW (widget), &iter, &line);
+ gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (widget), GTK_TEXT_WINDOW_WIDGET,
+ line.x, line.y, &x, &y);
+ theme_get_color (THEME_TOKEN_MARKER, &color);
+ gdk_cairo_set_source_rgba (cr, &color);
+ cairo_set_line_width (cr, 1);
+ cairo_move_to (cr, 10, y - 2.5);
+ cairo_line_to (cr, gtk_widget_get_allocated_width (widget) - 10, y - 2.5);
+ cairo_stroke (cr);
+ return FALSE;
+}
+
+static void
+page_stamp_changed (GtkComboBox *combo, PreferencesPage *page)
+{
+ const char *id = gtk_combo_box_get_active_id (combo);
+ if (!id) return;
+ gtk_entry_set_text (GTK_ENTRY (page->stamp_entry),
+ strcmp (id, "custom") ? id : page->custom_stamp);
+ page_refresh (page);
+}
+
+static void
+page_custom_stamp_changed (GtkEntry *entry, PreferencesPage *page)
+{
+ if (g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->stamp_combo)), "custom")) return;
+ g_free (page->custom_stamp);
+ page->custom_stamp = g_strdup (gtk_entry_get_text (entry));
+}
+
+GtkWidget *
+preferences_text_page_new (struct zoitechatprefs *prefs, const char *const *languages)
+{
+ PreferencesPage *page = page_new (PAGE_TEXT, prefs);
+ GtkWidget *box, *preview, *view, *preview_expander;
+ GtkTextBuffer *buffer;
+ const char *const alignment[] = {N_("Start of line"), N_("Nickname"), N_("Message"), NULL};
+ const char *formats[] = {"%H:%M ", "%I:%M %p ", "[%H:%M:%S] ", "%H:%M:%S ", NULL};
+ const char *const names[] = {N_("24-hour · 14:35"), N_("12-hour · 02:35 PM"),
+ N_("Bracketed · [14:35:08]"), N_("With seconds · 14:35:08"), NULL};
+ preview_expander = gtk_expander_new (_("Conversation preview"));
+ gtk_widget_set_name (preview_expander, "text-preview-expander");
+ gtk_expander_set_expanded (GTK_EXPANDER (preview_expander), TRUE);
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add (GTK_CONTAINER (preview_expander), box);
+ page->preview_title = page_label ("", TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (page->preview_title), PANGO_ELLIPSIZE_END);
+ page->preview_topic = page_label ("", TRUE);
+ page_pack (box, page->preview_title);
+ page_pack (box, page->preview_topic);
+ preview = gtk_scrolled_window_new (NULL, NULL);
+ /* Automatic scrolling lets the sample shrink with the preferences
+ * window; NEVER would impose TextView's full content width on it. */
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (preview), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (preview), GTK_SHADOW_IN);
+ gtk_widget_set_size_request (preview, -1, 110);
+ page->preview = view = gtk_text_view_new ();
+ gtk_widget_set_name (view, "conversation-preview");
+ gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
+ gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (view), FALSE);
+ gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD_CHAR);
+ gtk_text_view_set_left_margin (GTK_TEXT_VIEW (view), 10);
+ gtk_text_view_set_right_margin (GTK_TEXT_VIEW (view), 10);
+ gtk_text_view_set_top_margin (GTK_TEXT_VIEW (view), 10);
+ gtk_text_view_set_bottom_margin (GTK_TEXT_VIEW (view), 10);
+ atk_object_set_name (gtk_widget_get_accessible (view), _("Conversation preview"));
+ gtk_container_add (GTK_CONTAINER (preview), view);
+ g_signal_connect_after (view, "draw", G_CALLBACK (page_preview_marker), page);
+ page_pack (box, preview);
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+ gtk_text_buffer_create_tag (buffer, "body", NULL);
+ gtk_text_buffer_create_tag (buffer, "nickname0", NULL);
+ gtk_text_buffer_create_tag (buffer, "nickname1", NULL);
+ gtk_text_buffer_create_tag (buffer, "marker", "scale", 0.85, NULL);
+ for (int i = 0; i < 3; i++)
+ {
+ char name[16];
+ g_snprintf (name, sizeof name, "line%d", i);
+ gtk_text_buffer_create_tag (buffer, name, NULL);
+ }
+ box = page_section (page->root, _("Conversation text"), NULL);
+#ifdef WIN32
+ if (languages) page_combo (page, box, "language", _("Language"), &prefs->hex_gui_lang, 0, languages);
+ page_font (page, box, "chat-font", _("Main font"), prefs->hex_text_font_main, sizeof prefs->hex_text_font_main);
+#else
+ (void)languages;
+ page_font (page, box, "chat-font", _("Font"), prefs->hex_text_font, sizeof prefs->hex_text_font);
+#endif
+ page_toggle (page, box, "colored-nicks", _("Give nicknames different colours"), &prefs->hex_text_color_nicks);
+ page_toggle (page, box, "indent-nicks", _("Align nicknames in a separate column"), &prefs->hex_text_indent);
+ page->wrap_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ page_pack (box, page->wrap_options);
+ page_combo (page, page->wrap_options, "wrap-alignment", _("Align wrapped lines under"), &prefs->hex_text_wrap_align, 0, alignment);
+ page_toggle (page, box, "unread-marker", _("Show a line where unread messages begin"), &prefs->hex_text_show_marker);
+ page_toggle (page, box, "keep-search-position", _("Keep the conversation at the search result after closing search"), &prefs->hex_text_search_keep_position);
+ page_pack (page->root, preview_expander);
+ box = page_section (page->root, _("Timestamps"), NULL);
+ page_toggle (page, box, "timestamps", _("Show a time beside each message"), &prefs->hex_stamp_text);
+ page->stamp_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page_pack (box, page->stamp_options);
+ page->stamp_combo = gtk_combo_box_text_new ();
+ gtk_widget_set_name (page->stamp_combo, "timestamp-style");
+ for (int i = 0; formats[i]; i++)
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->stamp_combo), formats[i], _(names[i]));
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->stamp_combo), "custom", _("Custom format"));
+ if (!gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->stamp_combo), prefs->hex_stamp_text_format))
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->stamp_combo), "custom");
+ page->custom_stamp = g_strdup (prefs->hex_stamp_text_format);
+ page_row (page->stamp_options, _("Time style"), page->stamp_combo);
+ page->stamp_entry = page_entry (page, page->stamp_options, "timestamp-format", _("Custom format"),
+ prefs->hex_stamp_text_format, sizeof prefs->hex_stamp_text_format);
+ gtk_widget_show_all (gtk_widget_get_parent (page->stamp_entry));
+ gtk_widget_set_no_show_all (gtk_widget_get_parent (page->stamp_entry), TRUE);
+ page->stamp_example = page_label ("", TRUE);
+ page_pack (page->stamp_options, page->stamp_example);
+ gtk_widget_set_tooltip_text (page->stamp_entry, _("%H: hour (00–23), %I: hour (01–12), %M: minute, %S: second, %p: AM/PM. Spaces are kept."));
+ g_signal_connect (page->stamp_combo, "changed", G_CALLBACK (page_stamp_changed), page);
+ g_signal_connect (page->stamp_entry, "changed", G_CALLBACK (page_custom_stamp_changed), page);
+ box = page_section (page->root, _("Window information"), NULL);
+ page_toggle (page, box, "title-modes", _("Show channel modes in the title bar"), &prefs->hex_gui_win_modes);
+ page_toggle (page, box, "title-users", _("Show the number of users in the title bar"), &prefs->hex_gui_win_ucount);
+ page_toggle (page, box, "title-nick", _("Show your nickname in the title bar"), &prefs->hex_gui_win_nick);
+ page_toggle (page, box, "inline-modes", _("Place channel mode buttons beside the topic"), &prefs->hex_gui_mode_buttons_inline);
+ page->topic_multiline = page_toggle (page, box, "multiline-topic", _("Allow topics to use multiple lines"), &prefs->hex_gui_topicbar_multiline);
+ gtk_widget_set_tooltip_text (page->topic_multiline, _("Available when mode buttons are not beside the topic."));
+ page_refresh (page);
+ return page->root;
+}
+
+static void
+page_layout_outline (cairo_t *cr, double x, double y, double width, double height)
+{
+ const double radius = 4;
+ cairo_new_sub_path (cr);
+ cairo_arc (cr, x + width - radius, y + radius, radius, -G_PI / 2, 0);
+ cairo_arc (cr, x + width - radius, y + height - radius, radius, 0, G_PI / 2);
+ cairo_arc (cr, x + radius, y + height - radius, radius, G_PI / 2, G_PI);
+ cairo_arc (cr, x + radius, y + radius, radius, G_PI, 3 * G_PI / 2);
+ cairo_close_path (cr);
+}
+
+static gboolean
+page_layout_draw (GtkWidget *widget, cairo_t *cr, PreferencesPage *page)
+{
+ GtkStyleContext *style = gtk_widget_get_style_context (widget);
+ GdkRGBA ink, base = {1, 1, 1, 1}, accent = {0.2, 0.45, 0.8, 1};
+ int type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "layout"));
+ int position = page->prefs->hex_gui_tab_layout == type ? page->prefs->hex_gui_tab_pos : (type == 0 ? POS_BOTTOM : POS_TOPLEFT);
+ double x = 5, y = 5, width = gtk_widget_get_allocated_width (widget) - 10;
+ double height = gtk_widget_get_allocated_height (widget) - 10;
+ double nav_x = x, nav_y = y, nav_width = width, nav_height = 12;
+ gtk_style_context_get_color (style, GTK_STATE_FLAG_NORMAL, &ink);
+ gtk_style_context_lookup_color (style, "theme_base_color", &base);
+ gtk_style_context_lookup_color (style, "theme_selected_bg_color", &accent);
+ page_layout_outline (cr, x, y, width, height);
+ gdk_cairo_set_source_rgba (cr, &base);
+ cairo_fill_preserve (cr);
+ ink.alpha = 0.35;
+ gdk_cairo_set_source_rgba (cr, &ink);
+ cairo_set_line_width (cr, 1);
+ cairo_stroke (cr);
+ if (position <= POS_BOTTOMRIGHT)
+ {
+ nav_width = width * 0.28;
+ nav_height = height * 0.78;
+ if (position == POS_BOTTOMLEFT || position == POS_BOTTOMRIGHT) nav_y += height - nav_height;
+ if (position >= POS_TOPRIGHT) nav_x += width - nav_width;
+ }
+ else if (position == POS_BOTTOM) nav_y += height - nav_height;
+ cairo_save (cr);
+ page_layout_outline (cr, x, y, width, height);
+ cairo_clip (cr);
+ if (position != POS_HIDDEN)
+ {
+ accent.alpha = 0.18;
+ gdk_cairo_set_source_rgba (cr, &accent);
+ cairo_rectangle (cr, nav_x, nav_y, nav_width, nav_height);
+ cairo_fill (cr);
+ accent.alpha = 0.8;
+ gdk_cairo_set_source_rgba (cr, &accent);
+ for (int i = 0; i < 3; i++)
+ {
+ if (position <= POS_BOTTOMRIGHT)
+ cairo_rectangle (cr, nav_x + 5 + (type == 2 && i ? 3 : 0), nav_y + 8 + i * 10, nav_width - 12, 2);
+ else
+ cairo_rectangle (cr, nav_x + 6 + i * nav_width / 3, nav_y + 5, nav_width / 3 - 12, 2);
+ }
+ cairo_fill (cr);
+ }
+ double chat_x = x + 7, chat_y = y + 10, chat_width = width - 14;
+ if (position <= POS_BOTTOMRIGHT)
+ {
+ chat_width -= nav_width;
+ if (position <= POS_BOTTOMLEFT) chat_x += nav_width;
+ }
+ else if (position == POS_TOP) chat_y += nav_height;
+ ink.alpha = 0.3;
+ gdk_cairo_set_source_rgba (cr, &ink);
+ for (int i = 0; i < 3; i++)
+ cairo_rectangle (cr, chat_x, chat_y + i * 9, chat_width * (i == 1 ? 0.65 : 0.9), 2);
+ cairo_fill (cr);
+ cairo_restore (cr);
+ return FALSE;
+}
+
+static void
+page_layout_changed (GtkToggleButton *button, PreferencesPage *page)
+{
+ int layout = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "layout"));
+ if (!gtk_toggle_button_get_active (button) || page->prefs->hex_gui_tab_layout == layout) return;
+ if (layout == 0)
+ {
+ page->tree_position = page->prefs->hex_gui_tab_pos;
+ /* Mirror mg_change_layout's horizontal-tab placement, but make
+ * that result visible before Save. Merely opening changes none. */
+ if (page->prefs->hex_gui_tab_pos != POS_TOP && page->prefs->hex_gui_tab_pos != POS_BOTTOM)
+ page->prefs->hex_gui_tab_pos = POS_BOTTOM;
+ }
+ else page->prefs->hex_gui_tab_pos = page->tree_position;
+ page->prefs->hex_gui_tab_layout = layout;
+ gtk_combo_box_set_active (GTK_COMBO_BOX (page->placement), page->prefs->hex_gui_tab_pos - 1);
+ page_refresh (page);
+}
+
+GtkWidget *
+preferences_tabs_page_new (struct zoitechatprefs *prefs)
+{
+ PreferencesPage *page = page_new (PAGE_TABS, prefs);
+ GtkWidget *box, *choices, *button, *content, *drawing;
+ GSList *group = NULL;
+ const char *const positions[] = {N_("Left · upper"), N_("Left · lower"), N_("Right · upper"),
+ N_("Right · lower"), N_("Top"), N_("Bottom"), N_("Hidden"), NULL};
+ const char *const focus[] = {N_("Never"), N_("Always"), N_("Only tabs I request"), NULL};
+ const char *const notices[] = {N_("Automatic"), N_("A separate tab"), N_("The current tab"), NULL};
+ const char *const containers[] = {N_("Separate windows"), N_("Tabs"), NULL};
+ page->tree_position = prefs->hex_gui_tab_pos;
+ box = page_section (page->root, _("Channel navigation"),
+ _("Choose how networks and conversations are arranged."));
+ choices = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 18);
+ page_pack (box, choices);
+ for (int i = 0; i < 2; i++)
+ {
+ int layout = i == 0 ? 0 : 2;
+ button = gtk_radio_button_new (group);
+ gtk_widget_set_name (button, i == 0 ? "layout-tabs" : "layout-tree");
+ atk_object_set_name (gtk_widget_get_accessible (button), i == 0 ? _("Tabs") : _("Tree"));
+ group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), prefs->hex_gui_tab_layout == layout);
+ g_object_set_data (G_OBJECT (button), "layout", GINT_TO_POINTER (layout));
+ content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
+ page_pack (content, page_label (i == 0 ? _("Tabs") : _("Tree"), FALSE));
+ drawing = gtk_drawing_area_new ();
+ page->layout_drawings[i] = drawing;
+ gtk_widget_set_size_request (drawing, 120, 64);
+ g_object_set_data (G_OBJECT (drawing), "layout", GINT_TO_POINTER (layout));
+ g_signal_connect (drawing, "draw", G_CALLBACK (page_layout_draw), page);
+ page_pack (content, drawing);
+ gtk_container_add (GTK_CONTAINER (button), content);
+ page_pack (choices, button);
+ g_signal_connect (button, "toggled", G_CALLBACK (page_layout_changed), page);
+ }
+ page->placement = page_combo (page, box, "channel-placement", _("Placement"), &prefs->hex_gui_tab_pos, 1, positions);
+ gtk_widget_set_tooltip_text (page->placement, _("Switching to tabs uses the bottom when the tree is at a side or hidden."));
+ page_toggle (page, box, "alphabetical-tabs", _("Sort conversations alphabetically"), &prefs->hex_gui_tab_sort);
+ page_toggle (page, box, "wheel-tabs", _("Use the mouse wheel over the switcher to change conversations"), &prefs->hex_gui_tab_scrollchans);
+ page_toggle (page, box, "small-tabs", _("Use smaller text in the channel switcher"), &prefs->hex_gui_tab_small);
+ gtk_widget_set_tooltip_text (page_spin (page, box, "tab-label-length", _("Label length (characters)"), &prefs->hex_gui_tab_trunc, 99), _("0, 1 or 2 keeps full conversation names."));
+ page->tree_options = page_section (page->root, _("Sidebar tree details"), NULL);
+ /* Include the section heading when switching layout-specific controls. */
+ box = page->tree_options;
+ page->tree_options = gtk_widget_get_parent (box);
+ gtk_widget_set_no_show_all (page->tree_options, TRUE);
+ page_toggle (page, box, "tree-icons", _("Show network and channel icons"), &prefs->hex_gui_tab_icons);
+ page_toggle (page, box, "tree-dots", _("Show dotted lines between tree items"), &prefs->hex_gui_tab_dots);
+ gtk_widget_show_all (box);
+ page->tab_options = page_section (page->root, _("Tab details"), NULL);
+ box = page->tab_options;
+ page->tab_options = gtk_widget_get_parent (box);
+ gtk_widget_set_no_show_all (page->tab_options, TRUE);
+ page_font (page, box, "tab-font", _("Tab font"), prefs->hex_gui_tab_font, sizeof prefs->hex_gui_tab_font);
+ gtk_widget_set_tooltip_text (box, _("Leave the font blank to use the application theme. Smaller text still applies."));
+ page_toggle (page, box, "tab-close-buttons", _("Show a close button on each tab"), &prefs->hex_gui_tab_closebuttons);
+ gtk_widget_show_all (box);
+ box = page_section (page->root, _("Opening conversations"), NULL);
+ page_toggle (page, box, "server-message-tab", _("Keep server messages in a separate tab"), &prefs->hex_gui_tab_server);
+ page_toggle (page, box, "private-message-tab", _("Open a conversation when someone sends a private message"), &prefs->hex_gui_autoopen_dialog);
+ page_combo (page, box, "focus-new-tabs", _("Switch to new tabs"), &prefs->hex_gui_tab_newtofront, 0, focus);
+ page_combo (page, box, "notice-placement", _("Show notices in"), &prefs->hex_irc_notice_pos, 0, notices);
+ page_combo (page, box, "channel-container", _("Open channels in"), &prefs->hex_gui_tab_chans, 0, containers);
+ page_combo (page, box, "dialog-container", _("Open private conversations in"), &prefs->hex_gui_tab_dialogs, 0, containers);
+ page_combo (page, box, "utility-container", _("Open utility windows in"), &prefs->hex_gui_tab_utils, 0, containers);
+ box = page_section (page->root, _("Closing conversations"), NULL);
+ page_toggle (page, box, "middle-close", _("Close a conversation with the middle mouse button"), &prefs->hex_gui_tab_middleclose);
+ gtk_widget_show_all (page->root);
+ page_refresh (page);
+ return page->root;
+}
+
+static void
+page_sound_changed (GtkEntry *entry, PreferencesPage *page)
+{
+ const char *text = gtk_entry_get_text (entry);
+ if (page->refreshing || !g_strcmp0 (text, *page->beep_file)) return;
+ g_free (*page->beep_file);
+ *page->beep_file = g_strdup (text);
+ if (page->sound_changed)
+ page->sound_changed (page->sound_data);
+ page_refresh (page);
+}
+
+void
+preferences_notifications_refresh_sound (GtkWidget *widget)
+{
+ PreferencesPage *page = g_object_get_data (G_OBJECT (widget), "preferences-page");
+ if (!page || page->kind != PAGE_NOTIFICATIONS) return;
+ page->refreshing = TRUE;
+ gtk_entry_set_text (GTK_ENTRY (page->sound_entry), *page->beep_file ? *page->beep_file : "");
+ page->refreshing = FALSE;
+ page_refresh (page);
+}
+
+static void
+page_sound_response (GtkDialog *dialog, int response, GtkWidget *entry)
+{
+ if (response == GTK_RESPONSE_ACCEPT)
+ {
+ char *file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
+ if (file) gtk_entry_set_text (GTK_ENTRY (entry), file);
+ g_free (file);
+ }
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+page_choose_sound (GtkButton *button, PreferencesPage *page)
+{
+ GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Choose an alert sound"), page_window (page),
+ GTK_FILE_CHOOSER_ACTION_OPEN, _("Cancel"), GTK_RESPONSE_CANCEL,
+ _("Open"), GTK_RESPONSE_ACCEPT, NULL);
+ GtkFileFilter *filter = gtk_file_filter_new ();
+#ifdef WIN32
+ gtk_file_filter_set_name (filter, _("WAV sound files"));
+ gtk_file_filter_add_pattern (filter, "*.[wW][aA][vV]");
+#else
+ gtk_file_filter_set_name (filter, _("Audio files"));
+ gtk_file_filter_add_mime_type (filter, "audio/*");
+#endif
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+ if (*page->beep_file && g_path_is_absolute (*page->beep_file))
+ gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (dialog), *page->beep_file);
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+ gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE);
+ g_signal_connect_object (dialog, "response", G_CALLBACK (page_sound_response), page->sound_entry, 0);
+ gtk_widget_show (dialog);
+}
+
+static void
+page_system_sound (GtkButton *button, PreferencesPage *page)
+{
+ gtk_entry_set_text (GTK_ENTRY (page->sound_entry), "");
+}
+
+static void
+page_play_sound (GtkButton *button, PreferencesPage *page)
+{
+ if (*page->beep_file && **page->beep_file) sound_play (*page->beep_file, FALSE);
+ else fe_beep (NULL);
+}
+
+static void
+page_open_sounds (GtkButton *button, gpointer unused)
+{
+ /* The full editor remains available for less common event sounds. */
+ setup_open_page ("sounds");
+}
+
+static void
+page_test_notification (GtkButton *button, PreferencesPage *page)
+{
+ int event = gtk_combo_box_get_active (GTK_COMBO_BOX (page->test_event));
+ int enabled = event == 0 ? page->prefs->hex_input_balloon_priv :
+ event == 1 ? page->prefs->hex_input_balloon_hilight : page->prefs->hex_input_balloon_chans;
+ GString *status;
+ if (!(page->capabilities & PREFERENCES_ALERT_DESKTOP) || !notification_backend_supported ())
+ {
+ gtk_label_set_text (GTK_LABEL (page->test_status), _("Desktop notifications are not available in this session."));
+ return;
+ }
+ notification_backend_show (_("ZoiteChat · notification test"),
+ event == 0 ? _("alice sent you a private message: Hello!") :
+ event == 1 ? _("alice mentioned you in #zoitechat: Are you there?") :
+ _("alice posted in #zoitechat: Hello, everyone!"));
+ status = g_string_new (_("Test requested. Your desktop may still silence notifications."));
+ if (!enabled) g_string_append (status, _(" Desktop alerts for this message type are currently switched off in this page."));
+ if (page->prefs->hex_away_omit_alerts) g_string_append (status, _(" Real alerts are muted while you are marked away."));
+ if (page->prefs->hex_gui_focus_omitalerts) g_string_append (status, _(" Real desktop alerts are muted while the main window is active."));
+ if (page->prefs->hex_gui_tray && page->prefs->hex_gui_tray_quiet)
+ g_string_append (status, _(" Real desktop alerts are only shown when hidden to the tray."));
+ gtk_label_set_text (GTK_LABEL (page->test_status), status->str);
+ g_string_free (status, TRUE);
+}
+
+static void
+page_notification_group (PreferencesPage *page, GtkWidget *grid, int row,
+ const char *title, const char *description,
+ int *desktop, int *beep, int *tray, int *attention, const char *id)
+{
+ GtkWidget *label = page_label (title, FALSE), *control;
+ gtk_widget_set_tooltip_text (label, description);
+ gtk_widget_set_hexpand (label, TRUE);
+ gtk_grid_attach (GTK_GRID (grid), label, 0, row, 1, 1);
+ int *values[] = {desktop, beep, tray, attention};
+ const char *suffixes[] = {"desktop", "beep", "tray", "attention"};
+ const char *names[] = {N_("Desktop notification"), N_("Play alert sound"), N_("Blink tray icon"),
+#ifdef HAVE_GTK_MAC
+ N_("Bounce Dock icon")};
+#else
+ N_("Flash taskbar")};
+#endif
+ int column = 1;
+ for (int i = 0; i < 4; i++)
+ {
+ if (i == 2 && !(page->capabilities & PREFERENCES_ALERT_TRAY)) continue;
+ if (i == 3 && !(page->capabilities & PREFERENCES_ALERT_ATTENTION)) continue;
+ char *name = g_strdup_printf ("%s-%s", id, suffixes[i]);
+ char *accessible = g_strdup_printf ("%s: %s", title, _(names[i]));
+ control = page_toggle (page, NULL, name, "", values[i]);
+ atk_object_set_name (gtk_widget_get_accessible (control), accessible);
+ gtk_widget_set_tooltip_text (control, accessible);
+ gtk_widget_set_halign (control, GTK_ALIGN_CENTER);
+ gtk_grid_attach (GTK_GRID (grid), control, column++, row, 1, 1);
+ g_free (name); g_free (accessible);
+ if (i == 0)
+ {
+ gtk_widget_set_sensitive (control, page->capabilities & PREFERENCES_ALERT_DESKTOP);
+ if (!(page->capabilities & PREFERENCES_ALERT_DESKTOP))
+ gtk_widget_set_tooltip_text (control, _("Desktop notifications are unavailable. Your saved choice is kept."));
+ else if (!strcmp (id, "private"))
+ gtk_widget_set_tooltip_text (control, _("Also covers notices, channel invitations and file offers."));
+ }
+ if (i == 2)
+ {
+ page->tray_alerts[row - 1] = control;
+ gtk_widget_set_tooltip_text (control, _("Enable the system tray icon below to use this indicator."));
+ }
+ }
+}
+
+GtkWidget *
+preferences_notifications_page_new (struct zoitechatprefs *prefs,
+ PreferencesAlertCapabilities capabilities, char **beep_file,
+ PreferencesSoundChanged sound_changed, gpointer user_data)
+{
+ PreferencesPage *page = page_new (PAGE_NOTIFICATIONS, prefs);
+ GtkWidget *box, *row, *button, *advanced, *rules;
+ const char *const events[] = {N_("Private message"), N_("Mention"), N_("Channel message"), NULL};
+ page->capabilities = capabilities;
+ page->beep_file = beep_file;
+ page->sound_changed = sound_changed;
+ page->sound_data = user_data;
+ box = page_section (page->root, _("Notify me about"), NULL);
+ GtkWidget *grid = gtk_grid_new ();
+ gtk_widget_set_name (grid, "notification-options");
+ gtk_grid_set_column_spacing (GTK_GRID (grid), 16);
+ gtk_grid_set_row_spacing (GTK_GRID (grid), 4);
+ const char *headers[] = {N_("Desktop alert"), N_("Sound"), N_("Tray icon"),
+#ifdef HAVE_GTK_MAC
+ N_("Dock")};
+#else
+ N_("Taskbar")};
+#endif
+ int column = 1;
+ for (int i = 0; i < 4; i++)
+ {
+ if (i == 2 && !(capabilities & PREFERENCES_ALERT_TRAY)) continue;
+ if (i == 3 && !(capabilities & PREFERENCES_ALERT_ATTENTION)) continue;
+ GtkWidget *label = gtk_label_new (_(headers[i]));
+ gtk_grid_attach (GTK_GRID (grid), label, column++, 0, 1, 1);
+ }
+ GtkWidget *table = gtk_frame_new (NULL);
+ gtk_frame_set_shadow_type (GTK_FRAME (table), GTK_SHADOW_IN);
+ gtk_container_set_border_width (GTK_CONTAINER (grid), 10);
+ gtk_style_context_add_class (gtk_widget_get_style_context (grid), "view");
+ gtk_container_add (GTK_CONTAINER (table), grid);
+ page_pack (box, table);
+ page_notification_group (page, grid, 1, _("Private messages"), _("Messages sent directly to you."),
+ &prefs->hex_input_balloon_priv, &prefs->hex_input_beep_priv,
+ &prefs->hex_input_tray_priv, &prefs->hex_input_flash_priv, "private");
+ page_notification_group (page, grid, 2, _("Mentions"), _("Channel messages containing your nickname or matching your mention rules."),
+ &prefs->hex_input_balloon_hilight, &prefs->hex_input_beep_hilight,
+ &prefs->hex_input_tray_hilight, &prefs->hex_input_flash_hilight, "mention");
+ page_notification_group (page, grid, 3, _("Other channel messages"), _("Channel messages without a mention."),
+ &prefs->hex_input_balloon_chans, &prefs->hex_input_beep_chans,
+ &prefs->hex_input_tray_chans, &prefs->hex_input_flash_chans, "channel");
+ page_pack (box, page_label (_("Per-channel alert settings can override these choices."), TRUE));
+ box = page_section (page->root, _("Alert sound"),
+ _("Used by “Play alert sound” above. Leave the filename empty for the system sound. Other event sounds are configured separately."));
+ page->sound_entry = gtk_entry_new ();
+ gtk_widget_set_name (page->sound_entry, "alert-sound-file");
+ gtk_entry_set_placeholder_text (GTK_ENTRY (page->sound_entry), _("System sound"));
+ atk_object_set_name (gtk_widget_get_accessible (page->sound_entry), _("Alert sound filename"));
+ gtk_entry_set_text (GTK_ENTRY (page->sound_entry), *beep_file ? *beep_file : "");
+ page_pack (box, page->sound_entry);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ button = gtk_button_new_with_label (_("Choose file…"));
+ g_signal_connect (button, "clicked", G_CALLBACK (page_choose_sound), page);
+ page_pack (row, button);
+ button = gtk_button_new_with_label (_("Play"));
+ g_signal_connect (button, "clicked", G_CALLBACK (page_play_sound), page);
+ page_pack (row, button);
+ button = gtk_button_new_with_label (_("Use system sound"));
+ g_signal_connect (button, "clicked", G_CALLBACK (page_system_sound), page);
+ page_pack (row, button);
+ page_pack (box, row);
+ button = gtk_button_new_with_label (_("Event sounds…"));
+ gtk_widget_set_name (button, "open-event-sounds");
+ g_signal_connect (button, "clicked", G_CALLBACK (page_open_sounds), NULL);
+ gtk_widget_set_halign (button, GTK_ALIGN_START);
+ page_pack (box, button);
+ g_signal_connect (page->sound_entry, "changed", G_CALLBACK (page_sound_changed), page);
+ box = page_section (page->root, _("Quiet rules"), NULL);
+ page_toggle (page, box, "quiet-away", _("Mute alerts while marked away"), &prefs->hex_away_omit_alerts);
+ page_toggle (page, box, "quiet-focused", _("Mute desktop notifications and alert sounds while the main window is active"), &prefs->hex_gui_focus_omitalerts);
+ if (capabilities & PREFERENCES_ALERT_HIDDEN_ONLY)
+ page->quiet_hidden = page_toggle (page, box, "quiet-hidden", _("Show desktop notifications only when hidden to the tray"), &prefs->hex_gui_tray_quiet);
+ gtk_widget_set_tooltip_text (box, _("Per-channel alert overrides still apply. Event sounds and activity indicators can follow different rules."));
+ advanced = gtk_expander_new (_("Test notification"));
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_widget_set_margin_start (box, 12);
+ gtk_container_add (GTK_CONTAINER (advanced), box);
+ page_pack (page->root, advanced);
+ page_pack (box, page_label (_("Sends immediately and bypasses quiet rules."), TRUE));
+ page->test_event = gtk_combo_box_text_new ();
+ gtk_widget_set_name (page->test_event, "notification-test-event");
+ for (int i = 0; events[i]; i++)
+ gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (page->test_event), _(events[i]));
+ gtk_combo_box_set_active (GTK_COMBO_BOX (page->test_event), 0);
+ page_row (box, _("Message type"), page->test_event);
+ button = gtk_button_new_with_label (_("Send test"));
+ gtk_widget_set_name (button, "notification-test");
+ gtk_widget_set_sensitive (button, capabilities & PREFERENCES_ALERT_DESKTOP);
+ g_signal_connect (button, "clicked", G_CALLBACK (page_test_notification), page);
+ page_pack (box, button);
+ page->test_status = page_label (capabilities & PREFERENCES_ALERT_DESKTOP ?
+ _("Your desktop may require notifications to be allowed for ZoiteChat.") :
+ _("Desktop notifications are not available in this session. Your saved choices are kept."), TRUE);
+ gtk_widget_set_name (page->test_status, "notification-test-status");
+ page_pack (box, page->test_status);
+ advanced = gtk_expander_new (_("Mention rules"));
+ rules = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_widget_set_margin_start (rules, 12);
+ gtk_container_add (GTK_CONTAINER (advanced), rules);
+ page_pack (rules, page_label (_("Separate entries with commas. Wildcards (*) are supported. Your nickname is included automatically."), TRUE));
+ page_entry (page, rules, "mention-words", _("Additional words to highlight"), prefs->hex_irc_extra_hilight, sizeof prefs->hex_irc_extra_hilight);
+ page_entry (page, rules, "mention-ignore-nicks", _("Nicknames not to highlight"), prefs->hex_irc_no_hilight, sizeof prefs->hex_irc_no_hilight);
+ page_entry (page, rules, "mention-always-nicks", _("Nicknames to always highlight"), prefs->hex_irc_nick_hilight, sizeof prefs->hex_irc_nick_hilight);
+ page_pack (page->root, advanced);
+ if (capabilities & PREFERENCES_ALERT_TRAY_BEHAVIOR)
+ {
+ box = page_section (page->root, _("System tray"), NULL);
+ page_toggle (page, box, "tray-enabled", _("Show the system tray icon"), &prefs->hex_gui_tray);
+ page->tray_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page_pack (box, page->tray_options);
+ page_toggle (page, page->tray_options, "tray-minimize", _("Hide to the tray when minimized"), &prefs->hex_gui_tray_minimize);
+ page_toggle (page, page->tray_options, "tray-close", _("Hide to the tray when the main window is closed"), &prefs->hex_gui_tray_close);
+ page_toggle (page, page->tray_options, "tray-away", _("Mark away when hidden to the tray and back when restored"), &prefs->hex_gui_tray_away);
+ }
+ page_refresh (page);
+ return page->root;
+}
+
+static gboolean
+page_path_separator (char c)
+{
+#ifdef WIN32
+ return c == '/' || c == '\\';
+#else
+ return c == '/';
+#endif
+}
+
+/* Separate the fixed folder from a filename pattern. Stop at the first
+ * substitution so a custom dated directory remains part of the pattern. */
+static void
+page_log_parts (PreferencesPage *page, const char *mask, char **folder, char **pattern)
+{
+ const char *last = NULL;
+ if (!g_path_is_absolute (mask))
+ {
+ *folder = g_build_filename (page->config_dir, "logs", NULL);
+ *pattern = g_strdup (mask);
+ return;
+ }
+ for (const char *p = mask; *p; p++)
+ {
+ if (*p == '%' && p[1] == '%') { p++; continue; }
+ if (*p == '%') break;
+ if (page_path_separator (*p)) last = p;
+ }
+ if (!last)
+ {
+ *folder = g_path_get_dirname (mask);
+ *pattern = g_path_get_basename (mask);
+ }
+ else
+ {
+ char *raw = g_strndup (mask, last - mask + 1);
+ char **parts = g_strsplit (raw, "%%", -1);
+ *folder = g_strjoinv ("%", parts);
+ *pattern = g_strdup (last + 1);
+ g_strfreev (parts);
+ g_free (raw);
+ }
+}
+
+static char *
+page_log_join (const char *folder, const char *pattern)
+{
+ char **parts = g_strsplit (folder, "%", -1);
+ char *escaped = g_strjoinv ("%%", parts);
+ char *mask = g_build_filename (escaped, pattern, NULL);
+ g_strfreev (parts);
+ g_free (escaped);
+ return mask;
+}
+
+static void
+page_open_folder (GtkButton *button, gpointer unused)
+{
+ const char *path = g_object_get_data (G_OBJECT (button), "folder-path");
+ if (path && g_file_test (path, G_FILE_TEST_IS_DIR)) fe_open_url (path);
+}
+
+static void
+page_folder_target (GtkWidget *button, const char *path)
+{
+ g_object_set_data_full (G_OBJECT (button), "folder-path", g_strdup (path), g_free);
+ gtk_widget_set_sensitive (button, g_file_test (path, G_FILE_TEST_IS_DIR));
+}
+
+static GtkWidget *
+page_folder_button (GtkWidget *box, const char *name, const char *label, const char *path)
+{
+ GtkWidget *button = gtk_button_new_with_label (label);
+ gtk_widget_set_name (button, name);
+ gtk_widget_set_halign (button, GTK_ALIGN_START);
+ page_folder_target (button, path);
+ g_signal_connect (button, "clicked", G_CALLBACK (page_open_folder), NULL);
+ page_pack (box, button);
+ return button;
+}
+
+static GtkWidget *
+page_path_label (GtkWidget *box, const char *name, const char *path)
+{
+ GtkWidget *label = page_label (path, FALSE);
+ gtk_label_set_selectable (GTK_LABEL (label), TRUE);
+ gtk_label_set_line_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD_CHAR);
+ gtk_widget_set_name (label, name);
+ page_pack (box, label);
+ return label;
+}
+
+static void
+page_log_entry_changed (GtkEntry *entry, PreferencesPage *page)
+{
+ if (page->refreshing) return;
+ page->log_dirty = TRUE;
+ if (!g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->log_style)), "custom"))
+ {
+ g_free (page->custom_logmask);
+ page->custom_logmask = g_strdup (gtk_entry_get_text (entry));
+ }
+ page_refresh (page);
+}
+
+static void
+page_log_style_changed (GtkComboBox *combo, PreferencesPage *page)
+{
+ const char *id = gtk_combo_box_get_active_id (combo);
+ char *folder, *pattern, *mask;
+ if (!id) return;
+ page_log_parts (page, page->prefs->hex_irc_logmask, &folder, &pattern);
+ mask = !strcmp (id, "custom") ? g_strdup (page->custom_logmask) :
+ g_path_is_absolute (page->prefs->hex_irc_logmask) ? page_log_join (folder, id) : g_strdup (id);
+ gtk_entry_set_text (GTK_ENTRY (page->log_entry), mask);
+ page_refresh (page);
+ g_free (folder); g_free (pattern); g_free (mask);
+}
+
+static void
+page_log_change_folder (PreferencesPage *page, const char *folder)
+{
+ char *old_folder, *pattern, *custom_folder, *custom_pattern;
+ char *mask, *custom;
+ page_log_parts (page, page->prefs->hex_irc_logmask, &old_folder, &pattern);
+ page_log_parts (page, page->custom_logmask, &custom_folder, &custom_pattern);
+ mask = folder ? page_log_join (folder, pattern) : g_strdup (pattern);
+ custom = folder ? page_log_join (folder, custom_pattern) : g_strdup (custom_pattern);
+ if (strlen (mask) < sizeof page->prefs->hex_irc_logmask &&
+ strlen (custom) < sizeof page->prefs->hex_irc_logmask)
+ {
+ /* A later switch to Custom must keep the folder just chosen. */
+ g_free (page->custom_logmask);
+ page->custom_logmask = custom;
+ custom = NULL;
+ gtk_entry_set_text (GTK_ENTRY (page->log_entry), mask);
+ }
+ else page_status (page->log_status,
+ _("That folder makes the filename format too long. Choose a shorter path or use a shorter custom format."));
+ g_free (old_folder); g_free (pattern); g_free (custom_folder); g_free (custom_pattern);
+ g_free (mask); g_free (custom);
+}
+
+static void
+page_log_folder_response (GtkDialog *dialog, int response, GtkWidget *root)
+{
+ PreferencesPage *page = g_object_get_data (G_OBJECT (root), "preferences-page");
+ if (response == GTK_RESPONSE_ACCEPT)
+ {
+ char *native = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
+ char *folder = native ? g_filename_to_utf8 (native, -1, NULL, NULL, NULL) : NULL;
+ if (folder)
+ {
+ page_log_change_folder (page, folder);
+ }
+ else page_status (page->log_status, _("This folder name cannot be represented in the configuration's UTF-8 format."));
+ g_free (native); g_free (folder);
+ }
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+page_choose_log_folder (GtkButton *button, PreferencesPage *page)
+{
+ GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Choose a log folder"), page_window (page),
+ GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("Cancel"), GTK_RESPONSE_CANCEL,
+ _("Choose"), GTK_RESPONSE_ACCEPT, NULL);
+ char *folder, *pattern, *native;
+ page_log_parts (page, page->prefs->hex_irc_logmask, &folder, &pattern);
+ native = g_filename_from_utf8 (folder, -1, NULL, NULL, NULL);
+ if (native) gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), native);
+ g_free (folder); g_free (pattern); g_free (native);
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+ gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE);
+ g_signal_connect_object (dialog, "response", G_CALLBACK (page_log_folder_response), page->root, 0);
+ gtk_widget_show (dialog);
+}
+
+static void
+page_default_log_folder (GtkButton *button, PreferencesPage *page)
+{
+ page_log_change_folder (page, NULL);
+}
+
+static void
+page_history_refresh (PreferencesPage *page)
+{
+ struct zoitechatprefs *prefs = page->prefs;
+ char *folder, *pattern, *path, stamp[256], *line;
+ gboolean custom = !g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->log_style)), "custom");
+ const char *input = gtk_entry_get_text (GTK_ENTRY (page->log_entry));
+ gboolean input_valid = *input && strlen (input) < sizeof prefs->hex_irc_logmask &&
+ !strpbrk (input, "\r\n") && !page_path_separator (input[strlen (input) - 1]);
+ page_log_parts (page, prefs->hex_irc_logmask, &folder, &pattern);
+ gtk_label_set_text (GTK_LABEL (page->log_folder), folder);
+ page_folder_target (page->log_open, folder);
+ gtk_widget_set_sensitive (page->log_options, prefs->hex_irc_logging);
+ gtk_widget_set_visible (page->log_custom, custom);
+ gtk_widget_set_sensitive (page->stamp_options, prefs->hex_stamp_log);
+ gtk_widget_set_visible (gtk_widget_get_parent (page->stamp_entry),
+ !g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->stamp_combo)), "custom"));
+ gtk_widget_set_sensitive (page->stamp_entry,
+ !g_strcmp0 (gtk_combo_box_get_active_id (GTK_COMBO_BOX (page->stamp_combo)), "custom"));
+ path = input_valid ? log_build_pathname (prefs->hex_irc_logmask, page->config_dir,
+ "irc.example.net", "#example", "ExampleNet", time (NULL)) : NULL;
+ gtk_label_set_text (GTK_LABEL (page->log_file), path ? path : _("Enter a valid filename format to see an example."));
+ page_status (page->log_status, !path ?
+ _("The format must fit in 255 UTF-8 bytes and include a filename. Correct the custom format before saving changes to it.") :
+ "");
+ page_timestamp (page, stamp, sizeof stamp);
+ line = g_strdup_printf ("%s %s", prefs->hex_stamp_log ? stamp : "", _("Hello! This is an example log entry."));
+ gtk_label_set_text (GTK_LABEL (page->log_line), line);
+ gtk_widget_set_sensitive (page->links_limit, prefs->hex_url_grabber);
+ char *history_dir = g_build_filename (page->config_dir, "scrollback", NULL);
+ page_folder_target (page->history_open, history_dir);
+ gtk_widget_set_tooltip_text (page->history_open, prefs->hex_text_replay ?
+ _("History folders are created as needed.") :
+ _("Existing history is kept. Per-conversation overrides still apply."));
+ g_free (history_dir); g_free (folder); g_free (pattern); g_free (path); g_free (line);
+}
+
+gboolean
+preferences_history_page_stage (GtkWidget *widget, GError **error)
+{
+ PreferencesPage *page = g_object_get_data (G_OBJECT (widget), "preferences-page");
+ const char *input;
+ char *path;
+ if (!page || page->kind != PAGE_HISTORY || !page->log_dirty || !page->prefs->hex_irc_logging) return TRUE;
+ input = gtk_entry_get_text (GTK_ENTRY (page->log_entry));
+ path = *input && strlen (input) < sizeof page->prefs->hex_irc_logmask && !strpbrk (input, "\r\n") &&
+ !page_path_separator (input[strlen (input) - 1]) ?
+ log_build_pathname (input, page->config_dir, "irc.example.net", "#example", "ExampleNet", time (NULL)) : NULL;
+ if (!path)
+ {
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ _("Enter a log filename format of at most 255 UTF-8 bytes, including a filename and no line breaks."));
+ gtk_widget_grab_focus (gtk_widget_get_visible (page->log_custom) ? page->log_entry : page->log_style);
+ return FALSE;
+ }
+ g_free (path);
+ return TRUE;
+}
+
+GtkWidget *
+preferences_history_page_new (struct zoitechatprefs *prefs, const char *config_dir)
+{
+ PreferencesPage *page = page_new (PAGE_HISTORY, prefs);
+ GtkWidget *box, *row, *button, *details;
+ const char *patterns[] = {"%n" G_DIR_SEPARATOR_S "%c.log", "%n" G_DIR_SEPARATOR_S "%Y-%m" G_DIR_SEPARATOR_S "%c.log",
+ "%n" G_DIR_SEPARATOR_S "%c" G_DIR_SEPARATOR_S "%Y-%m-%d.log", NULL};
+ const char *const pattern_names[] = {N_("By network and conversation"), N_("By network and month"), N_("Daily file for each conversation"), NULL};
+ const char *formats[] = {"%H:%M ", "%I:%M %p ", "[%H:%M:%S] ", "%Y-%m-%d %H:%M:%S ", NULL};
+ const char *const format_names[] = {N_("24-hour · 14:35"), N_("12-hour · 02:35 PM"), N_("Bracketed · [14:35:08]"), N_("Date and time"), NULL};
+ char *history_dir, *folder, *pattern, *url_file;
+ page->config_dir = g_strdup (config_dir);
+ page->custom_logmask = g_strdup (prefs->hex_irc_logmask);
+ box = page_section (page->root, _("Conversation history"),
+ _("The conversation buffer holds messages while you chat. Saved history can bring recent messages back when you return; readable log files are configured separately below."));
+ page_pack (box, page_label (_("Recent history restores messages when you reopen a conversation."), TRUE));
+ page_spin (page, box, "history-line-limit", _("Conversation length (lines)"), &prefs->hex_text_max_lines, 100000);
+ gtk_widget_set_tooltip_text (box, _("The line limit also controls saved-history trimming. Values 0–2 remove the limit for open conversations. Saved history has a separate built-in cap; log files are unaffected."));
+ page_toggle (page, box, "restore-history", _("Save and restore recent history"), &prefs->hex_text_replay);
+ history_dir = g_build_filename (config_dir, "scrollback", NULL);
+ details = gtk_expander_new (_("History folder"));
+ row = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add (GTK_CONTAINER (details), row);
+ page_path_label (row, "history-folder", history_dir);
+ page->history_open = page_folder_button (row, "open-history-folder", _("Open history folder"), history_dir);
+ page_pack (box, details);
+ box = page_section (page->root, _("Saved logs"),
+ _("Keep readable conversation files on disk. Turning this off does not delete files already saved."));
+ page_pack (box, page_label (_("Log files keep a readable copy of conversations on disk."), TRUE));
+ page_toggle (page, box, "save-logs", _("Save conversations to log files"), &prefs->hex_irc_logging);
+ page->log_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+ page_pack (box, page->log_options);
+ page->log_style = gtk_combo_box_text_new ();
+ gtk_widget_set_name (page->log_style, "log-file-style");
+ for (int i = 0; patterns[i]; i++)
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->log_style), patterns[i], _(pattern_names[i]));
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->log_style), "custom", _("Custom filename format"));
+ page_log_parts (page, prefs->hex_irc_logmask, &folder, &pattern);
+ if (!gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->log_style), pattern))
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->log_style), "custom");
+ page_row (page->log_options, _("Organize files"), page->log_style);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ button = gtk_button_new_with_label (_("Choose folder…"));
+ gtk_widget_set_name (button, "choose-log-folder");
+ g_signal_connect (button, "clicked", G_CALLBACK (page_choose_log_folder), page);
+ page_pack (row, button);
+ button = gtk_button_new_with_label (_("Use default folder"));
+ gtk_widget_set_name (button, "default-log-folder");
+ g_signal_connect (button, "clicked", G_CALLBACK (page_default_log_folder), page);
+ page_pack (row, button);
+ page_pack (page->log_options, row);
+ page->log_custom = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page->log_entry = page_entry (page, page->log_custom, "log-file-format", _("Filename format"), prefs->hex_irc_logmask, sizeof prefs->hex_irc_logmask);
+ gtk_widget_set_tooltip_text (page->log_entry, _("%n: network, %c: conversation, %s: server, %Y: year, %m: month, %d: day. Relative paths use the default log folder. Use %% for a literal percent sign."));
+ gtk_widget_show_all (page->log_custom);
+ gtk_widget_set_no_show_all (page->log_custom, TRUE);
+ page_pack (page->log_options, page->log_custom);
+ page_pack (box, page_label (_("Log folder"), TRUE));
+ page->log_folder = page_path_label (box, "log-folder", folder);
+ page->log_open = page_folder_button (box, "open-log-folder", _("Open log folder"), folder);
+ details = gtk_expander_new (_("Filename example"));
+ row = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add (GTK_CONTAINER (details), row);
+ page->log_file = page_path_label (row, "log-example-file", "");
+ gtk_widget_set_tooltip_text (details, _("ExampleNet, #example, today's date."));
+ page_pack (box, details);
+ page->log_status = page_label ("", TRUE);
+ page_pack (box, page->log_status);
+ gtk_widget_set_tooltip_text (page->log_open, _("Folders are created when messages are logged. Available once the folder exists."));
+ g_signal_connect (page->log_style, "changed", G_CALLBACK (page_log_style_changed), page);
+ g_signal_connect (page->log_entry, "changed", G_CALLBACK (page_log_entry_changed), page);
+ box = page_section (page->root, _("Log timestamps"), _("These affect saved log files. Conversation timestamps are configured in Text & layout."));
+ page_toggle (page, box, "log-timestamps", _("Include a time with each saved message"), &prefs->hex_stamp_log);
+ page->stamp_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page_pack (box, page->stamp_options);
+ page->stamp_combo = gtk_combo_box_text_new ();
+ gtk_widget_set_name (page->stamp_combo, "log-timestamp-style");
+ for (int i = 0; formats[i]; i++)
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->stamp_combo), formats[i], _(format_names[i]));
+ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (page->stamp_combo), "custom", _("Custom format"));
+ if (!gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->stamp_combo), prefs->hex_stamp_log_format))
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (page->stamp_combo), "custom");
+ page->custom_stamp = g_strdup (prefs->hex_stamp_log_format);
+ page_row (page->stamp_options, _("Time style"), page->stamp_combo);
+ page->stamp_entry = page_entry (page, page->stamp_options, "log-timestamp-format", _("Custom timestamp format"), prefs->hex_stamp_log_format, sizeof prefs->hex_stamp_log_format);
+ gtk_widget_show_all (gtk_widget_get_parent (page->stamp_entry));
+ gtk_widget_set_no_show_all (gtk_widget_get_parent (page->stamp_entry), TRUE);
+ gtk_widget_set_tooltip_text (page->stamp_entry, _("%H: hour (00–23), %I: hour (01–12), %M: minute, %S: second, %p: AM/PM. Spaces are kept."));
+ page->log_line = page_path_label (box, "log-example-line", "");
+ g_signal_connect (page->stamp_combo, "changed", G_CALLBACK (page_stamp_changed), page);
+ g_signal_connect (page->stamp_entry, "changed", G_CALLBACK (page_custom_stamp_changed), page);
+ box = page_section (page->root, _("Collected links"), _("The URL grabber keeps a list of links found in chat. Saving links to a file is an independent choice."));
+ page_toggle (page, box, "collect-links", _("Collect links in the URL grabber"), &prefs->hex_url_grabber);
+ page->links_limit = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page_pack (box, page->links_limit);
+ page_spin (page, page->links_limit, "link-limit", _("Maximum collected links"), &prefs->hex_url_grabber_limit, 9999);
+ gtk_widget_set_tooltip_text (page->links_limit, _("0 keeps an unlimited number of links."));
+ page_toggle (page, box, "save-links", _("Also save discovered links to url.log"), &prefs->hex_url_logging);
+ url_file = g_build_filename (config_dir, "url.log", NULL);
+ page_path_label (box, "url-log-file", url_file);
+ page_folder_button (box, "open-data-folder", _("Open data folder"), config_dir);
+ gtk_widget_set_tooltip_text (box, _("Disabling collection or logging keeps existing files."));
+ page_history_refresh (page);
+ g_free (history_dir); g_free (folder); g_free (pattern); g_free (url_file);
+ return page->root;
+}
+
+static gboolean
+typing_has_language (const char *configured, const char *code)
+{
+ char **tokens = g_strsplit_set (configured, ", \t", -1);
+ gboolean found = FALSE;
+ for (int i = 0; tokens[i]; i++) if (!strcmp (tokens[i], code)) found = TRUE;
+ g_strfreev (tokens);
+ return found;
+}
+
+static void
+typing_completion_example (GtkWidget *widget, PreferencesPage *page)
+{
+ const char *prefix = gtk_entry_get_text (GTK_ENTRY (page->completion_input));
+ const char *names[] = {page->prefs->hex_completion_sort == 1 ? "Alice" : "Alex",
+ page->prefs->hex_completion_sort == 1 ? "Alex" : "Alice"};
+ char *folded = g_utf8_casefold (prefix, -1);
+ GPtrArray *matches = g_ptr_array_new ();
+ for (guint i = 0; i < G_N_ELEMENTS (names); i++)
+ {
+ char *name = g_utf8_casefold (names[i], -1);
+ if (*folded && g_str_has_prefix (name, folded)) g_ptr_array_add (matches, (gpointer)names[i]);
+ g_free (name);
+ }
+ char *result;
+ if (!matches->len) result = g_strdup (_("No sample match. Try Al, Alex or Alice."));
+ else if (matches->len > 1 && (page->prefs->hex_completion_amount <= 0 ||
+ matches->len > (guint) page->prefs->hex_completion_amount))
+ result = g_strdup_printf (_("Show matching names: %s, %s"), (char *)matches->pdata[0], (char *)matches->pdata[1]);
+ else
+ {
+ char suffix[8] = "";
+ gunichar ch = g_utf8_get_char_validated (page->prefs->hex_completion_suffix, -1);
+ if (ch && ch != (gunichar)-1 && ch != (gunichar)-2) g_unichar_to_utf8 (ch, suffix);
+ result = g_strdup_printf (_("Complete to: %s%s (followed by a space)"), (char *)matches->pdata[0], suffix);
+ }
+ gtk_label_set_text (GTK_LABEL (page->completion_result), result);
+ g_free (result); g_free (folded); g_ptr_array_unref (matches);
+}
+
+static void
+typing_refresh (PreferencesPage *page)
+{
+ struct zoitechatprefs *prefs = page->prefs;
+ GString *missing = g_string_new ("");
+ char **tokens = g_strsplit_set (prefs->hex_text_spell_langs, ", \t", -1);
+ page->refreshing = TRUE;
+ for (guint i = 0; i < page->language_buttons->len; i++)
+ {
+ GtkWidget *button = page->language_buttons->pdata[i];
+ const char *code = g_object_get_data (G_OBJECT (button), "language-code");
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), typing_has_language (prefs->hex_text_spell_langs, code));
+ }
+ for (int i = 0; tokens[i]; i++)
+ {
+ gboolean available = FALSE;
+ if (!*tokens[i]) continue;
+ for (guint j = 0; j < page->language_buttons->len; j++)
+ if (!strcmp (tokens[i], g_object_get_data (G_OBJECT (page->language_buttons->pdata[j]), "language-code"))) available = TRUE;
+ if (!available)
+ {
+ if (missing->len) g_string_append (missing, ", ");
+ g_string_append (missing, tokens[i]);
+ }
+ }
+ char *status = !page->language_buttons->len ? g_strdup (_("No dictionaries found. Install dictionaries and reopen Preferences.")) :
+ missing->len ? g_strdup_printf (_("Additional language codes: %s"), missing->str) :
+ g_strdup (
+ !*prefs->hex_text_spell_langs ? _("No selection; ZoiteChat will try English (en).") : "");
+ page_status (page->language_status, status);
+ gtk_widget_set_sensitive (page->spelling_options, prefs->hex_gui_input_spell);
+ gtk_widget_set_visible (page->typing_nick, prefs->hex_gui_input_nick);
+ gtk_widget_set_sensitive (page->typing_mode, prefs->hex_gui_input_nick);
+ gtk_label_set_text (GTK_LABEL (page->typing_nick), prefs->hex_gui_input_icon ? "● alice ▾" : "alice ▾");
+ if (page->update_typing_preview) page->update_typing_preview (page->typing_preview, prefs);
+ typing_completion_example (NULL, page);
+ page->refreshing = FALSE;
+ g_free (status); g_string_free (missing, TRUE); g_strfreev (tokens);
+}
+
+static void
+typing_codes_changed (GtkEntry *entry, PreferencesPage *page)
+{
+ const char *text = gtk_entry_get_text (entry);
+ if (strlen (text) >= sizeof page->prefs->hex_text_spell_langs)
+ {
+ page_status (page->language_status, _("Language codes must fit in 63 UTF-8 bytes. Shorten this list before saving."));
+ return;
+ }
+ g_strlcpy (page->prefs->hex_text_spell_langs, text, sizeof page->prefs->hex_text_spell_langs);
+ page_refresh (page);
+}
+
+static void
+typing_suffix_changed (GtkEntry *entry, PreferencesPage *page)
+{
+ const char *text = gtk_entry_get_text (entry);
+ if (strlen (text) < sizeof page->prefs->hex_completion_suffix)
+ g_strlcpy (page->prefs->hex_completion_suffix, text, sizeof page->prefs->hex_completion_suffix);
+ page_refresh (page);
+ if (strlen (text) >= sizeof page->prefs->hex_completion_suffix)
+ {
+ gtk_label_set_text (GTK_LABEL (page->completion_result), _("This suffix is too long. Use a letter or punctuation mark."));
+ GtkWidget *expander = gtk_widget_get_ancestor (page->completion_result, GTK_TYPE_EXPANDER);
+ if (expander) gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE);
+ }
+}
+
+static void
+typing_language_toggled (GtkToggleButton *button, PreferencesPage *page)
+{
+ if (page->refreshing) return;
+ const char *code = g_object_get_data (G_OBJECT (button), "language-code");
+ char **tokens = g_strsplit_set (page->prefs->hex_text_spell_langs, ", \t", -1);
+ GString *value = g_string_new ("");
+ for (int i = 0; tokens[i]; i++)
+ {
+ if (!*tokens[i] || !strcmp (tokens[i], code)) continue;
+ if (value->len) g_string_append_c (value, ',');
+ g_string_append (value, tokens[i]);
+ }
+ if (gtk_toggle_button_get_active (button))
+ {
+ if (value->len) g_string_append_c (value, ',');
+ g_string_append (value, code);
+ }
+ if (value->len < sizeof page->prefs->hex_text_spell_langs)
+ gtk_entry_set_text (GTK_ENTRY (page->language_entry), value->str);
+ else
+ {
+ typing_refresh (page);
+ page_status (page->language_status, _("The language list is full. Shorten the codes under Advanced before adding another dictionary."));
+ }
+ g_string_free (value, TRUE); g_strfreev (tokens);
+}
+
+static gboolean
+typing_language_filter (GtkListBoxRow *row, gpointer data)
+{
+ PreferencesPage *page = data;
+ const char *query = gtk_entry_get_text (GTK_ENTRY (page->language_search));
+ const char *text = g_object_get_data (G_OBJECT (row), "language-search");
+ char *folded = g_utf8_casefold (query, -1);
+ gboolean visible = !*folded || strstr (text, folded);
+ g_free (folded);
+ return visible;
+}
+
+static void
+typing_language_search (GtkEntry *entry, PreferencesPage *page)
+{
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (page->language_list));
+}
+
+static void
+typing_open_keyboard (GtkButton *button, gpointer unused)
+{
+ setup_open_page ("completion-shortcuts");
+}
+
+/* Some backends expose only locale tags when the optional ISO name database
+ * is absent. Friendly fallbacks are display-only; backend tags stay untouched. */
+static char *
+typing_language_name (const char *code, const char *provided)
+{
+ if (provided && *provided && strcmp (provided, code)) return g_strdup (provided);
+ static const struct { const char *id, *name; } languages[] = {
+ {"en", N_("English")}, {"fr", N_("French")}, {"de", N_("German")}, {"es", N_("Spanish")},
+ {"pt", N_("Portuguese")}, {"it", N_("Italian")}, {"nl", N_("Dutch")}, {"pl", N_("Polish")},
+ {"uk", N_("Ukrainian")}, {"ru", N_("Russian")}, {"sv", N_("Swedish")}, {"da", N_("Danish")},
+ {"nb", N_("Norwegian Bokmål")}, {"nn", N_("Norwegian Nynorsk")}, {"fi", N_("Finnish")},
+ {"cs", N_("Czech")}, {"sk", N_("Slovak")}, {"hu", N_("Hungarian")}, {"ro", N_("Romanian")},
+ {"el", N_("Greek")}, {"tr", N_("Turkish")}, {"ar", N_("Arabic")}, {"he", N_("Hebrew")},
+ {"ja", N_("Japanese")}, {"ko", N_("Korean")}, {"zh", N_("Chinese")}, {"hi", N_("Hindi")},
+ };
+ static const struct { const char *id, *name; } regions[] = {
+ {"CA", N_("Canada")}, {"US", N_("United States")}, {"GB", N_("United Kingdom")},
+ {"AU", N_("Australia")}, {"NZ", N_("New Zealand")}, {"IE", N_("Ireland")},
+ {"FR", N_("France")}, {"BE", N_("Belgium")}, {"CH", N_("Switzerland")},
+ {"DE", N_("Germany")}, {"AT", N_("Austria")}, {"ES", N_("Spain")},
+ {"MX", N_("Mexico")}, {"BR", N_("Brazil")}, {"PT", N_("Portugal")},
+ };
+ char **parts = g_strsplit_set (code, "_-.@", -1);
+ const char *language = NULL, *region = parts[0] && parts[1] && *parts[1] ? parts[1] : NULL;
+ for (guint i = 0; i < G_N_ELEMENTS (languages); i++)
+ if (!strcmp (parts[0], languages[i].id)) language = _(languages[i].name);
+ for (guint i = 0; region && i < G_N_ELEMENTS (regions); i++)
+ if (!strcmp (region, regions[i].id)) { region = _(regions[i].name); break; }
+ char *name = !language ? g_strdup (code) : region ? g_strdup_printf ("%s (%s)", language, region) : g_strdup (language);
+ g_strfreev (parts);
+ return name;
+}
+
+GtkWidget *
+preferences_typing_page_new (struct zoitechatprefs *prefs, GtkWidget *preview_entry,
+ const PreferencesSpellLanguage *languages, guint count, PreferencesTypingPreview update_preview)
+{
+ PreferencesPage *page = page_new (PAGE_TYPING, prefs);
+ GtkWidget *box, *row, *scroll, *advanced, *codes, *button, *input_preview, *example;
+ const char *const orders[] = {N_("Alphabetical order"), N_("Most recent speaker"), NULL};
+ page->typing_preview = preview_entry;
+ page->update_typing_preview = update_preview;
+ page->language_buttons = g_ptr_array_new ();
+ box = page_section (page->root, _("Message input"), NULL);
+ input_preview = gtk_expander_new (_("Input preview"));
+ gtk_widget_set_name (input_preview, "input-preview-expander");
+ gtk_expander_set_expanded (GTK_EXPANDER (input_preview), TRUE);
+ example = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add (GTK_CONTAINER (input_preview), example);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ page->typing_nick = page_label ("alice ▾", FALSE);
+ gtk_widget_set_no_show_all (page->typing_nick, TRUE);
+ page_pack (row, page->typing_nick);
+ gtk_widget_set_hexpand (preview_entry, TRUE);
+ gtk_entry_set_text (GTK_ENTRY (preview_entry), _("Hello, welcome to ZoiteChat."));
+ gtk_widget_set_name (preview_entry, "typing-preview");
+ atk_object_set_name (gtk_widget_get_accessible (preview_entry), _("Local typing preview; text is not sent"));
+ gtk_box_pack_start (GTK_BOX (row), preview_entry, TRUE, TRUE, 0);
+ page_pack (example, row);
+ page_pack (example, page_label (_("Local preview; text is never sent."), TRUE));
+ page_toggle (page, box, "input-formatting", _("Show colours and formatting in the input"), &prefs->hex_gui_input_attr);
+ page_toggle (page, box, "input-nickname", _("Show nickname selector"), &prefs->hex_gui_input_nick);
+ page->typing_mode = page_toggle (page, box, "input-mode-icon", _("Show channel status icon"), &prefs->hex_gui_input_icon);
+ gtk_widget_set_tooltip_text (page->typing_mode, _("Shows your channel role, such as operator or voice."));
+ page_pack (box, input_preview);
+ box = page_section (page->root, _("Spelling"), _("Choose dictionaries reported by this system. Other configured language codes are preserved."));
+ page_toggle (page, box, "spelling-enabled", _("Check spelling while I type"), &prefs->hex_gui_input_spell);
+ page->spelling_options = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ page_pack (box, page->spelling_options);
+ page->language_search = gtk_search_entry_new ();
+ gtk_widget_set_name (page->language_search, "spelling-search");
+ gtk_entry_set_placeholder_text (GTK_ENTRY (page->language_search), _("Find a language or language code"));
+ page_pack (page->spelling_options, page->language_search);
+ page->language_list = gtk_list_box_new ();
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (page->language_list), GTK_SELECTION_NONE);
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (page->language_list), typing_language_filter, page, NULL);
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (page->language_list), page_label (_("No matching installed dictionaries"), TRUE));
+ for (guint i = 0; i < count; i++)
+ {
+ char *name = typing_language_name (languages[i].code, languages[i].name);
+ char *label = !strcmp (name, languages[i].code) ? g_strdup (name) : g_strdup_printf ("%s · %s", name, languages[i].code);
+ button = gtk_check_button_new_with_label (label);
+ 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))), 52);
+ g_object_set_data_full (G_OBJECT (button), "language-code", g_strdup (languages[i].code), g_free);
+ gtk_container_add (GTK_CONTAINER (page->language_list), button);
+ GtkWidget *list_row = gtk_widget_get_parent (button);
+ g_object_set_data_full (G_OBJECT (list_row), "language-search", g_utf8_casefold (label, -1), g_free);
+ g_ptr_array_add (page->language_buttons, button);
+ g_signal_connect (button, "toggled", G_CALLBACK (typing_language_toggled), page);
+ g_free (label); g_free (name);
+ }
+ scroll = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), count ? 68 : 35);
+ gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (scroll), 120);
+ gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (scroll), TRUE);
+ gtk_container_add (GTK_CONTAINER (scroll), page->language_list);
+ page_pack (page->spelling_options, scroll);
+ advanced = gtk_expander_new (_("Advanced language codes"));
+ codes = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ gtk_container_set_border_width (GTK_CONTAINER (codes), 8);
+ gtk_container_add (GTK_CONTAINER (advanced), codes);
+ page->language_entry = gtk_entry_new ();
+ gtk_widget_set_name (page->language_entry, "spelling-codes");
+ atk_object_set_name (gtk_widget_get_accessible (page->language_entry), _("Dictionary language codes"));
+ gtk_entry_set_text (GTK_ENTRY (page->language_entry), prefs->hex_text_spell_langs);
+ page_pack (codes, page->language_entry);
+ gtk_entry_set_placeholder_text (GTK_ENTRY (page->language_entry), "en_CA, en_US, fr_FR");
+ gtk_widget_set_tooltip_text (page->language_entry, _("Separate codes with commas or spaces. Codes not listed on this system are preserved."));
+ page_pack (page->spelling_options, advanced);
+ page->language_status = page_label ("", TRUE);
+ page_pack (box, page->language_status);
+ g_signal_connect (page->language_search, "search-changed", G_CALLBACK (typing_language_search), page);
+ g_signal_connect (page->language_entry, "changed", G_CALLBACK (typing_codes_changed), page);
+ box = page_section (page->root, _("Nickname completion"), _("Completion finishes matching nicknames and commands. Your completion shortcut is configured under Keyboard shortcuts."));
+ page->suffix_entry = gtk_entry_new ();
+ gtk_widget_set_name (page->suffix_entry, "completion-suffix");
+ atk_object_set_name (gtk_widget_get_accessible (page->suffix_entry), _("Nickname completion suffix"));
+ gtk_entry_set_text (GTK_ENTRY (page->suffix_entry), prefs->hex_completion_suffix);
+ gtk_entry_set_width_chars (GTK_ENTRY (page->suffix_entry), 6);
+ gtk_entry_set_max_width_chars (GTK_ENTRY (page->suffix_entry), 6);
+ page_row (box, _("Add after nickname"), page->suffix_entry);
+ gtk_widget_set_tooltip_text (page->suffix_entry, _("First character only, followed by a space. Leave empty for no suffix."));
+ page_combo (page, box, "completion-order", _("Match order"), &prefs->hex_completion_sort, 0, orders);
+ gtk_widget_set_tooltip_text (page_spin (page, box, "completion-limit", _("List matches if more than"), &prefs->hex_completion_amount, 1000), _("Above this number, list matching names instead of completing. With 0, multiple matches are always listed; a unique match can still complete."));
+ advanced = gtk_expander_new (_("Completion example"));
+ gtk_widget_set_name (advanced, "completion-example-expander");
+ example = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_widget_set_margin_start (example, 12);
+ gtk_container_add (GTK_CONTAINER (advanced), example);
+ page_pack (box, advanced);
+ page->completion_input = gtk_entry_new ();
+ gtk_widget_set_name (page->completion_input, "completion-example-input");
+ gtk_entry_set_text (GTK_ENTRY (page->completion_input), "Al");
+ atk_object_set_name (gtk_widget_get_accessible (page->completion_input), _("Sample nickname prefix"));
+ page_pack (example, page_label (_("Alice (most recent) and Alex"), TRUE));
+ page_row (example, _("Prefix"), page->completion_input);
+ page->completion_result = page_label ("", FALSE);
+ gtk_label_set_selectable (GTK_LABEL (page->completion_result), TRUE);
+ page_pack (example, page->completion_result);
+ button = gtk_button_new_with_label (_("Open keyboard shortcuts…"));
+ gtk_widget_set_halign (button, GTK_ALIGN_START);
+ g_signal_connect (button, "clicked", G_CALLBACK (typing_open_keyboard), NULL);
+ page_pack (box, button);
+ g_signal_connect (page->suffix_entry, "changed", G_CALLBACK (typing_suffix_changed), page);
+ g_signal_connect (page->completion_input, "changed", G_CALLBACK (typing_completion_example), page);
+ g_signal_connect (page->completion_input, "activate", G_CALLBACK (typing_completion_example), page);
+ typing_refresh (page);
+ return page->root;
+}
+
+gboolean
+preferences_typing_page_stage (GtkWidget *widget, GError **error)
+{
+ PreferencesPage *page = g_object_get_data (G_OBJECT (widget), "preferences-page");
+ if (!page || page->kind != PAGE_TYPING) return TRUE;
+ GtkWidget *entries[] = {page->language_entry, page->suffix_entry};
+ gsize limits[] = {sizeof page->prefs->hex_text_spell_langs, sizeof page->prefs->hex_completion_suffix};
+ for (guint i = 0; i < G_N_ELEMENTS (entries); i++)
+ {
+ const char *text = gtk_entry_get_text (GTK_ENTRY (entries[i]));
+ if (strlen (text) >= limits[i] || strpbrk (text, "\r\n"))
+ {
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ i ? _("The nickname suffix must fit in 3 UTF-8 bytes and stay on one line.") :
+ _("Dictionary language codes must fit in 63 UTF-8 bytes and stay on one line."));
+ setup_reveal_widget (entries[i]);
+ gtk_widget_grab_focus (entries[i]);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+typedef struct
+{
+ const char *id, *title, *category, *data1, *data2;
+} ShortcutPresentation;
+
+static const ShortcutPresentation shortcut_presentations[] = {
+ {"Run Command", N_("Run text or a command"), "Commands", N_("Text or command (supports escaped sequences)"), NULL},
+ {"Change Page", N_("Switch conversation"), "Navigation", N_("Conversation, page number, or auto"), N_("Relative page flag (any non-empty value)")},
+ {"Insert in Buffer", N_("Insert text at the cursor"), "Typing", N_("Text to insert (supports escaped sequences)"), NULL},
+ {"Scroll Page", N_("Scroll the conversation"), "Navigation", N_("Direction: Top, Bottom, Up, Down, +1 or -1"), NULL},
+ {"Set Buffer", N_("Replace the input text"), "Typing", N_("Replacement input text"), NULL},
+ {"Last Command", N_("Previous input from history"), "Typing", NULL, NULL},
+ {"Next Command", N_("Next input from history"), "Typing", NULL, NULL},
+ {"Complete nick/command", N_("Complete a nickname or command"), "Typing", N_("Reverse completion flag (any non-empty value)"), NULL},
+ {"Change Selected Nick", N_("Cycle nickname completion"), "Typing", N_("Reverse completion flag (any non-empty value)"), NULL},
+ {"Check For Replace", N_("Apply text replacements"), "Typing", NULL, NULL},
+ {"Move front tab left", N_("Move current tab left"), "Navigation", NULL, NULL},
+ {"Move front tab right", N_("Move current tab right"), "Navigation", NULL, NULL},
+ {"Move tab family left", N_("Move current network left"), "Navigation", NULL, NULL},
+ {"Move tab family right", N_("Move current network right"), "Navigation", NULL, NULL},
+ {"Push input line into history", N_("Save input to history without sending"), "Typing", NULL, NULL},
+ {"Menu Shortcut", N_("Run an application action"), "Application", N_("Application action name or legacy number"), NULL},
+ {"Reopen Closed Tab", N_("Reopen the last closed channel"), "Navigation", NULL, NULL},
+};
+
+typedef struct
+{
+ GtkWidget *root, *view, *search, *category, *count, *editor;
+ GtkWidget *action, *key_label, *record, *duplicate, *remove, *up, *down;
+ GtkWidget *fields, *advanced, *advanced_fields, *rows[2], *labels[2], *entries[2];
+ GtkWidget *reference, *policy, *shared, *syntax;
+ GtkListStore *store;
+ GtkTreeModelFilter *filter;
+ GtkTreeRowReference *selected;
+ PreferencesShortcutAction *actions;
+ guint action_count;
+ PreferencesShortcutModifiers normalize;
+ PreferencesShortcutsRestore restore;
+ gpointer restore_data;
+ gboolean updating, keep_selected;
+} ShortcutsEditor;
+
+static const ShortcutPresentation *
+shortcut_presentation (const char *id)
+{
+ for (guint i = 0; i < G_N_ELEMENTS (shortcut_presentations); i++)
+ if (!g_strcmp0 (id, shortcut_presentations[i].id)) return &shortcut_presentations[i];
+ return NULL;
+}
+
+static const char *
+shortcut_title (const char *id)
+{
+ const ShortcutPresentation *p = shortcut_presentation (id);
+ return p ? _(p->title) : id ? id : _("Choose an action");
+}
+
+static gboolean
+shortcuts_selected (ShortcutsEditor *ui, GtkTreeIter *iter)
+{
+ GtkTreePath *path = ui->selected ? gtk_tree_row_reference_get_path (ui->selected) : NULL;
+ gboolean valid = path && gtk_tree_model_get_iter (GTK_TREE_MODEL (ui->store), iter, path);
+ if (path) gtk_tree_path_free (path);
+ return valid;
+}
+
+static void
+shortcuts_free (gpointer data)
+{
+ ShortcutsEditor *ui = data;
+ if (ui->selected) gtk_tree_row_reference_free (ui->selected);
+ g_object_unref (ui->store);
+ g_free (ui->actions);
+ g_free (ui);
+}
+
+static gboolean
+shortcuts_visible (GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
+{
+ ShortcutsEditor *ui = data;
+ if (ui->keep_selected && ui->selected)
+ {
+ GtkTreePath *a = gtk_tree_row_reference_get_path (ui->selected);
+ GtkTreePath *b = gtk_tree_model_get_path (model, iter);
+ gboolean same = a && !gtk_tree_path_compare (a, b);
+ if (a) gtk_tree_path_free (a);
+ gtk_tree_path_free (b);
+ if (same) return TRUE;
+ }
+ char *key, *action, *d1, *d2;
+ gtk_tree_model_get (model, iter, PREFERENCES_SHORTCUT_LABEL, &key, PREFERENCES_SHORTCUT_ACTION, &action,
+ PREFERENCES_SHORTCUT_DATA1, &d1, PREFERENCES_SHORTCUT_DATA2, &d2, -1);
+ const ShortcutPresentation *p = shortcut_presentation (action);
+ const char *category = gtk_combo_box_get_active_id (GTK_COMBO_BOX (ui->category));
+ gboolean visible = !category || !strcmp (category, "all") || !g_strcmp0 (category, p ? p->category : "Other");
+ char *all = g_strdup_printf ("%s %s %s %s %s", key ? key : "", action ? action : "", shortcut_title (action), d1 ? d1 : "", d2 ? d2 : "");
+ char *folded = g_utf8_casefold (all, -1);
+ char *query = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (ui->search)), -1);
+ char **words = g_strsplit_set (query, " \t", -1);
+ for (int i = 0; visible && words[i]; i++) if (*words[i] && !strstr (folded, words[i])) visible = FALSE;
+ g_free (key); g_free (action); g_free (d1); g_free (d2); g_free (all); g_free (folded); g_free (query); g_strfreev (words);
+ return visible;
+}
+
+static gboolean
+shortcuts_filtered (ShortcutsEditor *ui)
+{
+ const char *category = gtk_combo_box_get_active_id (GTK_COMBO_BOX (ui->category));
+ return *gtk_entry_get_text (GTK_ENTRY (ui->search)) || g_strcmp0 (category, "all");
+}
+
+static void
+shortcuts_status (ShortcutsEditor *ui)
+{
+ GtkTreeIter selected, iter;
+ guint key = 0, other_key, matches = 0;
+ GdkModifierType mods = 0, other_mods;
+ char *accel = NULL;
+ gboolean custom = FALSE, valid = shortcuts_selected (ui, &selected);
+ int total = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ui->store), NULL);
+ int visible = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ui->filter), NULL);
+ char *count = g_strdup_printf (_("%d of %d shortcuts"), visible, total);
+ gtk_label_set_text (GTK_LABEL (ui->count), count); g_free (count);
+ if (valid)
+ {
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &selected, PREFERENCES_SHORTCUT_ACCEL, &accel, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ gtk_accelerator_parse (accel ? accel : "", &key, &mods);
+ mods = ui->normalize (mods);
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (ui->store), &iter)) do
+ {
+ char *other;
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &iter, PREFERENCES_SHORTCUT_ACCEL, &other, -1);
+ gtk_accelerator_parse (other ? other : "", &other_key, &other_mods);
+ if (key && key == other_key && mods == ui->normalize (other_mods)) matches++;
+ g_free (other);
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (ui->store), &iter));
+ }
+ char *shared = matches > 1 ? g_strdup_printf (_("%u bindings share this key; checked in list order."), matches) :
+ g_strdup (valid && !key ? _("Assign a shortcut before saving this binding.") : "");
+ page_status (ui->shared, valid ? shared : _("Select a shortcut to see its settings."));
+ gtk_widget_set_sensitive (ui->remove, valid && custom);
+ gtk_widget_set_sensitive (ui->duplicate, valid);
+ gtk_widget_set_sensitive (ui->record, valid);
+ for (int delta = -1; delta <= 1; delta += 2)
+ {
+ gboolean can_move = FALSE;
+ if (valid && custom && !shortcuts_filtered (ui))
+ {
+ GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (ui->store), &selected);
+ if (delta > 0) gtk_tree_path_next (path);
+ if ((delta > 0 || gtk_tree_path_prev (path)) && gtk_tree_model_get_iter (GTK_TREE_MODEL (ui->store), &iter, path))
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &iter, PREFERENCES_SHORTCUT_CUSTOM, &can_move, -1);
+ gtk_tree_path_free (path);
+ }
+ gtk_widget_set_sensitive (delta < 0 ? ui->up : ui->down, can_move);
+ }
+ g_free (shared); g_free (accel);
+}
+
+static void
+shortcuts_refilter (ShortcutsEditor *ui)
+{
+ gtk_tree_model_filter_refilter (ui->filter);
+ shortcuts_status (ui);
+}
+
+static void
+shortcuts_query_changed (GtkWidget *widget, ShortcutsEditor *ui)
+{
+ if (ui->updating) return;
+ ui->keep_selected = FALSE;
+ shortcuts_refilter (ui);
+}
+
+static void
+shortcuts_selection_changed (GtkTreeSelection *selection, ShortcutsEditor *ui)
+{
+ if (ui->updating) return;
+ GtkTreeModel *model;
+ GtkTreeIter filtered, raw;
+ gboolean valid = gtk_tree_selection_get_selected (selection, &model, &filtered), custom = FALSE;
+ char *key = NULL, *accel = NULL, *action = NULL, *values[2] = {NULL, NULL};
+ if (ui->selected) gtk_tree_row_reference_free (ui->selected);
+ ui->selected = NULL;
+ if (valid)
+ {
+ gtk_tree_model_filter_convert_iter_to_child_iter (ui->filter, &raw, &filtered);
+ GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (ui->store), &raw);
+ ui->selected = gtk_tree_row_reference_new (GTK_TREE_MODEL (ui->store), path);
+ gtk_tree_path_free (path);
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &raw, PREFERENCES_SHORTCUT_LABEL, &key,
+ PREFERENCES_SHORTCUT_ACCEL, &accel, PREFERENCES_SHORTCUT_ACTION, &action,
+ PREFERENCES_SHORTCUT_DATA1, &values[0], PREFERENCES_SHORTCUT_DATA2, &values[1], PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ }
+ ui->updating = TRUE;
+ gtk_widget_set_sensitive (ui->editor, valid);
+ gtk_label_set_text (GTK_LABEL (ui->key_label), key && *key ? key : _("Unassigned"));
+ gtk_label_set_text (GTK_LABEL (ui->syntax), accel ? accel : "");
+ if (!action || !gtk_combo_box_set_active_id (GTK_COMBO_BOX (ui->action), action)) gtk_combo_box_set_active (GTK_COMBO_BOX (ui->action), -1);
+ gtk_widget_set_sensitive (ui->action, custom);
+ const ShortcutPresentation *p = shortcut_presentation (action);
+ for (int i = 0; i < 2; i++)
+ {
+ const char *label = p ? (i ? p->data2 : p->data1) : NULL;
+ GtkWidget *parent = label ? ui->fields : ui->advanced_fields;
+ if (gtk_widget_get_parent (ui->rows[i]) != parent)
+ {
+ g_object_ref (ui->rows[i]);
+ gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (ui->rows[i])), ui->rows[i]);
+ page_pack (parent, ui->rows[i]);
+ g_object_unref (ui->rows[i]);
+ }
+ gtk_label_set_text (GTK_LABEL (ui->labels[i]), label ? _(label) :
+ i ? _("Stored parameter 2 (not used by this action)") : _("Stored parameter 1 (not used by this action)"));
+ gtk_entry_set_text (GTK_ENTRY (ui->entries[i]), values[i] ? values[i] : "");
+ gtk_editable_set_editable (GTK_EDITABLE (ui->entries[i]), custom);
+ gtk_widget_set_sensitive (ui->entries[i], custom);
+ }
+ const char *help = "";
+ for (guint i = 0; i < ui->action_count; i++) if (!g_strcmp0 (action, ui->actions[i].id)) help = _(ui->actions[i].help);
+ GString *plain = g_string_new ("");
+ for (const unsigned char *c = (const unsigned char *)help; *c; c++) if (*c >= 32 || *c == '\n' || *c == '\t') g_string_append_c (plain, *c);
+ gtk_label_set_text (GTK_LABEL (ui->reference), plain->str);
+ g_string_free (plain, TRUE);
+ page_status (ui->policy, valid && !custom ? _("Built-in action. Duplicate to edit its action or parameters.") : "");
+ ui->updating = FALSE;
+ shortcuts_status (ui);
+ g_free (key); g_free (accel); g_free (action); g_free (values[0]); g_free (values[1]);
+}
+
+void
+preferences_shortcuts_reveal_path (GtkWidget *root, GtkTreePath *raw_path)
+{
+ ShortcutsEditor *ui = g_object_get_data (G_OBJECT (root), "shortcuts-editor");
+ gtk_entry_set_text (GTK_ENTRY (ui->search), "");
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (ui->category), "all");
+ ui->keep_selected = FALSE;
+ shortcuts_refilter (ui);
+ GtkTreePath *path = gtk_tree_model_filter_convert_child_path_to_path (ui->filter, raw_path);
+ if (path)
+ {
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (ui->view), path, NULL, FALSE);
+ gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ui->view), path, NULL, FALSE, 0, 0);
+ gtk_tree_path_free (path);
+ }
+}
+
+static void
+shortcuts_edit_changed (GtkWidget *widget, ShortcutsEditor *ui)
+{
+ GtkTreeIter iter;
+ gboolean custom;
+ if (ui->updating || !shortcuts_selected (ui, &iter)) return;
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &iter, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ if (!custom) return;
+ ui->keep_selected = TRUE;
+ ui->updating = TRUE;
+ if (widget == ui->action)
+ {
+ const char *id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (ui->action));
+ if (id) gtk_list_store_set (ui->store, &iter, PREFERENCES_SHORTCUT_ACTION, id, -1);
+ }
+ else for (int i = 0; i < 2; i++) if (widget == ui->entries[i])
+ gtk_list_store_set (ui->store, &iter, i ? PREFERENCES_SHORTCUT_DATA2 : PREFERENCES_SHORTCUT_DATA1,
+ gtk_entry_get_text (GTK_ENTRY (widget)), -1);
+ ui->updating = FALSE;
+ shortcuts_refilter (ui);
+ if (widget == ui->action) shortcuts_selection_changed (gtk_tree_view_get_selection (GTK_TREE_VIEW (ui->view)), ui);
+}
+
+void
+preferences_shortcuts_find_action (GtkWidget *root, const char *action_id)
+{
+ ShortcutsEditor *ui = g_object_get_data (G_OBJECT (root), "shortcuts-editor");
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (ui->category), "all");
+ gtk_entry_set_text (GTK_ENTRY (ui->search), action_id);
+ ui->keep_selected = FALSE;
+ shortcuts_refilter (ui);
+ if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (ui->filter), NULL))
+ {
+ GtkTreePath *path = gtk_tree_path_new_first ();
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (ui->view), path, NULL, FALSE);
+ gtk_tree_path_free (path);
+ }
+}
+
+static void
+shortcuts_model_changed (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, GtkWidget *root)
+{
+ ShortcutsEditor *ui = g_object_get_data (G_OBJECT (root), "shortcuts-editor");
+ if (!ui->updating) shortcuts_refilter (ui);
+}
+
+static void
+shortcuts_add (GtkButton *button, ShortcutsEditor *ui)
+{
+ GtkTreeIter source, added;
+ char *values[5] = {NULL};
+ gboolean duplicate = GTK_WIDGET (button) == ui->duplicate && shortcuts_selected (ui, &source);
+ if (duplicate) gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &source, 0, &values[0], 1, &values[1], 2, &values[2], 3, &values[3], 4, &values[4], -1);
+ ui->updating = TRUE;
+ gtk_list_store_append (ui->store, &added);
+ gtk_list_store_set (ui->store, &added, PREFERENCES_SHORTCUT_LABEL, duplicate ? values[0] : _("Unassigned"),
+ PREFERENCES_SHORTCUT_ACCEL, values[1], PREFERENCES_SHORTCUT_ACTION, duplicate ? values[2] : ui->actions[0].id,
+ PREFERENCES_SHORTCUT_DATA1, values[3], PREFERENCES_SHORTCUT_DATA2, values[4], PREFERENCES_SHORTCUT_CUSTOM, TRUE, -1);
+ ui->updating = FALSE;
+ GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (ui->store), &added);
+ preferences_shortcuts_reveal_path (ui->root, path);
+ gtk_tree_path_free (path);
+ for (int i = 0; i < 5; i++) g_free (values[i]);
+}
+
+static void
+shortcuts_remove (GtkButton *button, ShortcutsEditor *ui)
+{
+ GtkTreeIter iter;
+ gboolean custom;
+ if (!shortcuts_selected (ui, &iter)) return;
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &iter, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ if (!custom) return;
+ ui->updating = TRUE;
+ gtk_list_store_remove (ui->store, &iter);
+ if (ui->selected) gtk_tree_row_reference_free (ui->selected);
+ ui->selected = NULL;
+ ui->updating = FALSE;
+ shortcuts_refilter (ui);
+ shortcuts_selection_changed (gtk_tree_view_get_selection (GTK_TREE_VIEW (ui->view)), ui);
+}
+
+static void
+shortcuts_move (GtkButton *button, ShortcutsEditor *ui)
+{
+ GtkTreeIter iter, neighbor;
+ gboolean custom;
+ if (shortcuts_filtered (ui) || !shortcuts_selected (ui, &iter)) return;
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &iter, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ if (!custom) return;
+ GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (ui->store), &iter);
+ if (GTK_WIDGET (button) == ui->down) gtk_tree_path_next (path);
+ else if (!gtk_tree_path_prev (path)) { gtk_tree_path_free (path); return; }
+ if (gtk_tree_model_get_iter (GTK_TREE_MODEL (ui->store), &neighbor, path))
+ {
+ gtk_tree_model_get (GTK_TREE_MODEL (ui->store), &neighbor, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ if (custom) gtk_list_store_swap (ui->store, &iter, &neighbor);
+ }
+ gtk_tree_path_free (path);
+ shortcuts_status (ui);
+}
+
+static void
+shortcuts_restore (GtkButton *button, ShortcutsEditor *ui)
+{
+ ui->updating = TRUE;
+ ui->restore (ui->restore_data);
+ if (ui->selected) gtk_tree_row_reference_free (ui->selected);
+ ui->selected = NULL;
+ ui->updating = FALSE;
+ GtkTreePath *path = gtk_tree_path_new_first ();
+ preferences_shortcuts_reveal_path (ui->root, path);
+ gtk_tree_path_free (path);
+}
+
+static gboolean
+shortcuts_tree_keypress (GtkWidget *widget, GdkEventKey *event, ShortcutsEditor *ui)
+{
+ if (!(event->state & GDK_SHIFT_MASK) || (event->keyval != GDK_KEY_Up && event->keyval != GDK_KEY_Down)) return FALSE;
+ GtkWidget *button = event->keyval == GDK_KEY_Up ? ui->up : ui->down;
+ if (gtk_widget_get_sensitive (button)) shortcuts_move (GTK_BUTTON (button), ui);
+ return TRUE;
+}
+
+typedef struct { GWeakRef owner; GtkTreeRowReference *row; GtkWidget *view; } ShortcutRecorder;
+
+static void
+shortcut_recorder_free (gpointer data)
+{
+ ShortcutRecorder *rec = data;
+ g_weak_ref_clear (&rec->owner);
+ gtk_tree_row_reference_free (rec->row);
+ g_free (rec);
+}
+
+static void
+shortcut_recorded (GtkCellRendererAccel *renderer, char *path, guint key, GdkModifierType mods, guint code, GtkWidget *dialog)
+{
+ ShortcutRecorder *rec = g_object_get_data (G_OBJECT (dialog), "shortcut-recorder");
+ GtkWidget *root = g_weak_ref_get (&rec->owner);
+ if (root && key)
+ {
+ ShortcutsEditor *ui = g_object_get_data (G_OBJECT (root), "shortcuts-editor");
+ GtkTreePath *raw_path = gtk_tree_row_reference_get_path (rec->row);
+ GtkTreeIter iter;
+ if (raw_path && gtk_tree_model_get_iter (GTK_TREE_MODEL (ui->store), &iter, raw_path))
+ {
+ mods = ui->normalize (mods);
+ if (key == GDK_KEY_Tab && (mods & GDK_SHIFT_MASK)) key = GDK_KEY_ISO_Left_Tab;
+ char *label = gtk_accelerator_get_label (key, mods), *accel = gtk_accelerator_name (key, mods);
+ ui->keep_selected = TRUE;
+ gtk_list_store_set (ui->store, &iter, PREFERENCES_SHORTCUT_LABEL, label, PREFERENCES_SHORTCUT_ACCEL, accel, -1);
+ shortcuts_selection_changed (gtk_tree_view_get_selection (GTK_TREE_VIEW (ui->view)), ui);
+ g_free (label); g_free (accel);
+ }
+ if (raw_path) gtk_tree_path_free (raw_path);
+ }
+ if (root) g_object_unref (root);
+ gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
+}
+
+static void
+shortcut_record_map (GtkWidget *dialog, ShortcutRecorder *rec)
+{
+ GtkTreePath *path = gtk_tree_path_new_first ();
+ gtk_widget_grab_focus (rec->view);
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (rec->view), path, gtk_tree_view_get_column (GTK_TREE_VIEW (rec->view), 0), TRUE);
+ gtk_tree_path_free (path);
+}
+
+static void
+shortcut_record_cancelled (GtkCellRenderer *renderer, GtkWidget *dialog)
+{
+ if (!g_object_get_data (G_OBJECT (dialog), "recorder-closing"))
+ gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
+}
+
+static void
+shortcut_record_cleared (GtkCellRendererAccel *renderer, char *path, GtkWidget *dialog)
+{
+ shortcut_record_cancelled (GTK_CELL_RENDERER (renderer), dialog);
+}
+
+static void
+shortcut_record_response (GtkDialog *dialog, int response, gpointer unused)
+{
+ if (g_object_get_data (G_OBJECT (dialog), "recorder-closing")) return;
+ g_object_set_data (G_OBJECT (dialog), "recorder-closing", GINT_TO_POINTER (1));
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+shortcuts_record (GtkButton *button, ShortcutsEditor *ui)
+{
+ GtkTreeIter iter;
+ if (!shortcuts_selected (ui, &iter)) return;
+ GtkWidget *top = gtk_widget_get_toplevel (ui->root);
+ GtkWidget *dialog = gtk_dialog_new_with_buttons (_("Press a shortcut"), GTK_IS_WINDOW (top) ? GTK_WINDOW (top) : NULL,
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("Cancel"), GTK_RESPONSE_CANCEL, NULL);
+ gtk_widget_set_name (dialog, "shortcut-recorder");
+ ShortcutRecorder *rec = g_new0 (ShortcutRecorder, 1);
+ g_weak_ref_init (&rec->owner, ui->root);
+ GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (ui->store), &iter);
+ rec->row = gtk_tree_row_reference_new (GTK_TREE_MODEL (ui->store), path);
+ gtk_tree_path_free (path);
+ GtkWidget *box = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ gtk_container_set_border_width (GTK_CONTAINER (box), 18);
+ gtk_box_set_spacing (GTK_BOX (box), 12);
+ page_pack (box, page_label (_("Press a key combination now. Esc cancels. Nothing is executed."), FALSE));
+ GtkListStore *store = gtk_list_store_new (1, G_TYPE_STRING);
+ GtkTreeIter sample;
+ gtk_list_store_append (store, &sample);
+ gtk_list_store_set (store, &sample, 0, _("Press a shortcut…"), -1);
+ rec->view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
+ g_object_unref (store);
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (rec->view), FALSE);
+ GtkCellRenderer *renderer = gtk_cell_renderer_accel_new ();
+ g_object_set (renderer, "editable", TRUE,
+#ifndef WIN32
+ "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER,
+#endif
+ NULL);
+ GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes (_("Shortcut"), renderer, "text", 0, NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (rec->view), column);
+ page_pack (box, rec->view);
+ g_object_set_data_full (G_OBJECT (dialog), "shortcut-recorder", rec, shortcut_recorder_free);
+ g_signal_connect_object (renderer, "accel-edited", G_CALLBACK (shortcut_recorded), dialog, 0);
+ g_signal_connect_object (renderer, "editing-canceled", G_CALLBACK (shortcut_record_cancelled), dialog, 0);
+ g_signal_connect_object (renderer, "accel-cleared", G_CALLBACK (shortcut_record_cleared), dialog, 0);
+ g_signal_connect (dialog, "map", G_CALLBACK (shortcut_record_map), rec);
+ g_signal_connect (dialog, "response", G_CALLBACK (shortcut_record_response), NULL);
+ gtk_widget_show_all (dialog);
+}
+
+static void
+shortcuts_cell (GtkTreeViewColumn *column, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
+{
+ int field = GPOINTER_TO_INT (data);
+ char *action, *d1, *d2;
+ gboolean custom;
+ gtk_tree_model_get (model, iter, PREFERENCES_SHORTCUT_ACTION, &action, PREFERENCES_SHORTCUT_DATA1, &d1, PREFERENCES_SHORTCUT_DATA2, &d2, PREFERENCES_SHORTCUT_CUSTOM, &custom, -1);
+ char *text = field == 0 ? g_strdup (shortcut_title (action)) : field == 1 ?
+ g_strescape (d1 && *d1 ? d1 : d2 && *d2 ? d2 : "", NULL) : g_strdup (custom ? _("Custom") : _("Built-in"));
+ g_object_set (renderer, "text", text, NULL);
+ g_free (text); g_free (action); g_free (d1); g_free (d2);
+}
+
+static GtkWidget *
+shortcuts_button (GtkWidget *row, const char *name, const char *label, GCallback callback, ShortcutsEditor *ui)
+{
+ GtkWidget *button = gtk_button_new_with_label (label);
+ gtk_widget_set_name (button, name);
+ g_signal_connect (button, "clicked", callback, ui);
+ page_pack (row, button);
+ return button;
+}
+
+GtkWidget *
+preferences_shortcuts_page_new (GtkListStore *store, const PreferencesShortcutAction *actions, guint count,
+ PreferencesShortcutModifiers normalize, PreferencesShortcutsRestore restore, gpointer data)
+{
+ ShortcutsEditor *ui = g_new0 (ShortcutsEditor, 1);
+ GtkWidget *box, *row, *scroll, *reference;
+ ui->root = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ ui->store = g_object_ref (store);
+ ui->actions = g_new (PreferencesShortcutAction, count);
+ memcpy (ui->actions, actions, count * sizeof *actions);
+ ui->action_count = count; ui->normalize = normalize; ui->restore = restore; ui->restore_data = data;
+ g_object_set_data_full (G_OBJECT (ui->root), "shortcuts-editor", ui, shortcuts_free);
+ g_object_set_data (G_OBJECT (ui->root), "bindings-store", store);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ ui->search = gtk_search_entry_new ();
+ gtk_widget_set_name (ui->search, "shortcut-search");
+ gtk_entry_set_placeholder_text (GTK_ENTRY (ui->search), _("Search keys, actions or commands"));
+ gtk_box_pack_start (GTK_BOX (row), ui->search, TRUE, TRUE, 0);
+ ui->category = gtk_combo_box_text_new ();
+ gtk_widget_set_name (ui->category, "shortcut-category");
+ const char *ids[] = {"all", "Navigation", "Typing", "Commands", "Application", "Other"};
+ const char *titles[] = {N_("All actions"), N_("Navigation"), N_("Typing"), N_("Commands"), N_("Application"), N_("Other")};
+ for (guint i = 0; i < G_N_ELEMENTS (ids); i++) gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (ui->category), ids[i], _(titles[i]));
+ gtk_combo_box_set_active (GTK_COMBO_BOX (ui->category), 0);
+ page_pack (row, ui->category); page_pack (ui->root, row);
+ ui->count = page_label ("", TRUE); page_pack (ui->root, ui->count);
+ ui->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (GTK_TREE_MODEL (store), NULL));
+ gtk_tree_model_filter_set_visible_func (ui->filter, shortcuts_visible, ui, NULL);
+ ui->view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (ui->filter));
+ g_object_unref (ui->filter);
+ gtk_widget_set_name (ui->view, "shortcuts-list");
+ gtk_tree_view_set_enable_search (GTK_TREE_VIEW (ui->view), FALSE);
+ g_object_set_data (G_OBJECT (ui->root), "view", ui->view);
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+ gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (ui->view), -1, _("Shortcut"), renderer, "text", PREFERENCES_SHORTCUT_LABEL, NULL);
+ const char *columns[] = {N_("Action"), N_("Parameters"), N_("Type")};
+ for (int i = 0; i < 3; i++)
+ {
+ renderer = gtk_cell_renderer_text_new ();
+ g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+ GtkTreeViewColumn *column = gtk_tree_view_column_new ();
+ gtk_tree_view_column_set_title (column, _(columns[i]));
+ gtk_tree_view_column_pack_start (column, renderer, TRUE);
+ gtk_tree_view_column_set_cell_data_func (column, renderer, shortcuts_cell, GINT_TO_POINTER (i), NULL);
+ gtk_tree_view_column_set_expand (column, i == 0);
+ gtk_tree_view_column_set_min_width (column, i == 0 ? 170 : 65);
+ if (i == 1) gtk_tree_view_column_set_max_width (column, 170);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (ui->view), column);
+ }
+ 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), 190);
+ gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
+ gtk_container_add (GTK_CONTAINER (scroll), ui->view); page_pack (ui->root, scroll);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+ shortcuts_button (row, "shortcut-add", _("Add"), G_CALLBACK (shortcuts_add), ui);
+ ui->duplicate = shortcuts_button (row, "shortcut-duplicate", _("Duplicate"), G_CALLBACK (shortcuts_add), ui);
+ ui->remove = shortcuts_button (row, "shortcut-remove", _("Remove"), G_CALLBACK (shortcuts_remove), ui);
+ ui->up = shortcuts_button (row, "shortcut-up", _("Move up"), G_CALLBACK (shortcuts_move), ui);
+ ui->down = shortcuts_button (row, "shortcut-down", _("Move down"), G_CALLBACK (shortcuts_move), ui);
+ gtk_widget_set_tooltip_text (ui->up, _("Clear the search and choose All actions to reorder adjacent custom bindings."));
+ gtk_widget_set_tooltip_text (ui->down, _("Clear the search and choose All actions to reorder adjacent custom bindings."));
+ g_object_set_data (G_OBJECT (ui->root), "delete_button", ui->remove);
+ page_pack (ui->root, row);
+ box = page_section (ui->root, _("Selected shortcut"), NULL);
+ ui->editor = box;
+ ui->policy = page_label ("", TRUE); page_pack (box, ui->policy);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ ui->key_label = page_label ("", FALSE); gtk_widget_set_name (ui->key_label, "shortcut-key");
+ gtk_box_pack_start (GTK_BOX (row), ui->key_label, TRUE, TRUE, 0);
+ ui->record = shortcuts_button (row, "shortcut-record", _("Change shortcut…"), G_CALLBACK (shortcuts_record), ui);
+ gtk_widget_set_halign (ui->record, GTK_ALIGN_START);
+ page_pack (box, row);
+ ui->action = gtk_combo_box_text_new ();
+ gtk_widget_set_name (ui->action, "shortcut-action");
+ for (guint i = 0; i < count; i++) gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (ui->action), actions[i].id, shortcut_title (actions[i].id));
+ page_row (box, _("Action"), ui->action);
+ ui->fields = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); page_pack (box, ui->fields);
+ ui->advanced = gtk_expander_new (_("Advanced"));
+ ui->advanced_fields = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+ gtk_container_add (GTK_CONTAINER (ui->advanced), ui->advanced_fields);
+ for (int i = 0; i < 2; i++)
+ {
+ ui->rows[i] = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ ui->labels[i] = page_label ("", FALSE); page_pack (ui->rows[i], ui->labels[i]);
+ ui->entries[i] = gtk_entry_new ();
+ gtk_widget_set_name (ui->entries[i], i ? "shortcut-data2" : "shortcut-data1");
+ gtk_label_set_mnemonic_widget (GTK_LABEL (ui->labels[i]), ui->entries[i]);
+ page_pack (ui->rows[i], ui->entries[i]); page_pack (ui->fields, ui->rows[i]);
+ g_signal_connect (ui->entries[i], "changed", G_CALLBACK (shortcuts_edit_changed), ui);
+ }
+ ui->syntax = page_label ("", TRUE); gtk_label_set_selectable (GTK_LABEL (ui->syntax), TRUE);
+ page_pack (ui->advanced_fields, ui->syntax); page_pack (box, ui->advanced);
+ ui->shared = page_label ("", FALSE); gtk_widget_set_name (ui->shared, "shortcut-shared"); page_pack (box, ui->shared);
+ reference = gtk_expander_new (_("Action reference"));
+ ui->reference = page_label ("", TRUE); gtk_label_set_selectable (GTK_LABEL (ui->reference), TRUE);
+ gtk_container_add (GTK_CONTAINER (reference), ui->reference); page_pack (box, reference);
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ GtkWidget *restore_button = shortcuts_button (row, "shortcut-restore", _("Restore built-in shortcuts"), G_CALLBACK (shortcuts_restore), ui);
+ gtk_widget_set_tooltip_text (restore_button, _("Reset built-in keys and keep custom bindings. Applied when you save."));
+ gtk_widget_set_tooltip_text (ui->shared, _("Repeated keys form an action chain in saved order. An action can stop the chain."));
+ page_pack (ui->root, row);
+ g_signal_connect (ui->search, "search-changed", G_CALLBACK (shortcuts_query_changed), ui);
+ g_signal_connect (ui->category, "changed", G_CALLBACK (shortcuts_query_changed), ui);
+ g_signal_connect (ui->action, "changed", G_CALLBACK (shortcuts_edit_changed), ui);
+ g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (ui->view)), "changed", G_CALLBACK (shortcuts_selection_changed), ui);
+ g_signal_connect (ui->view, "key-press-event", G_CALLBACK (shortcuts_tree_keypress), ui);
+ g_signal_connect_object (store, "row-changed", G_CALLBACK (shortcuts_model_changed), ui->root, 0);
+ GtkTreePath *first = gtk_tree_path_new_first ();
+ preferences_shortcuts_reveal_path (ui->root, first); gtk_tree_path_free (first);
+ shortcuts_selection_changed (gtk_tree_view_get_selection (GTK_TREE_VIEW (ui->view)), ui);
+ gtk_widget_show_all (ui->root);
+ return ui->root;
+}
+
+static void
+page_refresh (PreferencesPage *page)
+{
+ if (page->refreshing) return;
+ if (page->kind == PAGE_TEXT) page_text_preview (page);
+ else if (page->kind == PAGE_HISTORY) page_history_refresh (page);
+ else if (page->kind == PAGE_TYPING) typing_refresh (page);
+ else if (page->kind == PAGE_TABS)
+ {
+ gboolean tree = page->prefs->hex_gui_tab_layout != 0;
+ gtk_widget_set_visible (page->tree_options, tree);
+ gtk_widget_set_visible (page->tab_options, !tree);
+ for (int i = 0; i < 2; i++) gtk_widget_queue_draw (page->layout_drawings[i]);
+ }
+ else
+ {
+ if (page->tray_options) gtk_widget_set_sensitive (page->tray_options, page->prefs->hex_gui_tray);
+ if (page->quiet_hidden) gtk_widget_set_sensitive (page->quiet_hidden, page->prefs->hex_gui_tray);
+ for (int i = 0; i < 3; i++)
+ if (page->tray_alerts[i]) gtk_widget_set_sensitive (page->tray_alerts[i], page->prefs->hex_gui_tray);
+
+ }
+}
diff --git a/src/fe-gtk/preferences-pages.h b/src/fe-gtk/preferences-pages.h
new file mode 100644
index 00000000..3269e927
--- /dev/null
+++ b/src/fe-gtk/preferences-pages.h
@@ -0,0 +1,62 @@
+/* ZoiteChat - native editors for the everyday preferences pages.
+ * Licensed under the GNU General Public License, version 2 or later.
+ */
+#ifndef ZOITECHAT_PREFERENCES_PAGES_H
+#define ZOITECHAT_PREFERENCES_PAGES_H
+
+#include
+
+struct zoitechatprefs;
+
+typedef enum
+{
+ PREFERENCES_ALERT_DESKTOP = 1 << 0,
+ PREFERENCES_ALERT_TRAY = 1 << 1,
+ PREFERENCES_ALERT_ATTENTION = 1 << 2,
+ PREFERENCES_ALERT_TRAY_BEHAVIOR = 1 << 3,
+ PREFERENCES_ALERT_HIDDEN_ONLY = 1 << 4
+} PreferencesAlertCapabilities;
+
+typedef void (*PreferencesSoundChanged) (gpointer user_data);
+
+/* All editors write only to the window's staged preferences. The caller owns
+ * both the preferences and the staged Beep event's allocated filename. */
+GtkWidget *preferences_text_page_new (struct zoitechatprefs *staged,
+ const char *const *languages);
+GtkWidget *preferences_tabs_page_new (struct zoitechatprefs *staged);
+GtkWidget *preferences_notifications_page_new (struct zoitechatprefs *staged,
+ PreferencesAlertCapabilities capabilities, char **beep_file,
+ PreferencesSoundChanged sound_changed, gpointer user_data);
+void preferences_notifications_refresh_sound (GtkWidget *page);
+GtkWidget *preferences_history_page_new (struct zoitechatprefs *staged,
+ const char *config_dir);
+gboolean preferences_history_page_stage (GtkWidget *page, GError **error);
+
+typedef struct
+{
+ const char *code;
+ const char *name;
+} PreferencesSpellLanguage;
+typedef void (*PreferencesTypingPreview) (GtkWidget *entry, const struct zoitechatprefs *staged);
+GtkWidget *preferences_typing_page_new (struct zoitechatprefs *staged, GtkWidget *preview_entry,
+ const PreferencesSpellLanguage *languages, guint count, PreferencesTypingPreview update_preview);
+gboolean preferences_typing_page_stage (GtkWidget *page, GError **error);
+
+/* These columns retain the existing keybindings.conf editor's model and order. */
+enum
+{
+ PREFERENCES_SHORTCUT_LABEL, PREFERENCES_SHORTCUT_ACCEL,
+ PREFERENCES_SHORTCUT_ACTION, PREFERENCES_SHORTCUT_DATA1,
+ PREFERENCES_SHORTCUT_DATA2, PREFERENCES_SHORTCUT_CUSTOM,
+ PREFERENCES_SHORTCUT_N_COLUMNS
+};
+typedef struct { const char *id; const char *help; } PreferencesShortcutAction;
+typedef GdkModifierType (*PreferencesShortcutModifiers) (GdkModifierType modifiers);
+typedef void (*PreferencesShortcutsRestore) (gpointer data);
+GtkWidget *preferences_shortcuts_page_new (GtkListStore *store,
+ const PreferencesShortcutAction *actions, guint count,
+ PreferencesShortcutModifiers normalize, PreferencesShortcutsRestore restore, gpointer data);
+void preferences_shortcuts_reveal_path (GtkWidget *page, GtkTreePath *raw_path);
+void preferences_shortcuts_find_action (GtkWidget *page, const char *action_id);
+
+#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..c40ee50b 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,16 @@
#include "pixmaps.h"
#include "menu.h"
#include "preferences-persistence.h"
+#include "preferences-pages.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 +60,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 +103,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 +370,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 +381,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 +633,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 +848,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 +861,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 +874,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 +932,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 +948,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 +958,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 +968,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 +1070,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 +1338,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 +1516,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 +1535,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 +1580,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 +1595,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_widget_set_margin_start (tab, 12);
+ gtk_grid_set_row_spacing (GTK_GRID (tab), 8);
+ gtk_grid_set_column_spacing (GTK_GRID (tab), 12);
return tab;
}
@@ -1591,20 +1612,36 @@ 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, 20);
+ 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);
+ {
+ GtkWidget *heading = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ GtkWidget *line = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ 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_halign (label, GTK_ALIGN_START);
+ frame = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ gtk_box_pack_start (GTK_BOX (heading), label, FALSE, FALSE, 0);
+ gtk_widget_set_valign (line, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (heading), line, TRUE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (frame), heading, FALSE, FALSE, 0);
+ tab = setup_create_frame ();
+ gtk_box_pack_start (GTK_BOX (frame), tab, FALSE, FALSE, 0);
+ 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 +1649,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 +1661,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 +1699,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,48 +1710,77 @@ 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;
+static GtkWidget *setup_sound_tree;
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_sounds_ensure_created (void)
+{
+ if (setup_sound_created) return;
+ for (int 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;
+}
+
+static void
+setup_alert_sound_refresh (void)
+{
+ for (guint i = 0; setup_pages && i < setup_pages->len; i++)
+ {
+ setup_page *page = &g_array_index (setup_pages, setup_page, i);
+ if (!strcmp (page->id, "notifications") && page->body)
+ preferences_notifications_refresh_sound (page->body);
+ }
+}
+
+/* Notifications and Sounds edit the same staged Beep event, even when both
+ * pages have already been created. Neither page may reinitialize the snapshot. */
+static void
+setup_alert_sound_changed (gpointer unused)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ GtkTreeSelection *selection;
+ int event;
+ if (!setup_sound_tree) return;
+ model = gtk_tree_view_get_model (GTK_TREE_VIEW (setup_sound_tree));
+ if (!gtk_tree_model_get_iter_first (model, &iter)) return;
+ do
+ {
+ gtk_tree_model_get (model, &iter, 2, &event, -1);
+ if (event == XP_TE_BEEP)
+ {
+ gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1,
+ setup_sound_files[XP_TE_BEEP] ? setup_sound_files[XP_TE_BEEP] : "", -1);
+ break;
+ }
+ } while (gtk_tree_model_iter_next (model, &iter));
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (setup_sound_tree));
+ if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+ {
+ gtk_tree_model_get (model, &iter, 2, &event, -1);
+ if (event == XP_TE_BEEP)
+ {
+ ignore_changed = TRUE;
+ gtk_entry_set_text (GTK_ENTRY (sndfile_entry),
+ setup_sound_files[XP_TE_BEEP] ? setup_sound_files[XP_TE_BEEP] : "");
+ ignore_changed = FALSE;
+ }
+ }
+}
static void
setup_snd_populate (GtkTreeView * treeview)
@@ -1734,8 +1797,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 +1840,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 +1871,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 +1909,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 +1938,13 @@ 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_widget_set_sensitive (cancel_button, FALSE);
+ gtk_list_store_set (store, &iter, 1, setup_sound_files[n], -1);
+ if (n == XP_TE_BEEP) setup_alert_sound_refresh ();
}
static GtkWidget *
@@ -1894,6 +1960,7 @@ setup_create_sound_page (void)
GtkWidget *sound_play;
GtkTreeSelection *sel;
+ setup_sounds_ensure_created ();
vbox1 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_container_set_border_width (GTK_CONTAINER (vbox1), 6);
gtk_widget_show (vbox1);
@@ -1911,6 +1978,7 @@ setup_create_sound_page (void)
GTK_SHADOW_IN);
sound_tree = gtk_tree_view_new ();
+ setup_sound_tree = sound_tree;
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (sound_tree));
gtk_tree_selection_set_mode (sel, GTK_SELECTION_SINGLE);
setup_snd_add_columns (GTK_TREE_VIEW (sound_tree));
@@ -1965,10 +2033,128 @@ setup_page_from_settings (const setting *settings)
}
static GtkWidget *
-setup_page_from_appearance (const setting *settings)
+setup_page_from_text (const setting *settings)
+{
+ (void) settings;
+ return preferences_text_page_new (&setup_prefs,
+#ifdef WIN32
+ langsmenu
+#else
+ NULL
+#endif
+ );
+}
+
+static GtkWidget *
+setup_page_from_tabs (const setting *settings)
+{
+ (void) settings;
+ return preferences_tabs_page_new (&setup_prefs);
+}
+
+static GtkWidget *
+setup_page_from_history (const setting *settings)
+{
+ (void) settings;
+ return preferences_history_page_new (&setup_prefs, get_xdir ());
+}
+
+static void
+setup_typing_preview_update (GtkWidget *widget, const struct zoitechatprefs *staged)
+{
+ SexySpellEntry *entry = SEXY_SPELL_ENTRY (widget);
+ const char *previous = g_object_get_data (G_OBJECT (widget), "preview-spelling-languages");
+ if (g_strcmp0 (previous, staged->hex_text_spell_langs))
+ {
+ GSList *available = sexy_spell_entry_get_languages (entry);
+ char **tokens = g_strsplit_set (staged->hex_text_spell_langs, ", \t", -1);
+ gboolean loaded = FALSE;
+ sexy_spell_entry_deactivate_language (entry, NULL);
+ for (int i = 0; tokens[i]; i++)
+ for (GSList *item = available; item; item = item->next)
+ if (!strcmp (item->data, tokens[i]))
+ loaded |= sexy_spell_entry_activate_language (entry, tokens[i], NULL);
+ if (!loaded)
+ sexy_spell_entry_activate_language (entry, "en", NULL);
+ g_slist_free_full (available, g_free);
+ g_strfreev (tokens);
+ g_object_set_data_full (G_OBJECT (widget), "preview-spelling-languages", g_strdup (staged->hex_text_spell_langs), g_free);
+ }
+ sexy_spell_entry_set_checked (entry, staged->hex_gui_input_spell);
+ sexy_spell_entry_set_parse_attributes (entry, staged->hex_gui_input_attr);
+}
+
+static gint
+setup_spell_language_compare (gconstpointer a, gconstpointer b)
+{
+ const PreferencesSpellLanguage *left = a, *right = b;
+ return g_utf8_collate (left->name, right->name);
+}
+
+static GtkWidget *
+setup_page_from_typing (const setting *settings)
+{
+ (void) settings;
+ GtkWidget *preview = sexy_spell_entry_new ();
+ GSList *available = sexy_spell_entry_get_languages (SEXY_SPELL_ENTRY (preview));
+ GArray *languages = g_array_new (FALSE, FALSE, sizeof (PreferencesSpellLanguage));
+ GHashTable *seen = g_hash_table_new (g_str_hash, g_str_equal);
+ for (GSList *item = available; item; item = item->next)
+ {
+ if (g_hash_table_contains (seen, item->data)) continue;
+ PreferencesSpellLanguage language = {g_strdup (item->data),
+ sexy_spell_entry_get_language_name (SEXY_SPELL_ENTRY (preview), item->data)};
+ if (!language.name || !*language.name)
+ {
+ g_free ((char *)language.name);
+ language.name = g_strdup (item->data);
+ }
+ g_hash_table_add (seen, (gpointer)language.code);
+ g_array_append_val (languages, language);
+ }
+ g_array_sort (languages, setup_spell_language_compare);
+ GtkWidget *page = preferences_typing_page_new (&setup_prefs, preview,
+ (PreferencesSpellLanguage *)languages->data, languages->len, setup_typing_preview_update);
+ g_hash_table_destroy (seen);
+ for (guint i = 0; i < languages->len; i++)
+ {
+ PreferencesSpellLanguage *language = &g_array_index (languages, PreferencesSpellLanguage, i);
+ g_free ((char *)language->code); g_free ((char *)language->name);
+ }
+ g_array_unref (languages);
+ g_slist_free_full (available, g_free);
+ return page;
+}
+
+static GtkWidget *
+setup_page_from_notifications (const setting *settings)
+{
+ PreferencesAlertCapabilities capabilities = 0;
+ /* Keep the established platform/backend capability decisions and the
+ * legacy descriptors for search aliases, without duplicating policy. */
+ for (int i = 0; settings[i].type != ST_END; i++)
+ {
+ if (settings[i].type == ST_3OGGLE)
+ {
+ if (settings[i].list == (void *)balloonlist) capabilities |= PREFERENCES_ALERT_DESKTOP;
+ if (settings[i].list == (void *)trayblinklist) capabilities |= PREFERENCES_ALERT_TRAY;
+ if (settings[i].list == (void *)taskbarlist) capabilities |= PREFERENCES_ALERT_ATTENTION;
+ }
+ if (settings[i].type == ST_TOGGLE && settings[i].offset == P_OFFINTNL(hex_gui_tray))
+ capabilities |= PREFERENCES_ALERT_TRAY_BEHAVIOR;
+ if (settings[i].type == ST_TOGGLE && settings[i].offset == P_OFFINTNL(hex_gui_tray_quiet))
+ capabilities |= PREFERENCES_ALERT_HIDDEN_ONLY;
+ }
+ setup_sounds_ensure_created ();
+ return preferences_notifications_page_new (&setup_prefs, capabilities,
+ &setup_sound_files[XP_TE_BEEP], setup_alert_sound_changed, NULL);
+}
+
+static GtkWidget *
+setup_page_from_theme (const setting *settings)
{
(void)settings;
- return setup_create_appearance_page ();
+ return setup_create_theme_page ();
}
static GtkWidget *
@@ -1979,271 +2165,485 @@ 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, *heading, *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, 12);
+ gtk_container_set_border_width (GTK_CONTAINER (box), 16);
+ heading = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_pack_start (GTK_BOX (box), heading, FALSE, FALSE, 0);
+ 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_widget_set_tooltip_text (label, _(description));
+ gtk_box_pack_start (GTK_BOX (heading), 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), 64);
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ gtk_box_pack_start (GTK_BOX (heading), label, FALSE, FALSE, 0);
+ page.container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ 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_text, appearance_settings, N_("Conversation preview timestamp time style 24-hour 12-hour custom format unread nickname alignment"));
+ setup_register_page ("appearance", N_("Interface"), N_("Appearance"),
+ N_("Application theme, chat colours and message formatting."),
+ 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_typing, inputbox_settings, N_("Language dictionaries installed unavailable codes search typing preview completion example suffix threshold matching names"));
+ 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_tabs, tabs_settings, N_("Sidebar tree navigation placement preview opening closing conversations"));
- 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_("Keyboard shortcuts and the actions they run."),
+ setup_page_from_keyboard, NULL, N_("Key accelerator action data reset add delete binding shortcuts search category record duplicate shared chain order parameters"));
+ 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_notifications, alerts, N_("Mentions quiet rules test notification alert system sound beep file"));
+ 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_history, logging_settings, N_("Conversation history buffer restore saved logs folder location filename format daily monthly date timestamp preview collected links URL grabber unlimited"));
+ 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_EXPANDER (parent))
+ gtk_expander_set_expanded (GTK_EXPANDER (parent), TRUE);
+ 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 +2872,135 @@ 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, "typing") && page->body &&
+ !preferences_typing_page_stage (page->body, &error))
+ {
+ setup_show_error (error->message);
+ g_clear_error (&error);
+ return;
+ }
+ if (!strcmp (page->id, "logging") && page->body &&
+ !preferences_history_page_stage (page->body, &error))
+ {
+ setup_reveal_widget (page->body);
+ setup_show_error (error->message);
+ g_clear_error (&error);
+ return;
+ }
+ 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 +3008,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 +3026,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 +3111,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 +3127,26 @@ 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;
+ setup_sound_tree = 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 +3194,34 @@ 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";
+ if (!strcmp (id, "completion-shortcuts")) page_id = "keyboard";
+ 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 (!strcmp (id, "completion-shortcuts"))
+ preferences_shortcuts_find_action (page->body, "Complete nick/command");
+ 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/sexy-spell-entry.c b/src/fe-gtk/sexy-spell-entry.c
index 34b0499d..2ed65fd3 100644
--- a/src/fe-gtk/sexy-spell-entry.c
+++ b/src/fe-gtk/sexy-spell-entry.c
@@ -928,6 +928,10 @@ sexy_spell_entry_init(SexySpellEntry *entry)
entry->priv = g_new0(SexySpellEntryPriv, 1);
entry->priv->dict_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+ entry->priv->attr_list = pango_attr_list_new();
+ /* An untouched input still needs an empty word list. Loading the first
+ * working dictionary on Save can recheck it before any changed signal. */
+ entry->priv->words = g_new0(gchar *, 1);
if (have_enchant)
{
@@ -939,8 +943,6 @@ sexy_spell_entry_init(SexySpellEntry *entry)
sexy_spell_entry_activate_default_languages(entry);
}
- entry->priv->attr_list = pango_attr_list_new();
-
entry->priv->checked = TRUE;
entry->priv->parseattr = TRUE;
diff --git a/src/fe-gtk/tests/test-preferences-pages.c b/src/fe-gtk/tests/test-preferences-pages.c
new file mode 100644
index 00000000..d32344be
--- /dev/null
+++ b/src/fe-gtk/tests/test-preferences-pages.c
@@ -0,0 +1,633 @@
+/* Native preferences editor regressions. GPL-2.0-or-later. */
+#include "../../common/zoitechat.h"
+#include
+#include "../../common/fe.h"
+#include "../../common/util.h"
+#include "../preferences-pages.h"
+#include "../theme/theme-access.h"
+#include "../notifications/notification-backend.h"
+
+static gboolean gtk_available, notifications_available;
+static int notification_calls, beep_calls, file_calls, sound_changes;
+static char *played_file, *opened_page;
+static char *opened_folder, *preview_mask;
+
+/* External effects are recorded; tests drive the real native page widgets. */
+int notification_backend_supported (void) { return notifications_available; }
+void notification_backend_show (const char *title, const char *text) { notification_calls++; }
+void fe_beep (session *sess) { beep_calls++; }
+void sound_play (const char *file, gboolean quiet)
+{
+ file_calls++;
+ g_free (played_file);
+ played_file = g_strdup (file);
+}
+void setup_open_page (const char *id)
+{
+ g_free (opened_page);
+ opened_page = g_strdup (id);
+}
+void setup_reveal_widget (GtkWidget *widget) { }
+void fe_open_url (const char *url)
+{
+ g_free (opened_folder);
+ opened_folder = g_strdup (url);
+}
+char *log_build_pathname (const char *mask, const char *config_dir,
+ const char *server, const char *channel,
+ const char *network, time_t now)
+{
+ /* Record the resolver input and return a sentinel path, so these UI
+ * tests verify the displayed result comes from the resolver. */
+ g_free (preview_mask);
+ preview_mask = g_strdup (mask);
+ return g_build_filename (config_dir, "resolved-example.log", NULL);
+}
+gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *rgba)
+{
+ return gdk_rgba_parse (rgba, token == THEME_TOKEN_TEXT_BACKGROUND ? "#20242a" : "#eeeeee");
+}
+int get_stamp_str (char *format, time_t stamp, char **result)
+{
+ GDateTime *date = g_date_time_new_from_unix_local (stamp);
+ *result = g_date_time_format (date, format);
+ g_date_time_unref (date);
+ return *result ? strlen (*result) : 0;
+}
+
+static void changed (gpointer data) { sound_changes++; }
+
+static GtkWidget *
+find_widget (GtkWidget *root, const char *name)
+{
+ GtkWidget *found = NULL;
+ if (!strcmp (gtk_widget_get_name (root), name)) return root;
+ if (GTK_IS_BUTTON (root) && !g_strcmp0 (gtk_button_get_label (GTK_BUTTON (root)), name)) return root;
+ if (!GTK_IS_CONTAINER (root)) return NULL;
+ GList *children = gtk_container_get_children (GTK_CONTAINER (root));
+ for (GList *i = children; i && !found; i = i->next) found = find_widget (i->data, name);
+ g_list_free (children);
+ return found;
+}
+
+static GtkWidget *control (GtkWidget *root, const char *name)
+{
+ GtkWidget *widget = find_widget (root, name);
+ g_assert_nonnull (widget);
+ return widget;
+}
+
+static void
+destroy_page (GtkWidget *page)
+{
+ g_object_ref_sink (page);
+ gtk_widget_destroy (page);
+ g_object_unref (page);
+}
+
+static void
+defaults (struct zoitechatprefs *prefs)
+{
+ memset (prefs, 0, sizeof *prefs);
+ g_strlcpy (prefs->hex_text_font, "Monospace 11", sizeof prefs->hex_text_font);
+#ifdef WIN32
+ g_strlcpy (prefs->hex_text_font_main, "Monospace 11", sizeof prefs->hex_text_font_main);
+#endif
+ g_strlcpy (prefs->hex_stamp_text_format, "[%H:%M:%S] ", sizeof prefs->hex_stamp_text_format);
+ prefs->hex_gui_tab_layout = 2;
+ prefs->hex_gui_tab_pos = 3;
+ prefs->hex_text_indent = 1;
+ prefs->hex_text_show_marker = 1;
+ prefs->hex_stamp_text = 1;
+}
+
+static void
+test_open_preserves_values (void)
+{
+ struct zoitechatprefs prefs, original;
+ GtkWidget *page;
+ char *sound = NULL;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ /* Unknown legacy values and formats must survive merely visiting a page. */
+ prefs.hex_gui_tab_layout = 1;
+ prefs.hex_gui_tab_pos = 7;
+ prefs.hex_gui_tab_trunc = 123;
+ prefs.hex_gui_tab_newtofront = 9;
+ prefs.hex_text_wrap_align = 9;
+ g_strlcpy (prefs.hex_stamp_text_format, "时刻 %H:%M ", sizeof prefs.hex_stamp_text_format);
+ original = prefs;
+ page = preferences_text_page_new (&prefs, NULL);
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ destroy_page (page);
+ page = preferences_tabs_page_new (&prefs);
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ destroy_page (page);
+ page = preferences_notifications_page_new (&prefs, 0, &sound, changed, NULL);
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ g_assert_null (sound);
+ destroy_page (page);
+}
+
+static void
+test_timestamp_and_dependencies (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page, *style, *format;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ g_strlcpy (prefs.hex_stamp_text_format, "Custom %H:%M ", sizeof prefs.hex_stamp_text_format);
+ page = preferences_text_page_new (&prefs, NULL);
+ style = control (page, "timestamp-style"); format = control (page, "timestamp-format");
+ g_assert_cmpstr (gtk_combo_box_get_active_id (GTK_COMBO_BOX (style)), ==, "custom");
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (style), "%H:%M ");
+ g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "%H:%M ");
+ g_assert_false (gtk_widget_is_sensitive (format));
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (style), "custom");
+ g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "Custom %H:%M ");
+ gtk_entry_set_text (GTK_ENTRY (format), "[%H:%M] ");
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "timestamps")), FALSE);
+ g_assert_false (gtk_widget_is_sensitive (format));
+ g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "[%H:%M] ");
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "indent-nicks")), FALSE);
+ g_assert_true (gtk_widget_is_sensitive (control (page, "wrap-alignment")));
+ gtk_combo_box_set_active (GTK_COMBO_BOX (control (page, "wrap-alignment")), 2);
+ g_assert_cmpint (prefs.hex_text_wrap_align, ==, 2);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "indent-nicks")), TRUE);
+ g_assert_false (gtk_widget_is_sensitive (control (page, "wrap-alignment")));
+ g_assert_cmpint (prefs.hex_text_wrap_align, ==, 2);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "inline-modes")), TRUE);
+ g_assert_false (gtk_widget_is_sensitive (control (page, "multiline-topic")));
+ destroy_page (page);
+}
+
+static void
+test_preview_updates (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page;
+ GtkTextBuffer *buffer;
+ GtkTextIter start, end;
+ char *text;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ page = preferences_text_page_new (&prefs, NULL);
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (control (page, "conversation-preview")));
+ gtk_text_buffer_get_bounds (buffer, &start, &end);
+ text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+ g_assert_nonnull (strstr (text, "[14:35:08]"));
+ g_assert_nonnull (strstr (text, "Unread messages"));
+ g_free (text);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "timestamps")), FALSE);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "unread-marker")), FALSE);
+ gtk_text_buffer_get_bounds (buffer, &start, &end);
+ text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+ g_assert_null (strstr (text, "14:35:08"));
+ g_assert_null (strstr (text, "Unread messages"));
+ g_free (text);
+ destroy_page (page);
+}
+
+static void
+test_layout_switching (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page, *tree_control, *tab_control;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ prefs.hex_gui_tab_closebuttons = 1;
+ prefs.hex_gui_tab_dots = 1;
+ page = preferences_tabs_page_new (&prefs);
+ tree_control = control (page, "tree-dots"); tab_control = control (page, "tab-close-buttons");
+ g_assert_true (gtk_widget_is_visible (tree_control));
+ g_assert_false (gtk_widget_is_visible (tab_control));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "layout-tabs")), TRUE);
+ g_assert_cmpint (prefs.hex_gui_tab_layout, ==, 0);
+ g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 6);
+ g_assert_true (gtk_widget_is_visible (tab_control));
+ g_assert_false (gtk_widget_is_visible (tree_control));
+ g_assert_cmpint (prefs.hex_gui_tab_dots, ==, 1);
+ gtk_combo_box_set_active (GTK_COMBO_BOX (control (page, "channel-placement")), 4);
+ g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 5);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "layout-tree")), TRUE);
+ g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 3);
+ g_assert_cmpint (prefs.hex_gui_tab_closebuttons, ==, 1);
+ destroy_page (page);
+}
+
+static void
+test_notification_mapping (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page;
+ char *sound = NULL;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "mention-desktop")), TRUE);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "private-beep")), TRUE);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "channel-tray")), TRUE);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "private-attention")), TRUE);
+ g_assert_cmpint (prefs.hex_input_balloon_hilight, ==, 1);
+ g_assert_cmpint (prefs.hex_input_balloon_chans, ==, 0);
+ g_assert_cmpint (prefs.hex_input_beep_priv, ==, 1);
+ g_assert_cmpint (prefs.hex_input_beep_hilight, ==, 0);
+ g_assert_cmpint (prefs.hex_input_tray_chans, ==, 1);
+ g_assert_cmpint (prefs.hex_input_tray_priv, ==, 0);
+ g_assert_cmpint (prefs.hex_input_flash_priv, ==, 1);
+ g_assert_false (gtk_widget_is_sensitive (control (page, "tray-close")));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "tray-enabled")), TRUE);
+ g_assert_true (gtk_widget_is_sensitive (control (page, "tray-close")));
+ destroy_page (page);
+}
+
+static void
+test_shared_sound_and_effects (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page;
+ char *sound = g_strdup ("original.wav");
+ if (!gtk_available) { g_free (sound); g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
+ sound_changes = beep_calls = file_calls = 0;
+ gtk_entry_set_text (GTK_ENTRY (control (page, "alert-sound-file")), "new.wav");
+ g_assert_cmpstr (sound, ==, "new.wav");
+ g_assert_cmpint (sound_changes, ==, 1);
+ gtk_button_clicked (GTK_BUTTON (control (page, "Play")));
+ g_assert_cmpstr (played_file, ==, "new.wav");
+ gtk_button_clicked (GTK_BUTTON (control (page, "Use system sound")));
+ g_assert_cmpstr (sound, ==, "");
+ gtk_button_clicked (GTK_BUTTON (control (page, "Play")));
+ g_assert_cmpint (beep_calls, ==, 1);
+ g_free (sound); sound = g_strdup ("changed-in-sounds.wav");
+ preferences_notifications_refresh_sound (page);
+ g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (control (page, "alert-sound-file"))), ==, sound);
+ g_assert_cmpint (sound_changes, ==, 2);
+ gtk_button_clicked (GTK_BUTTON (control (page, "open-event-sounds")));
+ g_assert_cmpstr (opened_page, ==, "sounds");
+ destroy_page (page);
+ g_free (sound);
+}
+
+static void
+test_notification_test_safety (void)
+{
+ struct zoitechatprefs prefs, original;
+ GtkWidget *page;
+ char *sound = NULL;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ prefs.hex_away_omit_alerts = prefs.hex_gui_focus_omitalerts = 1;
+ original = prefs;
+ notifications_available = TRUE; notification_calls = 0;
+ page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
+ gtk_button_clicked (GTK_BUTTON (control (page, "notification-test")));
+ g_assert_cmpint (notification_calls, ==, 1);
+ g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "notification-test-status"))), "marked away"));
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ notifications_available = FALSE;
+ gtk_button_clicked (GTK_BUTTON (control (page, "notification-test")));
+ g_assert_cmpint (notification_calls, ==, 1);
+ destroy_page (page);
+ page = preferences_notifications_page_new (&prefs, 0, &sound, changed, NULL);
+ g_assert_false (gtk_widget_is_sensitive (control (page, "notification-test")));
+ g_assert_false (gtk_widget_is_sensitive (control (page, "private-desktop")));
+ g_assert_null (find_widget (page, "private-tray"));
+ g_assert_null (find_widget (page, "private-attention"));
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ destroy_page (page);
+}
+
+static void
+test_history_preserves_values (void)
+{
+ struct zoitechatprefs prefs, original;
+ GtkWidget *page;
+ char *config, *history;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ config = g_dir_make_tmp ("zoitechat-history-ui-XXXXXX", NULL);
+ history = g_build_filename (config, "scrollback", NULL);
+ g_strlcpy (prefs.hex_irc_logmask, "custom-%s-%Y-%m.log", sizeof prefs.hex_irc_logmask);
+ g_strlcpy (prefs.hex_stamp_log_format, "Custom %H:%M ", sizeof prefs.hex_stamp_log_format);
+ prefs.hex_text_max_lines = 120000;
+ prefs.hex_url_grabber_limit = 12000;
+ original = prefs;
+ page = preferences_history_page_new (&prefs, config);
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ g_assert_false (g_file_test (history, G_FILE_TEST_EXISTS));
+ g_assert_false (gtk_widget_is_sensitive (control (page, "open-history-folder")));
+ g_assert_cmpstr (gtk_combo_box_get_active_id (GTK_COMBO_BOX (control (page, "log-file-style"))), ==, "custom");
+ char *expected = g_build_filename (config, "resolved-example.log", NULL);
+ g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (control (page, "log-example-file"))), ==, expected);
+ g_free (expected);
+ destroy_page (page);
+ g_rmdir (config);
+ g_free (config); g_free (history);
+}
+
+static void
+test_history_presets_and_custom (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page, *combo;
+ char *mask, *expected;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs); prefs.hex_irc_logging = 1;
+ mask = g_build_filename (g_get_tmp_dir (), "100%%done", "%s-custom.log", NULL);
+ g_strlcpy (prefs.hex_irc_logmask, mask, sizeof prefs.hex_irc_logmask);
+ page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
+ combo = control (page, "log-file-style");
+ gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 1);
+ expected = g_build_filename (g_get_tmp_dir (), "100%%done", "%n", "%Y-%m", "%c.log", NULL);
+ g_assert_cmpstr (prefs.hex_irc_logmask, ==, expected);
+ g_assert_cmpstr (preview_mask, ==, expected);
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "custom");
+ g_assert_cmpstr (prefs.hex_irc_logmask, ==, mask);
+ gtk_button_clicked (GTK_BUTTON (control (page, "default-log-folder")));
+ g_assert_cmpstr (prefs.hex_irc_logmask, ==, "%s-custom.log");
+ g_free (expected); g_free (mask);
+ destroy_page (page);
+}
+
+static void
+test_history_dependencies_and_timestamps (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page, *combo;
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs);
+ g_strlcpy (prefs.hex_irc_logmask, "%n" G_DIR_SEPARATOR_S "%c.log", sizeof prefs.hex_irc_logmask);
+ g_strlcpy (prefs.hex_stamp_log_format, "Time %H:%M ", sizeof prefs.hex_stamp_log_format);
+ prefs.hex_stamp_log = 1;
+ prefs.hex_url_logging = 1;
+ prefs.hex_url_grabber_limit = 91;
+ page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
+ g_assert_false (gtk_widget_is_sensitive (control (page, "choose-log-folder")));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "save-logs")), TRUE);
+ g_assert_true (gtk_widget_is_sensitive (control (page, "choose-log-folder")));
+ combo = control (page, "log-timestamp-style");
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "%H:%M ");
+ g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "%H:%M ");
+ g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "log-example-line"))), "14:35"));
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "custom");
+ g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "Time %H:%M ");
+ g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "[%H:%M:%S] ");
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "log-timestamps")), FALSE);
+ g_assert_false (gtk_widget_is_sensitive (control (page, "log-timestamp-format")));
+ g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "Time %H:%M ");
+ g_assert_false (gtk_widget_is_sensitive (control (page, "link-limit")));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "collect-links")), TRUE);
+ g_assert_true (gtk_widget_is_sensitive (control (page, "link-limit")));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "collect-links")), FALSE);
+ g_assert_cmpint (prefs.hex_url_grabber_limit, ==, 91);
+ g_assert_cmpint (prefs.hex_url_logging, ==, 1);
+ gtk_button_clicked (GTK_BUTTON (control (page, "open-data-folder")));
+ g_assert_cmpstr (opened_folder, ==, g_get_tmp_dir ());
+ destroy_page (page);
+}
+
+static void
+test_history_validation (void)
+{
+ struct zoitechatprefs prefs;
+ GtkWidget *page;
+ GError *error = NULL;
+ GString *long_format = g_string_new ("");
+ if (!gtk_available) { g_string_free (long_format, TRUE); g_test_skip ("GTK display unavailable"); return; }
+ defaults (&prefs); prefs.hex_irc_logging = 1;
+ g_strlcpy (prefs.hex_irc_logmask, "old-custom.log", sizeof prefs.hex_irc_logmask);
+ page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
+ g_assert_true (preferences_history_page_stage (page, &error));
+ gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), "");
+ g_assert_false (preferences_history_page_stage (page, &error));
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_clear_error (&error);
+ for (int i = 0; i < 140; i++) g_string_append (long_format, "é");
+ gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), long_format->str);
+ g_assert_false (preferences_history_page_stage (page, &error));
+ g_clear_error (&error);
+ gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), "corrected-%Y.log");
+ g_assert_true (preferences_history_page_stage (page, &error));
+ g_assert_no_error (error);
+ destroy_page (page);
+ g_string_free (long_format, TRUE);
+}
+
+static GtkWidget *
+language_button (GtkWidget *root, const char *code)
+{
+ GtkWidget *found = NULL;
+ if (!g_strcmp0 (g_object_get_data (G_OBJECT (root), "language-code"), code)) return root;
+ if (!GTK_IS_CONTAINER (root)) return NULL;
+ GList *children = gtk_container_get_children (GTK_CONTAINER (root));
+ for (GList *i = children; i && !found; i = i->next) found = language_button (i->data, code);
+ g_list_free (children); return found;
+}
+
+static void
+test_typing_preservation_and_languages (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ struct zoitechatprefs prefs, original;
+ defaults (&prefs);
+ prefs.hex_gui_input_spell = prefs.hex_gui_input_icon = 1;
+ prefs.hex_completion_amount = 5;
+ g_strlcpy (prefs.hex_text_spell_langs, "en_US, missing_XX", sizeof prefs.hex_text_spell_langs);
+ g_strlcpy (prefs.hex_completion_suffix, ",", sizeof prefs.hex_completion_suffix);
+ original = prefs;
+ PreferencesSpellLanguage languages[] = {{"en_US", "English (United States)"}, {"en_CA", "English (Canada)"}};
+ GtkWidget *page = preferences_typing_page_new (&prefs, gtk_entry_new (), languages, 2, NULL);
+ g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
+ g_assert_true (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (language_button (page, "en_US"))));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (language_button (page, "en_CA")), TRUE);
+ g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "missing_XX"));
+ g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "en_CA"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (language_button (page, "en_US")), FALSE);
+ g_assert_null (strstr (prefs.hex_text_spell_langs, "en_US"));
+ g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "missing_XX"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "spelling-enabled")), FALSE);
+ g_assert_false (gtk_widget_is_sensitive (language_button (page, "en_CA")));
+ g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "en_CA"));
+ g_assert_false (gtk_widget_is_sensitive (control (page, "input-mode-icon")));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "input-nickname")), TRUE);
+ g_assert_true (gtk_widget_is_sensitive (control (page, "input-mode-icon")));
+ destroy_page (page);
+}
+
+static void
+test_typing_completion_and_validation (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ struct zoitechatprefs prefs;
+ defaults (&prefs); prefs.hex_completion_amount = 5;
+ GtkWidget *page = preferences_typing_page_new (&prefs, gtk_entry_new (), NULL, 0, NULL);
+ GtkWidget *result = control (page, "completion-example-input");
+ gtk_entry_set_text (GTK_ENTRY (result), "Al");
+ gtk_spin_button_set_value (GTK_SPIN_BUTTON (control (page, "completion-limit")), 0);
+ gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), "✓");
+ g_assert_cmpstr (prefs.hex_completion_suffix, ==, "✓");
+ GError *error = NULL;
+ g_assert_true (preferences_typing_page_stage (page, &error));
+ gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), "😀");
+ g_assert_false (preferences_typing_page_stage (page, &error));
+ g_assert_cmpstr (prefs.hex_completion_suffix, ==, "✓");
+ g_clear_error (&error);
+ gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), ":");
+ char *too_long = g_strnfill (64, 'a');
+ gtk_entry_set_text (GTK_ENTRY (control (page, "spelling-codes")), too_long);
+ g_assert_false (preferences_typing_page_stage (page, &error));
+ g_clear_error (&error); g_free (too_long);
+ gtk_entry_set_text (GTK_ENTRY (control (page, "spelling-codes")), "unavailable-code");
+ g_assert_true (preferences_typing_page_stage (page, &error));
+ g_assert_no_error (error);
+ destroy_page (page);
+}
+
+static GdkModifierType test_modifiers (GdkModifierType mods)
+{
+ return mods & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_META_MASK);
+}
+static void test_restore (gpointer data) { }
+
+static GtkWidget *
+shortcut_fixture (GtkListStore **store)
+{
+ *store = gtk_list_store_new (6, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ GtkTreeIter iter;
+ for (int i = 0; i < 3; i++)
+ {
+ gtk_list_store_append (*store, &iter);
+ gtk_list_store_set (*store, &iter, 0, "Ctrl+K", 1, "k", 2, i == 1 ? "Change Page" : "Run Command",
+ 3, i == 0 ? "\002" : i == 1 ? "2" : "echo original\\nsecond", 4, i == 1 ? "Relative-custom-value" : NULL, 5, i != 0, -1);
+ }
+ PreferencesShortcutAction actions[] = {{"Run Command", "Run a command"}, {"Change Page", "Change a page"}};
+ return preferences_shortcuts_page_new (*store, actions, 2, test_modifiers, test_restore, NULL);
+}
+
+static void
+test_shortcuts_filter_and_protection (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ GtkListStore *store;
+ GtkWidget *page = shortcut_fixture (&store);
+ GtkTreeIter iter;
+ gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
+ char *data;
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
+ g_assert_cmpstr (data, ==, "\002"); g_free (data);
+ g_assert_false (gtk_widget_get_sensitive (control (page, "shortcut-action")));
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-remove")));
+ g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
+ g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "shortcut-shared"))), "3 bindings"));
+ gtk_entry_set_text (GTK_ENTRY (control (page, "shortcut-search")), "Relative-custom-value");
+ g_signal_emit_by_name (control (page, "shortcut-search"), "search-changed");
+ GtkTreeModel *filtered = gtk_tree_view_get_model (GTK_TREE_VIEW (control (page, "shortcuts-list")));
+ g_assert_cmpint (gtk_tree_model_iter_n_children (filtered, NULL), ==, 1);
+ g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
+ GtkTreePath *path = gtk_tree_path_new_from_indices (1, -1);
+ preferences_shortcuts_reveal_path (page, path); gtk_tree_path_free (path);
+ g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (control (page, "shortcut-data2"))), ==, "Relative-custom-value");
+ gtk_combo_box_set_active_id (GTK_COMBO_BOX (control (page, "shortcut-action")), "Run Command");
+ gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 4, &data, -1);
+ g_assert_cmpstr (data, ==, "Relative-custom-value"); g_free (data);
+ destroy_page (page); g_object_unref (store);
+}
+
+static void
+test_shortcuts_duplicate_and_record (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ GtkListStore *store;
+ GtkWidget *page = shortcut_fixture (&store);
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-duplicate")));
+ g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 4);
+ gtk_entry_set_text (GTK_ENTRY (control (page, "shortcut-data1")), "echo changed\\nnext");
+ GtkTreeIter iter; char *data;
+ gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 3);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
+ g_assert_cmpstr (data, ==, "echo changed\\nnext"); g_free (data);
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
+ GtkWidget *dialog = NULL;
+ GList *tops = gtk_window_list_toplevels ();
+ for (GList *i = tops; i; i = i->next) if (!strcmp (gtk_widget_get_name (i->data), "shortcut-recorder")) dialog = i->data;
+ g_list_free (tops); g_assert_nonnull (dialog);
+ GtkWidget *view = control (dialog, "GtkTreeView");
+ GList *cells = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (gtk_tree_view_get_column (GTK_TREE_VIEW (view), 0)));
+ g_signal_emit_by_name (cells->data, "accel-edited", "0", GDK_KEY_Tab, GDK_SHIFT_MASK | GDK_LOCK_MASK, 23);
+ g_list_free (cells);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 1, &data, -1);
+ guint key; GdkModifierType mods;
+ gtk_accelerator_parse (data, &key, &mods);
+ g_assert_cmpuint (key, ==, GDK_KEY_ISO_Left_Tab);
+ g_assert_cmpuint (mods, ==, GDK_SHIFT_MASK); g_free (data);
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-remove")));
+ g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
+ destroy_page (page); g_object_unref (store);
+}
+
+static void
+test_shortcuts_order_and_recorder_close (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ GtkListStore *store;
+ GtkWidget *page = shortcut_fixture (&store);
+ GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ g_object_ref_sink (window);
+ gtk_container_add (GTK_CONTAINER (window), page);
+ gtk_widget_show_all (window);
+ GtkTreePath *path = gtk_tree_path_new_from_indices (2, -1);
+ preferences_shortcuts_reveal_path (page, path); gtk_tree_path_free (path);
+ GdkEventKey event = {0};
+ event.type = GDK_KEY_PRESS; event.keyval = GDK_KEY_Up; event.state = GDK_SHIFT_MASK;
+ gboolean handled = FALSE;
+ g_signal_emit_by_name (control (page, "shortcuts-list"), "key-press-event", &event, &handled);
+ g_assert_true (handled);
+ GtkTreeIter iter; char *data;
+ gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
+ g_assert_cmpstr (data, ==, "echo original\\nsecond"); g_free (data);
+ g_signal_emit_by_name (control (page, "shortcuts-list"), "key-press-event", &event, &handled);
+ gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
+ g_assert_cmpstr (data, ==, "\002"); g_free (data);
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
+ GList *tops = gtk_window_list_toplevels ();
+ GtkWidget *dialog = NULL;
+ for (GList *i = tops; i; i = i->next) if (!strcmp (gtk_widget_get_name (i->data), "shortcut-recorder")) dialog = i->data;
+ g_list_free (tops); g_assert_nonnull (dialog);
+ gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
+ gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 1, &data, -1);
+ g_assert_cmpstr (data, ==, "k"); g_free (data);
+ gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
+ gtk_widget_destroy (window);
+ g_object_unref (window); g_object_unref (store);
+}
+
+int main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+ gtk_available = gtk_init_check (&argc, &argv);
+ g_test_add_func ("/preferences/pages/open-preserves-values", test_open_preserves_values);
+ g_test_add_func ("/preferences/pages/timestamps-and-dependencies", test_timestamp_and_dependencies);
+ g_test_add_func ("/preferences/pages/conversation-preview", test_preview_updates);
+ g_test_add_func ("/preferences/pages/layout-switching", test_layout_switching);
+ g_test_add_func ("/preferences/pages/notification-mapping", test_notification_mapping);
+ g_test_add_func ("/preferences/pages/shared-sound", test_shared_sound_and_effects);
+ g_test_add_func ("/preferences/pages/notification-test", test_notification_test_safety);
+ g_test_add_func ("/preferences/history/open-preserves-values", test_history_preserves_values);
+ g_test_add_func ("/preferences/history/presets-and-custom", test_history_presets_and_custom);
+ g_test_add_func ("/preferences/history/dependencies-and-timestamps", test_history_dependencies_and_timestamps);
+ g_test_add_func ("/preferences/history/validation", test_history_validation);
+ g_test_add_func ("/preferences/typing/languages-preserved", test_typing_preservation_and_languages);
+ g_test_add_func ("/preferences/typing/completion-validation", test_typing_completion_and_validation);
+ g_test_add_func ("/preferences/shortcuts/filter-and-protection", test_shortcuts_filter_and_protection);
+ g_test_add_func ("/preferences/shortcuts/duplicate-and-record", test_shortcuts_duplicate_and_record);
+ g_test_add_func ("/preferences/shortcuts/order-and-recorder-close", test_shortcuts_order_and_recorder_close);
+ int result = g_test_run ();
+ g_free (played_file); g_free (opened_page);
+ g_free (opened_folder); g_free (preview_mask);
+ return result;
+}
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/tests/test-spell-entry.c b/src/fe-gtk/tests/test-spell-entry.c
new file mode 100644
index 00000000..f010bb13
--- /dev/null
+++ b/src/fe-gtk/tests/test-spell-entry.c
@@ -0,0 +1,180 @@
+/* Spell-entry dictionary reload regressions. GPL-2.0-or-later. */
+#include "../../common/zoitechat.h"
+
+/* Exercise the actual widget with a deterministic dictionary provider. This
+ * must also run on builders without Enchant or installed dictionaries. */
+#include "../sexy-spell-entry.c"
+
+struct zoitechatprefs prefs;
+struct EnchantBroker { int unused; };
+struct EnchantDict { char *language; };
+static gboolean gtk_available, dictionaries_available;
+static int active_dicts, checked_words;
+
+gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *color)
+{
+ return gdk_rgba_parse (color, token == THEME_TOKEN_TEXT_BACKGROUND ? "#ffffff" : "#000000");
+}
+gboolean theme_get_mirc_color (unsigned int index, GdkRGBA *color)
+{
+ return gdk_rgba_parse (color, "#000000");
+}
+void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *values)
+{
+ memset (values, 0, sizeof *values);
+ gdk_rgba_parse (&values->background, "#ffffff");
+}
+void theme_palette_color_get_rgb16 (const GdkRGBA *color, guint16 *red, guint16 *green, guint16 *blue)
+{
+ *red = color->red * 65535;
+ *green = color->green * 65535;
+ *blue = color->blue * 65535;
+}
+const char *gtkutil_icon_name_from_stock (const char *stock) { return stock; }
+GtkWidget *gtkutil_image_new_from_stock (const char *stock, GtkIconSize size)
+{
+ return gtk_image_new_from_icon_name (stock, size);
+}
+
+#ifdef HAVE_ISO_CODES
+void codetable_init (void) { }
+void codetable_free (void) { }
+void codetable_lookup (const gchar *code, const gchar **language, const gchar **country)
+{
+ *language = code;
+ *country = "";
+}
+#endif
+
+static struct EnchantBroker *test_broker_new (void) { return g_new0 (struct EnchantBroker, 1); }
+static void test_broker_free (struct EnchantBroker *broker) { g_free (broker); }
+static void test_list_dicts (struct EnchantBroker *broker, EnchantDictDescribeFn describe, void *data)
+{
+ if (dictionaries_available) describe ("en_US", "test", "test", "", data);
+}
+static struct EnchantDict *test_request_dict (struct EnchantBroker *broker, const char *const language)
+{
+ if (!dictionaries_available || (strcmp (language, "en_US") && strcmp (language, "en"))) return NULL;
+ struct EnchantDict *dict = g_new0 (struct EnchantDict, 1);
+ dict->language = g_strdup (language);
+ active_dicts++;
+ return dict;
+}
+static void test_free_dict (struct EnchantBroker *broker, struct EnchantDict *dict)
+{
+ g_free (dict->language);
+ g_free (dict);
+ active_dicts--;
+}
+static void test_describe_dict (struct EnchantDict *dict, EnchantDictDescribeFn describe, void *data)
+{
+ describe (dict->language, "test", "test", "", data);
+}
+static void test_add_word (struct EnchantDict *dict, const char *const word, ssize_t length) { }
+static int test_check_word (struct EnchantDict *dict, const char *const word, ssize_t length)
+{
+ checked_words++;
+ return 0;
+}
+
+static SexySpellEntry *
+new_input (gboolean available)
+{
+ dictionaries_available = available;
+ memset (&prefs, 0, sizeof prefs);
+ g_strlcpy (prefs.hex_text_spell_langs, "missing_XX", sizeof prefs.hex_text_spell_langs);
+ SexySpellEntry *entry = SEXY_SPELL_ENTRY (sexy_spell_entry_new ());
+ g_object_ref_sink (entry);
+ return entry;
+}
+
+static void
+destroy_input (SexySpellEntry *entry)
+{
+ gtk_widget_destroy (GTK_WIDGET (entry));
+ g_object_unref (entry);
+ g_assert_cmpint (active_dicts, ==, 0);
+}
+
+static void
+test_first_dictionary_on_empty_input (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ SexySpellEntry *entry = new_input (FALSE);
+ g_assert_null (entry->priv->dict_list);
+ /* The real Save path: an untouched input, no old dictionary, then a newly
+ * selected dictionary. Before the fix, the final recheck dereferenced NULL. */
+ dictionaries_available = TRUE;
+ g_strlcpy (prefs.hex_text_spell_langs, "en_US", sizeof prefs.hex_text_spell_langs);
+ for (int i = 0; i < 3; i++)
+ {
+ sexy_spell_entry_deactivate_language (entry, NULL);
+ sexy_spell_entry_activate_default_languages (entry);
+ g_assert_true (sexy_spell_entry_language_is_active (entry, "en_US"));
+ g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "");
+ }
+ destroy_input (entry);
+}
+
+static void
+test_reload_preserves_draft (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ SexySpellEntry *entry = new_input (FALSE);
+ const char *draft = "Hello café";
+ gtk_entry_set_text (GTK_ENTRY (entry), draft);
+ dictionaries_available = TRUE;
+ g_strlcpy (prefs.hex_text_spell_langs, "en_US", sizeof prefs.hex_text_spell_langs);
+ checked_words = 0;
+ sexy_spell_entry_deactivate_language (entry, NULL);
+ sexy_spell_entry_activate_default_languages (entry);
+ g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, draft);
+ g_assert_cmpint (checked_words, >, 0);
+ /* Save-failure rollback and retry also reload the input dictionaries. */
+ dictionaries_available = FALSE;
+ sexy_spell_entry_deactivate_language (entry, NULL);
+ sexy_spell_entry_activate_default_languages (entry);
+ dictionaries_available = TRUE;
+ sexy_spell_entry_activate_default_languages (entry);
+ g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, draft);
+ destroy_input (entry);
+}
+
+static void
+test_empty_input_spelling_toggles (void)
+{
+ if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
+ SexySpellEntry *entry = new_input (TRUE);
+ sexy_spell_entry_set_parse_attributes (entry, FALSE);
+ sexy_spell_entry_set_checked (entry, FALSE);
+ sexy_spell_entry_set_checked (entry, TRUE);
+ sexy_spell_entry_set_parse_attributes (entry, TRUE);
+ sexy_spell_entry_deactivate_language (entry, NULL);
+ sexy_spell_entry_activate_default_languages (entry);
+ g_assert_true (sexy_spell_entry_language_is_active (entry, "en"));
+ destroy_input (entry);
+}
+
+int main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+ gtk_available = gtk_init_check (&argc, &argv);
+ if (gtk_available)
+ {
+ SexySpellEntryClass *klass = g_type_class_ref (SEXY_TYPE_SPELL_ENTRY);
+ have_enchant = TRUE;
+ klass->word_check = default_word_check;
+ enchant_broker_init = test_broker_new;
+ enchant_broker_free = test_broker_free;
+ enchant_broker_list_dicts = test_list_dicts;
+ enchant_broker_request_dict = test_request_dict;
+ enchant_broker_free_dict = test_free_dict;
+ enchant_dict_describe = test_describe_dict;
+ enchant_dict_add_to_session = test_add_word;
+ enchant_dict_check = test_check_word;
+ }
+ g_test_add_func ("/spell-entry/first-dictionary-empty-input", test_first_dictionary_on_empty_input);
+ g_test_add_func ("/spell-entry/reload-preserves-draft", test_reload_preserves_draft);
+ g_test_add_func ("/spell-entry/empty-input-spelling-toggles", test_empty_input_spelling_toggles);
+ 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..03347f69 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,140 @@ 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 ("");
+ gtk_label_set_text (GTK_LABEL (ui->contrast), message);
+ gtk_widget_set_visible (ui->contrast, warning);
+ 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 (_("Preview"));
+ 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);
+ gtk_widget_set_tooltip_text (frame, _("Select part of the preview to edit its colour."));
+ 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 +605,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 +744,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 +779,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 +853,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 +898,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 +938,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 +959,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 +968,415 @@ 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)
+ {
+ gtk_label_set_text (GTK_LABEL (title), name);
+ }
+}
+
+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") :
+ matched == 0 ? _("Follows application theme") : "");
+}
+
+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);
+static GtkWidget *
+theme_preferences_heading (const char *text)
+{
+ GtkWidget *heading = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ GtkWidget *label = gtk_label_new (NULL);
+ GtkWidget *line = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ char *markup = g_markup_printf_escaped ("%s", text);
+ gtk_label_set_markup (GTK_LABEL (label), markup);
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ g_free (markup);
+ gtk_box_pack_start (GTK_BOX (heading), label, FALSE, FALSE, 0);
+ gtk_widget_set_valign (line, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (heading), line, TRUE, TRUE, 0);
+ return heading;
+}
+
+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, 8);
+ 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), 4);
+ 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 ("");
+ gtk_widget_set_no_show_all (ui->contrast, TRUE);
+ 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_widget_set_tooltip_text (common, _("Theme uses the application appearance. Set a colour to override it."));
+ 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 (_("More colours"));
+ 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 +1511,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 +1857,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 +1866,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 +1876,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 +1900,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 +1916,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 +1959,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 +1972,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 +2038,7 @@ theme_preferences_populate_gtk3 (theme_preferences_ui *ui)
}
g_ptr_array_unref (themes);
+ theme_preferences_rebuild_cards (ui);
}
static void
@@ -1779,14 +2046,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 +2130,231 @@ 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 void
+theme_preferences_sync_cards (theme_preferences_ui *ui)
{
- 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;
+ 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;
+}
- ui = g_new0 (theme_preferences_ui, 1);
- ui->parent = parent;
- ui->setup_prefs = setup_prefs;
+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);
+}
- box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6);
- gtk_container_set_border_width (GTK_CONTAINER (box), 6);
+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);
+}
- 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 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;
+}
- 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_search_changed (GtkEditable *entry, gpointer data)
+{
+ theme_preferences_ui *ui = data;
+ (void)entry;
+ gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (ui->theme_cards));
+}
- 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);
-
- 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);
-
- 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_HORIZONTAL, 8);
+ GtkWidget *picture = NULL;
+ if (theme->thumbnail_path)
+ {
+ GdkPixbuf *image = gdk_pixbuf_new_from_file_at_scale (theme->thumbnail_path, 48, 28, 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_MENU);
+ gtk_container_set_border_width (GTK_CONTAINER (content), 4);
+ 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_widget_set_tooltip_text (button, theme->source == ZOITECHAT_GTK3_THEME_SOURCE_USER ? _("Installed by you") : _("Available on this computer"));
+ 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, 12);
+ 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, 0);
+ const char *names[] = { N_("Follow system"), N_("Light"), N_("Dark") };
+ gtk_box_pack_start (GTK_BOX (box), theme_preferences_heading (_("Application theme")), FALSE, FALSE, 0);
+ gtk_box_pack_start (GTK_BOX (box), modes, FALSE, FALSE, 0);
+ gtk_widget_set_tooltip_text (modes, _("Follow system uses your desktop theme. Light and Dark use the built-in appearance."));
+ gtk_style_context_add_class (gtk_widget_get_style_context (modes), "linked");
+ for (int i = 0; i < 3; i++)
+ {
+ GtkWidget *button = gtk_toggle_button_new_with_label (_(names[i]));
+ 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;
+ }
+ 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), 96);
+ gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (scroller), 180);
+ 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), 1);
+ gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (ui->theme_cards), 1);
+ 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 and removing theme files takes effect immediately.")), 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_heading (_("Chat colours")), 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_widget_set_tooltip_text (import, _("Import colours.conf / colors.conf or HexChat themes (.hct). Changes apply when you save Preferences."));
+ 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);