aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE2
-rw-r--r--README.adoc21
-rw-r--r--src/stardict.c22
-rw-r--r--src/stardict.h1
-rw-r--r--src/tdv-gui.c307
-rw-r--r--src/utils.c21
-rw-r--r--src/utils.h1
7 files changed, 351 insertions, 24 deletions
diff --git a/LICENSE b/LICENSE
index 7f1d888..04bc6cb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013 - 2023, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2013 - 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.
diff --git a/README.adoc b/README.adoc
index 461236e..b77956b 100644
--- a/README.adoc
+++ b/README.adoc
@@ -6,14 +6,15 @@ of StarDict dictionaries, and is inspired by the dictionary component
of PC Translator. I was unsuccessful in finding any free software of this kind,
and thus decided to write my own.
-The project is covered by a permissive license, unlike vast majority of other
-similar projects, and can serve as a base for implementing other dictionary
-software.
+The program offers both a terminal user interface, and a GTK+ 3 based UI.
+The styling of the latter will follow your theme, and may be customized
+from 'gtk.css'.
image::tdv.png[align="center"]
-As a recent addition, the program also offers a GTK+ 3 based user interface,
-whose styling will follow your theme, and may be customized from 'gtk.css'.
+The project is covered by a permissive license, unlike vast majority of other
+similar projects, and can serve as a base for implementing other dictionary
+software.
Packages
--------
@@ -92,14 +93,8 @@ https://mega.co.nz/#!axtD0QRK!sbtBgizksyfkPqKvKEgr8GQ11rsWhtqyRgUUV0B7pwg[CZ <--
Further Development
-------------------
-While I've been successfully using 'tdv' for many years now, some issues
-should be addressed before including the software in regular Linux and/or
-BSD distributions:
-
- - The GUI is awkward to configure.
- - Lacking configuration, standard StarDict locations should be scanned.
-
-Given all issues with the file format, it might be better to start anew.
+Lacking configuration, standard StarDict locations should be scanned.
+We should try harder to display arbitrary dictionaries sensibly.
Contributing and Support
------------------------
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 -----------------------------------------------------------------