diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2024-12-19 08:51:52 +0100 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2024-12-19 14:38:19 +0100 |
commit | 531f18d827dd515befa8a91f5ac80c6b3872498d (patch) | |
tree | 5cbfc9e79b0335d19141511b57df285f580ed51a /src | |
parent | 862cde36aeaf9b6fbaadc9ab32b95eaa3a8ba7f4 (diff) | |
download | tdv-531f18d827dd515befa8a91f5ac80c6b3872498d.tar.gz tdv-531f18d827dd515befa8a91f5ac80c6b3872498d.tar.xz tdv-531f18d827dd515befa8a91f5ac80c6b3872498d.zip |
GUI: add basic configuration
It is simply not feasible to write the text file by hand on Windows.
Diffstat (limited to 'src')
-rw-r--r-- | src/stardict.c | 22 | ||||
-rw-r--r-- | src/stardict.h | 1 | ||||
-rw-r--r-- | src/tdv-gui.c | 307 | ||||
-rw-r--r-- | src/utils.c | 21 | ||||
-rw-r--r-- | src/utils.h | 1 |
5 files changed, 342 insertions, 10 deletions
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<StardictInfo *>. Deallocate the list with: /// @code @@ -377,12 +391,10 @@ stardict_list_dictionaries (const gchar *path) continue; gchar *filename = g_build_filename (path, name, NULL); - StardictInfo *ifo = g_new (StardictInfo, 1); - if (load_ifo (ifo, filename, NULL)) - dicts = g_list_append (dicts, ifo); - else - g_free (ifo); + StardictInfo *ifo = stardict_info_new (filename, NULL); g_free (filename); + if (ifo) + dicts = g_list_append (dicts, ifo); } g_dir_close (dir); g_pattern_spec_free (ps); diff --git a/src/stardict.h b/src/stardict.h index 85fd396..79dde47 100644 --- a/src/stardict.h +++ b/src/stardict.h @@ -108,6 +108,7 @@ GQuark stardict_error_quark (void); // --- Dictionary information -------------------------------------------------- +StardictInfo *stardict_info_new (const gchar *filename, GError **error); const gchar *stardict_info_get_path (StardictInfo *sdi) G_GNUC_PURE; const gchar *stardict_info_get_book_name (StardictInfo *sdi) G_GNUC_PURE; gsize stardict_info_get_word_count (StardictInfo *sd) G_GNUC_PURE; diff --git a/src/tdv-gui.c b/src/tdv-gui.c index fcb254f..6c1cf59 100644 --- a/src/tdv-gui.c +++ b/src/tdv-gui.c @@ -1,7 +1,7 @@ /* * StarDict GTK+ UI * - * Copyright (c) 2020 - 2022, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2020 - 2024, Přemysl Eric Janouch <p@janouch.name> * * 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 ----------------------------------------------------------------- |