From aa198484997003294fe053d72e3f37471dca162f Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch 
Date: Mon, 7 Sep 2020 19:08:04 +0200
Subject: Add an experimental GTK+ UI
It has a potential to stay simpler than the TUI,
while having a wider feature set.
Not building this toy by default, it needs some time investment.
---
 CMakeLists.txt |   9 ++
 src/sdgtk.c    | 418 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 427 insertions(+)
 create mode 100644 src/sdgtk.c
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e9b75b0..8771632 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -179,6 +179,15 @@ add_executable (${PROJECT_NAME}
 	${project_sources} ${project_headers} ${project_common_sources})
 target_link_libraries (${PROJECT_NAME} ${project_common_libraries})
 
+# Experimental GTK+ frontend, we link it with ncurses but we don't care
+pkg_check_modules (gtk gtk+-3.0)
+if (gtk_FOUND)
+	add_executable (sdgtk EXCLUDE_FROM_ALL
+		src/sdgtk.c ${project_common_sources})
+	target_include_directories (sdgtk PUBLIC ${gtk_INCLUDE_DIRS})
+	target_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries})
+endif (gtk_FOUND)
+
 # Tools
 set (tools add-pronunciation query-tool transform)
 foreach (tool ${tools})
diff --git a/src/sdgtk.c b/src/sdgtk.c
new file mode 100644
index 0000000..30d2e57
--- /dev/null
+++ b/src/sdgtk.c
@@ -0,0 +1,418 @@
+/*
+ * StarDict GTK+ UI
+ *
+ * Copyright (c) 2020, Přemysl Eric Janouch 
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#include 
+#include 
+
+#include 
+
+#include "config.h"
+#include "stardict.h"
+#include "utils.h"
+
+typedef struct dictionary Dictionary;
+
+struct dictionary
+{
+	const gchar  *filename;          ///< Filename
+	StardictDict *dict;              ///< Stardict dictionary data
+	gchar        *name;              ///< Name to show
+	guint         position;          ///< Current position
+};
+
+static struct
+{
+	GtkWidget    *window;            ///< Top-level window
+	GtkWidget    *notebook;          ///< Notebook with tabs
+	GtkWidget    *entry;             ///< Search entry widget
+	GtkWidget    *grid;              ///< Entries container
+
+	gint          dictionary;        ///< Index of the current dictionary
+	Dictionary   *dictionaries;      ///< All open dictionaries
+	gsize         dictionaries_len;  ///< Total number of dictionaries
+
+	gboolean      watch_selection;   ///< Following X11 PRIMARY?
+}
+g;
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static gboolean
+dictionary_load (Dictionary *self, gchar *filename, GError **e)
+{
+	self->filename = filename;
+	if (!(self->dict = stardict_dict_new (self->filename, e)))
+		return FALSE;
+
+	if (!self->name)
+	{
+		self->name = g_strdup (stardict_info_get_book_name
+			(stardict_dict_get_info (self->dict)));
+	}
+	return TRUE;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static gboolean
+init (gchar **filenames, GError **e)
+{
+	while (filenames[g.dictionaries_len])
+		g.dictionaries_len++;
+
+	g.dictionaries = g_malloc0_n (sizeof *g.dictionaries, g.dictionaries_len);
+	for (gsize i = 0; i < g.dictionaries_len; i++)
+	{
+		Dictionary *dict = &g.dictionaries[i];
+		if (!dictionary_load (dict, filenames[i], e))
+			return FALSE;
+	}
+	return TRUE;
+}
+
+static void
+add_row (StardictIterator *iterator, gint row, gint *height_acc)
+{
+	Dictionary *dict = &g.dictionaries[g.dictionary];
+
+	StardictEntry *entry = stardict_iterator_get_entry (iterator);
+	g_return_if_fail (entry != NULL);
+	StardictEntryField *field = entry->fields->data;
+	g_return_if_fail (g_ascii_islower (field->type));
+
+	GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
+	const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
+	g_return_if_fail (input_utf8 != NULL);
+
+	const gchar *word_str = stardict_iterator_get_word (iterator);
+	gsize common_prefix = stardict_longest_common_collation_prefix
+		(dict->dict, word_str, input_utf8);
+	gchar *pre = g_markup_escape_text (word_str, common_prefix),
+		*post = g_markup_escape_text (word_str + common_prefix, -1),
+		*marked_up = g_strdup_printf ("%s%s", pre, post);
+
+	GtkWidget *word = gtk_label_new (marked_up);
+	gtk_label_set_use_markup (GTK_LABEL (word), TRUE);
+	gtk_label_set_ellipsize (GTK_LABEL (word), PANGO_ELLIPSIZE_END);
+	gtk_label_set_selectable (GTK_LABEL (word), TRUE);
+	gtk_label_set_xalign (GTK_LABEL (word), 0);
+	gtk_label_set_yalign (GTK_LABEL (word), 0);
+	// FIXME: they can't be deselected by just clicking outside of them
+	gtk_widget_set_can_focus (word, FALSE);
+
+	g_free (pre);
+	g_free (post);
+	g_free (marked_up);
+
+	GtkWidget *desc = gtk_label_new (field->data);
+	gtk_label_set_ellipsize (GTK_LABEL (desc), PANGO_ELLIPSIZE_END);
+	gtk_label_set_selectable (GTK_LABEL (desc), TRUE);
+	gtk_label_set_xalign (GTK_LABEL (desc), 0);
+	gtk_widget_set_can_focus (desc, FALSE);
+
+	g_object_unref (entry);
+
+	if (iterator->offset % 2 == 0)
+	{
+		GtkStyleContext *ctx;
+		ctx = gtk_widget_get_style_context (word);
+		gtk_style_context_add_class (ctx, "odd");
+		ctx = gtk_widget_get_style_context (desc);
+		gtk_style_context_add_class (ctx, "odd");
+	}
+
+	gtk_grid_attach (GTK_GRID (g.grid), word, 0, row, 1, 1);
+	gtk_grid_attach (GTK_GRID (g.grid), desc, 1, row, 1, 1);
+
+	gtk_widget_show (word);
+	gtk_widget_show (desc);
+
+	gint minimum_word = 0, minimum_desc = 0;
+	gtk_widget_get_preferred_height (word, &minimum_word, NULL);
+	gtk_widget_get_preferred_height (desc, &minimum_desc, NULL);
+	*height_acc += MAX (minimum_word, minimum_desc);
+}
+
+static void
+reload (GtkWidget *grid)
+{
+	Dictionary *dict = &g.dictionaries[g.dictionary];
+
+	GList *children = gtk_container_get_children (GTK_CONTAINER (grid));
+	for (GList *iter = children; iter != NULL; iter = g_list_next (iter))
+		gtk_widget_destroy (GTK_WIDGET (iter->data));
+	g_list_free (children);
+
+	gint window_height = 0;
+	gtk_window_get_size (GTK_WINDOW (g.window), NULL, &window_height);
+	if (window_height <= 0)
+		return;
+
+	StardictIterator *iterator =
+		stardict_iterator_new (dict->dict, dict->position);
+	gint row = 0, height_acc = 0;
+	while (stardict_iterator_is_valid (iterator))
+	{
+		add_row (iterator, row++, &height_acc);
+		if (height_acc >= window_height)
+			break;
+
+		stardict_iterator_next (iterator);
+	}
+	gtk_widget_show_all (grid);
+	g_object_unref (iterator);
+}
+
+static void
+search (Dictionary *dict)
+{
+	GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
+	const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
+
+	StardictIterator *iterator =
+		stardict_dict_search (dict->dict, input_utf8, NULL);
+	dict->position = stardict_iterator_get_offset (iterator);
+	g_object_unref (iterator);
+}
+
+static void
+on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
+{
+	search (&g.dictionaries[g.dictionary]);
+	reload (g.grid);
+}
+
+static void
+on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
+	G_GNUC_UNUSED gpointer data)
+{
+	if (!text)
+		return;
+
+	gtk_entry_set_text (GTK_ENTRY (g.entry), text);
+	g_signal_emit_by_name (g.entry,
+		"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
+}
+
+static void
+on_selection (GtkClipboard *clipboard, GdkEvent *event,
+	G_GNUC_UNUSED gpointer data)
+{
+	if (g.watch_selection
+	 && event->owner_change.owner != NULL)
+		gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
+}
+
+static void
+on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
+{
+	g.watch_selection = gtk_check_menu_item_get_active (item);
+}
+
+static void
+on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
+	guint page_num, G_GNUC_UNUSED gpointer data)
+{
+	g.dictionary = page_num;
+	search (&g.dictionaries[g.dictionary]);
+	reload (g.grid);
+}
+
+static gboolean
+on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
+	G_GNUC_UNUSED gpointer data)
+{
+	if (event->key.state == GDK_CONTROL_MASK)
+	{
+		if (event->key.keyval == GDK_KEY_Page_Up)
+		{
+			gtk_notebook_prev_page (GTK_NOTEBOOK (g.notebook));
+			return TRUE;
+		}
+		if (event->key.keyval == GDK_KEY_Page_Down)
+		{
+			gtk_notebook_next_page (GTK_NOTEBOOK (g.notebook));
+			return TRUE;
+		}
+	}
+	if (event->key.state == GDK_MOD1_MASK)
+	{
+		if (event->key.keyval >= GDK_KEY_0
+		 && event->key.keyval <= GDK_KEY_9)
+		{
+			gint n = event->key.keyval - GDK_KEY_0;
+			gtk_notebook_set_current_page
+				(GTK_NOTEBOOK (g.notebook), n ? (n - 1) : 10);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+static void
+on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
+{
+	gtk_main_quit ();
+}
+
+static void
+die_with_dialog (const gchar *message)
+{
+	GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
+		GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
+	gtk_dialog_run (GTK_DIALOG (dialog));
+	gtk_widget_destroy (dialog);
+	exit (EXIT_FAILURE);
+}
+
+int
+main (int argc, char *argv[])
+{
+	if (!setlocale (LC_ALL, ""))
+		g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
+
+	bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
+	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+	textdomain (GETTEXT_PACKAGE);
+
+	gchar **filenames = NULL;
+	GOptionEntry option_entries[] =
+	{
+		{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
+			NULL, N_("FILE...")},
+		{},
+	};
+
+	GError *error = NULL;
+	gtk_init_with_args (&argc, &argv, N_("- StarDict GTK+ UI"),
+		option_entries, GETTEXT_PACKAGE, &error);
+	if (error)
+	{
+		g_warning ("%s", error->message);
+		g_error_free (error);
+		return 1;
+	}
+
+	if (!filenames)
+	{
+		// TODO: eventually just load all dictionaries from configuration
+		die_with_dialog ("No arguments have been passed.");
+	}
+	if (!init (filenames, &error))
+		die_with_dialog (error->message);
+
+	// Some Adwaita stupidity and our own additions
+	const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
+		"grid { border-top: 1px solid rgba(0, 0, 0, 0.2); background: white; }"
+		"grid label { padding: 0 5px; "
+			"/*border-bottom: 1px solid rgba(0, 0, 0, 0.2);*/ }"
+		"grid label.odd { background: rgba(0, 0, 0, 0.05); }";
+
+	GdkScreen *screen = gdk_screen_get_default ();
+	GtkCssProvider *provider = gtk_css_provider_new ();
+	gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
+	gtk_style_context_add_provider_for_screen (screen,
+		GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+	g.grid = gtk_grid_new ();
+	gtk_grid_set_column_homogeneous (GTK_GRID (g.grid), TRUE);
+
+	// FIXME: we'd rather like to trim the contents, not make it scrollable.
+	// This just limits the allocation.
+	// TODO: probably create a whole new custom widget, everything is text
+	// anyway and mostly handled by Pango, including pango_layout_xy_to_index()
+	//  - I don't know where to get selection colour but inversion works, too
+	GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
+		GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL);
+	gtk_widget_set_can_focus (scrolled_window, FALSE);
+	gtk_container_add (GTK_CONTAINER (scrolled_window), g.grid);
+
+	g.notebook = gtk_notebook_new ();
+	g_signal_connect (g.notebook, "switch-page",
+		G_CALLBACK (on_switch_page), NULL);
+	gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
+
+	g.watch_selection = TRUE;
+	GtkWidget *item =
+		gtk_check_menu_item_new_with_label (_("Follow selection"));
+	gtk_check_menu_item_set_active
+		(GTK_CHECK_MENU_ITEM (item), g.watch_selection);
+	g_signal_connect (item, "toggled",
+		G_CALLBACK (on_selection_watch_toggle), NULL);
+
+	GtkWidget *menu = gtk_menu_new ();
+	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+	gtk_widget_show_all (menu);
+
+	GtkWidget *hamburger = gtk_menu_button_new ();
+	gtk_menu_button_set_direction (GTK_MENU_BUTTON (hamburger), GTK_ARROW_NONE);
+	gtk_menu_button_set_popup (GTK_MENU_BUTTON (hamburger), menu);
+	gtk_button_set_relief (GTK_BUTTON (hamburger), GTK_RELIEF_NONE);
+	gtk_widget_show (hamburger);
+
+	gtk_notebook_set_action_widget
+		(GTK_NOTEBOOK (g.notebook), hamburger, GTK_PACK_END);
+
+	// FIXME: when the clear icon shows, the widget changes in height
+	g.entry = gtk_search_entry_new ();
+	// TODO: attach to the "key-press-event" signal and implement ^W at least,
+	// though ^U is working already!  Note that bindings can be done in CSS
+	// as well, if we have any extra specially for the editor
+	g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.grid);
+	gtk_entry_set_has_frame (GTK_ENTRY (g.entry), FALSE);
+
+	// TODO: supposedly attach to "key-press-event" here and react to
+	// PageUp/PageDown and up/down arrow keys... either here or in the Entry
+	g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+	g_signal_connect (g.window, "destroy",
+		G_CALLBACK (on_destroy), NULL);
+	g_signal_connect (g.window, "key-press-event",
+		G_CALLBACK (on_key_press), NULL);
+	GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
+	gtk_container_add (GTK_CONTAINER (g.window), superbox);
+	gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
+	gtk_container_add (GTK_CONTAINER (superbox), g.entry);
+	gtk_box_pack_end (GTK_BOX (superbox), scrolled_window, TRUE, TRUE, 0);
+
+	for (gsize i = 0; i < g.dictionaries_len; i++)
+	{
+		Dictionary *dict = &g.dictionaries[i];
+		GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+		GtkWidget *label = gtk_label_new (dict->name);
+		gtk_notebook_append_page (GTK_NOTEBOOK (g.notebook), dummy, label);
+	}
+
+	GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
+	g_signal_connect (clipboard, "owner-change",
+		G_CALLBACK (on_selection), NULL);
+
+	// Make sure to fill up the window with entries once we're resized
+	// XXX: this is rather inefficient as we rebuild everything each time
+	g_signal_connect (g.window, "configure-event",
+		G_CALLBACK (on_changed), NULL);
+	g_signal_connect (g.window, "map-event",
+		G_CALLBACK (on_changed), NULL);
+
+	gtk_widget_grab_focus (g.entry);
+	gtk_widget_show_all (g.window);
+	gtk_main ();
+
+	g_strfreev (filenames);
+	return 0;
+}
-- 
cgit v1.2.3-70-g09d2