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 | |
| 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.
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | README.adoc | 21 | ||||
| -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 | 
7 files changed, 351 insertions, 24 deletions
| @@ -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 ----------------------------------------------------------------- | 
