From 531f18d827dd515befa8a91f5ac80c6b3872498d Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch
Date: Thu, 19 Dec 2024 08:51:52 +0100
Subject: GUI: add basic configuration
It is simply not feasible to write the text file by hand on Windows.
---
 src/stardict.c |  22 ++++-
 src/stardict.h |   1 +
 src/tdv-gui.c  | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/utils.c    |  21 +++-
 src/utils.h    |   1 +
 5 files changed, 342 insertions(+), 10 deletions(-)
(limited to 'src')
diff --git a/src/stardict.c b/src/stardict.c
index 6775553..8fb2f68 100644
--- a/src/stardict.c
+++ b/src/stardict.c
@@ -357,6 +357,20 @@ error:
 	return ret_val;
 }
 
+/// Read an .ifo file.
+/// @return StardictInfo *. Deallocate with stardict_info_free();
+StardictInfo *
+stardict_info_new (const gchar *filename, GError **error)
+{
+	StardictInfo *ifo = g_new (StardictInfo, 1);
+	if (!load_ifo (ifo, filename, error))
+	{
+		g_free (ifo);
+		return NULL;
+	}
+	return ifo;
+}
+
 /// List all dictionary files located in a path.
 /// @return GList 
+ * Copyright (c) 2020 - 2024, Přemysl Eric Janouch  
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted.
@@ -295,6 +295,8 @@ show_error_dialog (GError *error)
 	g_error_free (error);
 }
 
+// --- Loading -----------------------------------------------------------------
+
 static void
 on_new_dictionaries_loaded (G_GNUC_UNUSED GObject* source_object,
 	GAsyncResult* res, G_GNUC_UNUSED gpointer user_data)
@@ -360,8 +362,8 @@ reload_dictionaries (GPtrArray *new_dictionaries, GError **error)
 	return TRUE;
 }
 
-static void
-on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
+static GtkWidget *
+new_open_dialog (void)
 {
 	// The default is local-only.  Paths are returned absolute.
 	GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open dictionary"),
@@ -375,7 +377,14 @@ on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
 	GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
 	gtk_file_chooser_add_filter (chooser, filter);
 	gtk_file_chooser_set_select_multiple (chooser, TRUE);
+	return dialog;
+}
 
+static void
+on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
+{
+	GtkWidget *dialog = new_open_dialog ();
+	GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
 	GPtrArray *new_dictionaries =
 		g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
 	if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
@@ -431,6 +440,280 @@ on_drag_data_received (G_GNUC_UNUSED GtkWidget *widget, GdkDragContext *context,
 		show_error_dialog (error);
 }
 
+// --- Settings ----------------------------------------------------------------
+
+typedef struct settings_data            SettingsData;
+
+enum
+{
+	SETTINGS_COLUMN_NAME,
+	SETTINGS_COLUMN_PATH,
+	SETTINGS_COLUMN_COUNT
+};
+
+struct settings_data
+{
+	GKeyFile *key_file;                 ///< Configuration file
+	GtkTreeModel *model;                ///< GtkListStore
+};
+
+static void
+settings_load (SettingsData *data)
+{
+	// We want to keep original comments, as well as any other data.
+	GError *error = NULL;
+	data->key_file = load_project_config_file (&error);
+	if (!data->key_file)
+	{
+		if (error)
+			show_error_dialog (error);
+		data->key_file = g_key_file_new ();
+	}
+
+	GtkListStore *list_store = gtk_list_store_new (SETTINGS_COLUMN_COUNT,
+		G_TYPE_STRING, G_TYPE_STRING);
+	data->model = GTK_TREE_MODEL (list_store);
+
+	const gchar *dictionaries = "Dictionaries";
+	gchar **names =
+		g_key_file_get_keys (data->key_file, dictionaries, NULL, NULL);
+	if (!names)
+		return;
+
+	for (gsize i = 0; names[i]; i++)
+	{
+		gchar *path = g_key_file_get_string (data->key_file,
+			dictionaries, names[i], NULL);
+		if (!path)
+			continue;
+
+		GtkTreeIter iter = { 0 };
+		gtk_list_store_append (list_store, &iter);
+		gtk_list_store_set (list_store, &iter,
+			SETTINGS_COLUMN_NAME, names[i], SETTINGS_COLUMN_PATH, path, -1);
+		g_free (path);
+	}
+	g_strfreev (names);
+}
+
+static void
+settings_save (SettingsData *data)
+{
+	const gchar *dictionaries = "Dictionaries";
+	g_key_file_remove_group (data->key_file, dictionaries, NULL);
+
+	GtkTreeIter iter = { 0 };
+	gboolean valid = gtk_tree_model_get_iter_first (data->model, &iter);
+	while (valid)
+	{
+		gchar *name = NULL, *path = NULL;
+		gtk_tree_model_get (data->model, &iter,
+			SETTINGS_COLUMN_NAME, &name, SETTINGS_COLUMN_PATH, &path, -1);
+		if (name && path)
+			g_key_file_set_string (data->key_file, dictionaries, name, path);
+		g_free (name);
+		g_free (path);
+
+		valid = gtk_tree_model_iter_next (data->model, &iter);
+	}
+
+	GError *e = NULL;
+	if (!save_project_config_file (data->key_file, &e))
+		show_error_dialog (e);
+}
+
+static void
+on_settings_name_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
+	const gchar *path_string, const gchar *new_text, gpointer data)
+{
+	GtkTreeModel *model = GTK_TREE_MODEL (data);
+	GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
+	GtkTreeIter iter = { 0 };
+	gtk_tree_model_get_iter (model, &iter, path);
+	gtk_list_store_set (GTK_LIST_STORE (model), &iter,
+		SETTINGS_COLUMN_NAME, new_text, -1);
+	gtk_tree_path_free (path);
+}
+
+static void
+on_settings_path_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
+	const gchar *path_string, const gchar *new_text, gpointer data)
+{
+	GtkTreeModel *model = GTK_TREE_MODEL (data);
+	GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
+	GtkTreeIter iter = { 0 };
+	gtk_tree_model_get_iter (model, &iter, path);
+	gtk_list_store_set (GTK_LIST_STORE (model), &iter,
+		SETTINGS_COLUMN_PATH, new_text, -1);
+	gtk_tree_path_free (path);
+}
+
+static void
+on_settings_add (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
+{
+	GtkWidget *dialog = new_open_dialog ();
+	GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
+
+	GSList *paths = NULL;
+	if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
+		paths = gtk_file_chooser_get_filenames (chooser);
+	gtk_widget_destroy (dialog);
+	// When the dialog is aborted, we simply add an empty list.
+
+	GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
+	gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
+	GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
+	GtkListStore *list_store = GTK_LIST_STORE (model);
+
+	const gchar *home = g_get_home_dir ();
+	for (GSList *iter = paths; iter; iter = iter->next)
+	{
+		GError *error = NULL;
+		StardictInfo *ifo = stardict_info_new (iter->data, &error);
+		g_free (iter->data);
+		if (!ifo)
+		{
+			show_error_dialog (error);
+			continue;
+		}
+
+		// We also expand tildes, even on Windows, so no problem there.
+		const gchar *path = stardict_info_get_path (ifo);
+		gchar *tildified = g_str_has_prefix (stardict_info_get_path (ifo), home)
+			? g_strdup_printf ("~%s", path + strlen (home))
+			: g_strdup (path);
+
+		GtkTreeIter iter = { 0 };
+		gtk_list_store_append (list_store, &iter);
+		gtk_list_store_set (list_store, &iter,
+			SETTINGS_COLUMN_NAME, stardict_info_get_book_name (ifo),
+			SETTINGS_COLUMN_PATH, tildified, -1);
+		g_free (tildified);
+		stardict_info_free (ifo);
+	}
+	g_slist_free (paths);
+}
+
+static void
+on_settings_remove (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
+{
+	GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
+	GtkTreeSelection *selection = gtk_tree_view_get_selection (tree_view);
+	GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
+	GtkListStore *list_store = GTK_LIST_STORE (model);
+
+	GList *selected = gtk_tree_selection_get_selected_rows (selection, &model);
+	for (GList *iter = selected; iter; iter = iter->next)
+	{
+		GtkTreePath *path = iter->data;
+		iter->data = gtk_tree_row_reference_new (model, path);
+		gtk_tree_path_free (path);
+	}
+	for (GList *iter = selected; iter; iter = iter->next)
+	{
+		GtkTreePath *path = gtk_tree_row_reference_get_path (iter->data);
+		if (path)
+		{
+			GtkTreeIter tree_iter = { 0 };
+			if (gtk_tree_model_get_iter (model, &tree_iter, path))
+				gtk_list_store_remove (list_store, &tree_iter);
+			gtk_tree_path_free (path);
+		}
+	}
+	g_list_free_full (selected, (GDestroyNotify) gtk_tree_row_reference_free);
+}
+
+static void
+on_settings_selection_changed
+	(GtkTreeSelection* selection, gpointer user_data)
+{
+	GtkWidget *remove = GTK_WIDGET (user_data);
+	gtk_widget_set_sensitive (remove,
+		gtk_tree_selection_count_selected_rows (selection) > 0);
+}
+
+static void
+on_settings (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
+{
+	SettingsData sd = {};
+	settings_load (&sd);
+
+	GtkWidget *treeview = gtk_tree_view_new_with_model (sd.model);
+	gtk_tree_view_set_reorderable (GTK_TREE_VIEW (treeview), TRUE);
+	g_object_unref (sd.model);
+
+	GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+	g_object_set (renderer, "editable", TRUE, NULL);
+	g_signal_connect (renderer, "edited",
+		G_CALLBACK (on_settings_name_edited), sd.model);
+	GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes
+		(_("Name"), renderer, "text", SETTINGS_COLUMN_NAME, NULL);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
+
+	renderer = gtk_cell_renderer_text_new ();
+	g_object_set (renderer, "editable", TRUE, NULL);
+	g_signal_connect (renderer, "edited",
+		G_CALLBACK (on_settings_path_edited), sd.model);
+	column = gtk_tree_view_column_new_with_attributes
+		(_("Path"), renderer, "text", SETTINGS_COLUMN_PATH, NULL);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
+
+	GtkWidget *scrolled = gtk_scrolled_window_new (NULL, NULL);
+	gtk_scrolled_window_set_shadow_type
+		(GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_ETCHED_IN);
+	gtk_container_add (GTK_CONTAINER (scrolled), treeview);
+	GtkWidget *dialog = gtk_dialog_new_with_buttons (_("Settings"),
+		GTK_WINDOW (g.window),
+		GTK_DIALOG_MODAL,
+		_("_Cancel"), GTK_RESPONSE_CANCEL,
+		_("_Save"), GTK_RESPONSE_ACCEPT,
+		NULL);
+	gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
+	gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 400);
+
+	GtkWidget *remove = gtk_button_new_with_mnemonic (_("_Remove"));
+	gtk_widget_set_sensitive (remove, FALSE);
+	g_signal_connect (remove, "clicked",
+		G_CALLBACK (on_settings_remove), treeview);
+
+	GtkWidget *add = gtk_button_new_with_mnemonic (_("_Add..."));
+	g_signal_connect (add, "clicked",
+		G_CALLBACK (on_settings_add), treeview);
+
+	GtkTreeSelection *selection =
+		gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
+	gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
+	g_signal_connect (selection, "changed",
+		G_CALLBACK (on_settings_selection_changed), remove);
+
+	GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+	gtk_box_pack_start (GTK_BOX (box),
+		gtk_label_new (_("Here you can configure the default dictionaries.")),
+		FALSE, FALSE, 0);
+	gtk_box_pack_end (GTK_BOX (box), remove, FALSE, FALSE, 0);
+	gtk_box_pack_end (GTK_BOX (box), add, FALSE, FALSE, 0);
+
+	GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+	g_object_set (content_area, "margin", 12, NULL);
+	gtk_box_pack_start (GTK_BOX (content_area), box, FALSE, FALSE, 0);
+	gtk_box_pack_start (GTK_BOX (content_area), scrolled, TRUE, TRUE, 12);
+
+	gtk_widget_show_all (dialog);
+	switch (gtk_dialog_run (GTK_DIALOG (dialog)))
+	{
+	case GTK_RESPONSE_NONE:
+		break;
+	case GTK_RESPONSE_ACCEPT:
+		settings_save (&sd);
+		// Fall through
+	default:
+		gtk_widget_destroy (dialog);
+	}
+	g_key_file_free (sd.key_file);
+}
+
+// --- Main --------------------------------------------------------------------
+
 static void
 on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
 {
@@ -464,8 +747,19 @@ gui_main (char *argv[])
 		die_with_dialog (error->message);
 
 	if (!new_dictionaries->len)
-		die_with_dialog (_("No dictionaries found either in "
+	{
+		GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
+			GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
+			_("No dictionaries found either in "
 			"the configuration or on the command line"));
+		gtk_dialog_run (GTK_DIALOG (dialog));
+		gtk_widget_destroy (dialog);
+
+		// This is better than nothing.
+		// Our GtkNotebook action widget would be invisible without any tabs.
+		on_settings (NULL, NULL);
+		exit (EXIT_SUCCESS);
+	}
 
 	// Some Adwaita stupidity, plus defaults for our own widget.
 	// All the named colours have been there since GNOME 3.4
@@ -504,6 +798,10 @@ gui_main (char *argv[])
 	GtkWidget *item_open = gtk_menu_item_new_with_mnemonic (_("_Open..."));
 	g_signal_connect (item_open, "activate", G_CALLBACK (on_open), NULL);
 
+	GtkWidget *item_settings = gtk_menu_item_new_with_mnemonic (_("_Settings"));
+	g_signal_connect (item_settings, "activate",
+		G_CALLBACK (on_settings), NULL);
+
 	g.watch_selection = TRUE;
 	GtkWidget *item_selection =
 		gtk_check_menu_item_new_with_mnemonic (_("_Follow selection"));
@@ -515,6 +813,7 @@ gui_main (char *argv[])
 	GtkWidget *menu = gtk_menu_new ();
 	gtk_widget_set_halign (menu, GTK_ALIGN_END);
 	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_open);
+	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_settings);
 #ifndef G_OS_WIN32
 	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_selection);
 #endif  // ! G_OS_WIN32
diff --git a/src/utils.c b/src/utils.c
index 0926848..ab5f2e7 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -222,7 +222,7 @@ load_project_config_file (GError **error)
 	//   which is completely undocumented
 	g_key_file_load_from_dirs (key_file,
 		PROJECT_NAME G_DIR_SEPARATOR_S PROJECT_NAME ".conf",
-		paths, NULL, 0, &e);
+		paths, NULL, G_KEY_FILE_KEEP_COMMENTS, &e);
 	g_free (paths);
 	if (!e)
 		return key_file;
@@ -236,6 +236,25 @@ load_project_config_file (GError **error)
 	return NULL;
 }
 
+gboolean
+save_project_config_file (GKeyFile *key_file, GError **error)
+{
+	gchar *dirname =
+		g_build_filename (g_get_user_config_dir (), PROJECT_NAME, NULL);
+	(void) g_mkdir_with_parents (dirname, 0755);
+	gchar *path = g_build_filename (dirname, PROJECT_NAME ".conf", NULL);
+	g_free (dirname);
+
+	gsize length = 0;
+	gchar *data = g_key_file_to_data (key_file, &length, error);
+	if (!data)
+		return FALSE;
+
+	gboolean result = g_file_set_contents (path, data, length, error);
+	g_free (data);
+	return result;
+}
+
 // --- Loading -----------------------------------------------------------------
 
 void
diff --git a/src/utils.h b/src/utils.h
index bc5775b..ccd660e 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -54,6 +54,7 @@ gchar *resolve_relative_config_filename (const gchar *filename);
 gchar *resolve_filename
 	(const gchar *filename, gchar *(*relative_cb) (const char *));
 GKeyFile *load_project_config_file (GError **error);
+gboolean save_project_config_file (GKeyFile *key_file, GError **error);
 
 // --- Loading -----------------------------------------------------------------
 
-- 
cgit v1.2.3-70-g09d2