diff options
| -rw-r--r-- | CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/sdgtk.c | 144 | ||||
| -rw-r--r-- | src/sdtui.c | 3 | ||||
| -rw-r--r-- | src/stardict-view.c | 526 | ||||
| -rw-r--r-- | src/stardict-view.h | 34 | ||||
| -rw-r--r-- | src/stardict.h | 4 | 
6 files changed, 585 insertions, 130 deletions
| diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a8e100..f661cbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,7 +157,9 @@ target_link_libraries (${PROJECT_NAME} ${project_common_libraries})  pkg_check_modules (gtk gtk+-3.0)  if (gtk_FOUND)  	add_executable (sdgtk EXCLUDE_FROM_ALL -		src/sdgtk.c ${project_common_sources}) +		src/sdgtk.c +		src/stardict-view.c +		${project_common_sources})  	target_include_directories (sdgtk PUBLIC ${gtk_INCLUDE_DIRS})  	target_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries})  endif () diff --git a/src/sdgtk.c b/src/sdgtk.c index c104f38..3565bc6 100644 --- a/src/sdgtk.c +++ b/src/sdgtk.c @@ -1,7 +1,7 @@  /*   * StarDict GTK+ UI   * - * Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2020 - 2021, 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. @@ -24,6 +24,7 @@  #include "config.h"  #include "stardict.h"  #include "utils.h" +#include "stardict-view.h"  typedef struct dictionary Dictionary; @@ -40,7 +41,7 @@ static struct  	GtkWidget    *window;            ///< Top-level window  	GtkWidget    *notebook;          ///< Notebook with tabs  	GtkWidget    *entry;             ///< Search entry widget -	GtkWidget    *grid;              ///< Entries container +	GtkWidget    *view;              ///< Entries view  	gint          dictionary;        ///< Index of the current dictionary  	Dictionary   *dictionaries;      ///< All open dictionaries @@ -86,99 +87,6 @@ init (gchar **filenames, GError **e)  }  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 ("<u>%s</u>%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)); @@ -188,13 +96,16 @@ search (Dictionary *dict)  		stardict_dict_search (dict->dict, input_utf8, NULL);  	dict->position = stardict_iterator_get_offset (iterator);  	g_object_unref (iterator); + +	stardict_view_set_position (STARDICT_VIEW (g.view), +		dict->dict, dict->position); +	stardict_view_set_matched (STARDICT_VIEW (g.view), input_utf8);  }  static void  on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)  {  	search (&g.dictionaries[g.dictionary]); -	reload (g.grid);  }  static void @@ -231,7 +142,6 @@ on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,  {  	g.dictionary = page_num;  	search (&g.dictionaries[g.dictionary]); -	reload (g.grid);  }  static gboolean @@ -317,12 +227,8 @@ main (int argc, char *argv[])  	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); }"; +	// Some Adwaita stupidity +	const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }";  	GdkScreen *screen = gdk_screen_get_default ();  	GtkCssProvider *provider = gtk_css_provider_new (); @@ -330,20 +236,6 @@ main (int argc, char *argv[])  	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); @@ -375,21 +267,28 @@ main (int argc, char *argv[])  	// 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); +	g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view); +	// TODO: make the entry have a background colour, rather than transparency  	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); +	gtk_window_set_default_size (GTK_WINDOW (g.window), 300, 600);  	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); +	gtk_container_add (GTK_CONTAINER (superbox), +		gtk_separator_new (GTK_ORIENTATION_HORIZONTAL)); + +	g.view = stardict_view_new (); +	gtk_box_pack_end (GTK_BOX (superbox), g.view, TRUE, TRUE, 0);  	for (gsize i = 0; i < g.dictionaries_len; i++)  	{ @@ -403,13 +302,6 @@ main (int argc, char *argv[])  	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 (); diff --git a/src/sdtui.c b/src/sdtui.c index d64f1d1..3be34f4 100644 --- a/src/sdtui.c +++ b/src/sdtui.c @@ -230,7 +230,7 @@ struct application  	guint32         top_position;       ///< Index of the topmost dict. entry  	guint           top_offset;         ///< Offset into the top entry  	guint           selected;           ///< Offset to the selected definition -	GPtrArray     * entries;            ///< ViewEntry's within the view +	GPtrArray     * entries;            ///< ViewEntry-s within the view  	gchar         * search_label;       ///< Text of the "Search" label  	GArray        * input;              ///< The current search input @@ -386,6 +386,7 @@ view_entry_new (StardictIterator *iterator)  			found_anything_displayable = TRUE;  			break;  		case STARDICT_FIELD_PHONETIC: +			// FIXME this makes it highlightable  			g_string_append_printf (word, " /%s/", (const gchar *) field->data);  			break;  		default: diff --git a/src/stardict-view.c b/src/stardict-view.c new file mode 100644 index 0000000..884f2e8 --- /dev/null +++ b/src/stardict-view.c @@ -0,0 +1,526 @@ +/* + * StarDict GTK+ UI - dictionary view component + * + * Copyright (c) 2021, 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. + * + * 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 <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "stardict-view.h" +#include "utils.h" + + +typedef struct view_entry ViewEntry; + +struct view_entry +{ +	gchar *word;                        ///< The word, in Pango markup +	gchar *definition;                  ///< Definition lines, in Pango markup + +	PangoLayout *word_layout;           ///< Ellipsized one-line layout or NULL +	PangoLayout *definition_layout;     ///< Multiline layout or NULL +}; + +static void +view_entry_destroy (ViewEntry *self) +{ +	g_free (self->word); +	g_free (self->definition); +	g_clear_object (&self->word_layout); +	g_clear_object (&self->definition_layout); +	g_slice_free1 (sizeof *self, self); +} + +static ViewEntry * +view_entry_new (StardictIterator *iterator, const gchar *matched) +{ +	g_return_val_if_fail (stardict_iterator_is_valid (iterator), NULL); + +	StardictEntry *entry = stardict_iterator_get_entry (iterator); +	g_return_val_if_fail (entry != NULL, NULL); + +	// Highlighting may change the rendition, so far it's easiest to recompute +	// it on each search field change by rebuilding the list of view entries. +	// The phonetics suffix would need to be stored separately. +	const gchar *word = stardict_iterator_get_word (iterator); +	gsize common_prefix = stardict_longest_common_collation_prefix +		(iterator->owner, word, matched); + +	ViewEntry *ve = g_slice_alloc0 (sizeof *ve); + +	GString *adjusted_word = g_string_new (""); +	gchar *pre = g_markup_escape_text (word, common_prefix); +	gchar *post = g_markup_escape_text (word + common_prefix, -1); +	g_string_printf (adjusted_word, "<u>%s</u>%s", pre, post); +	g_free (pre); +	g_free (post); + +	GPtrArray *definitions = g_ptr_array_new_full (2, g_free); +	for (const GList *fields = stardict_entry_get_fields (entry); fields; ) +	{ +		const StardictEntryField *field = fields->data; +		switch (field->type) +		{ +		case STARDICT_FIELD_MEANING: +			g_ptr_array_add (definitions, +				g_markup_escape_text (field->data, -1)); +			break; +		case STARDICT_FIELD_PANGO: +			g_ptr_array_add (definitions, g_strdup (field->data)); +			break; +		case STARDICT_FIELD_XDXF: +			g_ptr_array_add (definitions, +				xdxf_to_pango_markup_with_reduced_effort (field->data)); +			break; +		case STARDICT_FIELD_PHONETIC: +		{ +			gchar *escaped = g_markup_escape_text (field->data, -1); +			g_string_append_printf (adjusted_word, " /%s/", escaped); +			g_free (escaped); +			break; +		} +		default: +			// TODO: support more of them +			break; +		} +		fields = fields->next; +	} +	g_object_unref (entry); + +	ve->word = g_string_free (adjusted_word, FALSE); +	if (!definitions->len) +	{ +		gchar *message = g_markup_escape_text (_("no usable field found"), -1); +		g_ptr_array_add (definitions, g_strdup_printf ("<%s>", message)); +		g_free (message); +	} + +	g_ptr_array_add (definitions, NULL); +	ve->definition = g_strjoinv ("\n", (gchar **) definitions->pdata); +	g_ptr_array_free (definitions, TRUE); +	return ve; +} + +static gint +view_entry_height (ViewEntry *ve, gint *word_offset, gint *defn_offset) +{ +	gint word_w = 0, word_h = 0; +	gint defn_w = 0, defn_h = 0; +	pango_layout_get_pixel_size (ve->word_layout,       &word_w, &word_h); +	pango_layout_get_pixel_size (ve->definition_layout, &defn_w, &defn_h); + +	// Align baselines, without further considerations +	gint wb = pango_layout_get_baseline (ve->word_layout)       / PANGO_SCALE; +	gint db = pango_layout_get_baseline (ve->definition_layout) / PANGO_SCALE; +	gint word_y = MAX (0, db - wb); +	gint defn_y = MAX (0, wb - db); + +	if (word_offset) +		*word_offset = word_y; +	if (defn_offset) +		*defn_offset = defn_y; + +	return MAX (word_y + word_h, defn_y + defn_h); +} + +#define PADDING 5 + +static gint +view_entry_draw (ViewEntry *ve, cairo_t *cr, gint full_width, gboolean even) +{ +	// TODO: this shouldn't be hardcoded, read it out from somewhere +	gdouble g = even ? 1. : .95; + +	gint word_y = 0, defn_y = 0, +		height = view_entry_height (ve, &word_y, &defn_y); +	cairo_rectangle (cr, 0, 0, full_width, height); +	cairo_set_source_rgb (cr, g, g, g); +	cairo_fill (cr); + +	cairo_set_source_rgb (cr, 0, 0, 0); +	cairo_move_to (cr, full_width / 2 + PADDING, defn_y); +	pango_cairo_show_layout (cr, ve->definition_layout); + +	PangoLayoutIter *iter = pango_layout_get_iter (ve->definition_layout); +	do +	{ +		if (!pango_layout_iter_get_line_readonly (iter)->is_paragraph_start) +			continue; + +		PangoRectangle logical = {}; +		pango_layout_iter_get_line_extents (iter, NULL, &logical); +		cairo_move_to (cr, PADDING, word_y + logical.y / PANGO_SCALE); +		pango_cairo_show_layout (cr, ve->word_layout); +	} +	while (pango_layout_iter_next_line (iter)); +	pango_layout_iter_free (iter); +	return height; +} + +static void +view_entry_rebuild_layout (ViewEntry *ve, PangoContext *pc, gint width) +{ +	g_clear_object (&ve->word_layout); +	g_clear_object (&ve->definition_layout); + +	int left_width = width / 2 - 2 * PADDING; +	int right_width = width - left_width - 2 * PADDING; +	if (left_width < 1 || right_width < 1) +		return; + +	// TODO: preferably pre-validate the layouts with pango_parse_markup(), +	//   so that it doesn't warn without indication on the frontend +	ve->word_layout = pango_layout_new (pc); +	pango_layout_set_markup (ve->word_layout, ve->word, -1); +	pango_layout_set_ellipsize (ve->word_layout, PANGO_ELLIPSIZE_END); +	pango_layout_set_single_paragraph_mode (ve->word_layout, TRUE); +	pango_layout_set_width (ve->word_layout, PANGO_SCALE * left_width); + +	ve->definition_layout = pango_layout_new (pc); +	pango_layout_set_markup (ve->definition_layout, ve->definition, -1); +	pango_layout_set_width (ve->definition_layout, PANGO_SCALE * right_width); +} + +// --- Widget ------------------------------------------------------------------ + +struct _StardictView +{ +	GtkWidget parent_instance; + +	StardictDict *dict;                 ///< The displayed dictionary +	guint top_position;                 ///< Index of the topmost dict. entry +	gchar *matched;                     ///< Highlight common word part of this + +	gint top_offset;                    ///< Pixel offset into the entry +	// TODO: think about making it, e.g., a pair of (ViewEntry *, guint) +	// NOTE: this is the index of a Pango paragraph (a virtual entity) +	guint selected;                     ///< Offset to the selected definition +	GList *entries;                     ///< ViewEntry-s within the view +}; + +static void +adjust_for_offset (StardictView *self) +{ +	// FIXME: lots of code duplication with reload(), could be refactored +	GtkWidget *widget = GTK_WIDGET (self); +	PangoContext *pc = gtk_widget_get_pango_context (widget); +	const gchar *matched = self->matched ? self->matched : ""; + +	GtkAllocation allocation = {}; +	gtk_widget_get_allocation (widget, &allocation); + +	// If scrolled way up, prepend entries so long as it's possible +	StardictIterator *iterator = +		stardict_iterator_new (self->dict, self->top_position); +	while (self->top_offset < 0) +	{ +		stardict_iterator_prev (iterator); +		if (!stardict_iterator_is_valid (iterator)) +		{ +			self->top_offset = 0; +			break; +		} + +		self->top_position = stardict_iterator_get_offset (iterator); +		ViewEntry *ve = view_entry_new (iterator, matched); +		view_entry_rebuild_layout (ve, pc, allocation.width); +		self->top_offset += view_entry_height (ve, NULL, NULL); +		self->entries = g_list_prepend (self->entries, ve); +	} +	g_object_unref (iterator); + +	// If scrolled way down, drop leading entries so long as it's possible +	while (self->entries) +	{ +		gint height = view_entry_height (self->entries->data, NULL, NULL); +		if (self->top_offset < height) +			break; + +		self->top_offset -= height; +		view_entry_destroy (self->entries->data); +		self->entries = g_list_delete_link (self->entries, self->entries); +		self->top_position++; + +	} +	if (self->top_offset && !self->entries) +		self->top_offset = 0; + +	// Load replacement trailing entries, or drop those no longer visible +	iterator = stardict_iterator_new (self->dict, self->top_position); +	gint used = -self->top_offset; +	for (GList *iter = self->entries, *next; +		 next = g_list_next (iter), iter; iter = next) +	{ +		if (used < allocation.height) +			used += view_entry_height (iter->data, NULL, NULL); +		else +		{ +			view_entry_destroy (iter->data); +			self->entries = g_list_delete_link (self->entries, iter); +		} +		stardict_iterator_next (iterator); +	} +	while (used < allocation.height && stardict_iterator_is_valid (iterator)) +	{ +		ViewEntry *ve = view_entry_new (iterator, matched); +		view_entry_rebuild_layout (ve, pc, allocation.width); +		used += view_entry_height (ve, NULL, NULL); +		self->entries = g_list_append (self->entries, ve); +		stardict_iterator_next (iterator); +	} +	g_object_unref (iterator); + +	gtk_widget_queue_draw (widget); +} + +static void +reload (StardictView *self) +{ +	g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy); +	self->entries = NULL; + +	GtkWidget *widget = GTK_WIDGET (self); +	if (!gtk_widget_get_realized (widget) || !self->dict) +		return; + +	GtkAllocation allocation = {}; +	gtk_widget_get_allocation (widget, &allocation); + +	PangoContext *pc = gtk_widget_get_pango_context (widget); +	StardictIterator *iterator = +		stardict_iterator_new (self->dict, self->top_position); + +	gint used = 0; +	const gchar *matched = self->matched ? self->matched : ""; +	while (used < allocation.height && stardict_iterator_is_valid (iterator)) +	{ +		ViewEntry *ve = view_entry_new (iterator, matched); +		view_entry_rebuild_layout (ve, pc, allocation.width); +		used += view_entry_height (ve, NULL, NULL); +		self->entries = g_list_prepend (self->entries, ve); +		stardict_iterator_next (iterator); +	} +	g_object_unref (iterator); +	self->entries = g_list_reverse (self->entries); + +	// Right now, we're being lazy--this could be integrated here +	adjust_for_offset (self); + +	gtk_widget_queue_draw (widget); +} + +// --- Boilerplate ------------------------------------------------------------- + +G_DEFINE_TYPE (StardictView, stardict_view, GTK_TYPE_WIDGET) + +static void +stardict_view_finalize (GObject *gobject) +{ +	StardictView *self = STARDICT_VIEW (gobject); +	g_clear_object (&self->dict); + +	g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy); +	self->entries = NULL; + +	g_free (self->matched); +	self->matched = NULL; + +	G_OBJECT_CLASS (stardict_view_parent_class)->finalize (gobject); +} + +static void +stardict_view_get_preferred_height (GtkWidget *widget, +	gint *minimum, gint *natural) +{ +	PangoLayout *layout = gtk_widget_create_pango_layout (widget, NULL); + +	gint width = 0, height = 0; +	pango_layout_get_pixel_size (layout, &width, &height); +	g_object_unref (layout); + +	// There isn't any value that would make any real sense +	if (!STARDICT_VIEW (widget)->dict) +		*natural = *minimum = 0; +	else +		*natural = *minimum = height; +} + +static void +stardict_view_get_preferred_width (GtkWidget *widget G_GNUC_UNUSED, +	gint *minimum, gint *natural) +{ +	*natural = *minimum = 4 * PADDING; +} + +static void +stardict_view_realize (GtkWidget *widget) +{ +	GtkAllocation allocation; +	gtk_widget_get_allocation (widget, &allocation); + +	GdkWindowAttr attributes = +	{ +		.window_type = GDK_WINDOW_CHILD, +		.x           = allocation.x, +		.y           = allocation.y, +		.width       = allocation.width, +		.height      = allocation.height, + +		// Input-only would presumably also work (as in GtkPathBar, e.g.), +		// but it merely seems to involve more work. +		.wclass      = GDK_INPUT_OUTPUT, + +		.visual      = gtk_widget_get_visual (widget), +		.event_mask  = gtk_widget_get_events (widget) | GDK_SCROLL_MASK, +	}; + +	// We need this window to receive input events at all. +	// TODO: see if we don't want GDK_WA_CURSOR for setting a text cursor +	GdkWindow *window = gdk_window_new (gtk_widget_get_parent_window (widget), +		&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + +	// The default background colour of the GDK window is transparent, +	// we'll keep it that way, rather than apply the style context. + +	gtk_widget_register_window (widget, window); +	gtk_widget_set_window (widget, window); +	gtk_widget_set_realized (widget, TRUE); +} + +static gboolean +stardict_view_draw (GtkWidget *widget, cairo_t *cr) +{ +	StardictView *self = STARDICT_VIEW (widget); + +	GtkAllocation allocation; +	gtk_widget_get_allocation (widget, &allocation); + +	gint offset = -self->top_offset; +	gint i = self->top_position; +	for (GList *iter = self->entries; iter; iter = iter->next) +	{ +		cairo_save (cr); +		cairo_translate (cr, 0, offset); +		// TODO: later exclude clipped entries, but it's not that important +		offset += view_entry_draw (iter->data, cr, allocation.width, i++ & 1); +		cairo_restore (cr); +	} +	return TRUE; +} + +static void +stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ +	GTK_WIDGET_CLASS (stardict_view_parent_class) +		->size_allocate (widget, allocation); + +	reload (STARDICT_VIEW (widget)); +} + +static void +stardict_view_screen_changed (GtkWidget *widget, G_GNUC_UNUSED GdkScreen *prev) +{ +	// Update the minimum size +	gtk_widget_queue_resize (widget); + +	// Recreate Pango layouts +	reload (STARDICT_VIEW (widget)); +} + +static gboolean +stardict_view_scroll_event (GtkWidget *widget, GdkEventScroll *event) +{ +	// TODO: rethink the notes here to rather iterate over /definition lines/ +	//  - iterate over selected lines, maybe one, maybe three +	StardictView *self = STARDICT_VIEW (widget); +	if (!self->dict) +		return FALSE; + +	switch (event->direction) +	{ +	case GDK_SCROLL_UP: +		self->top_offset -= 50; +		adjust_for_offset (self); +		return TRUE; +	case GDK_SCROLL_DOWN: +		self->top_offset += 50; +		adjust_for_offset (self); +		return TRUE; +	default: +		// GDK_SCROLL_SMOOTH doesn't fit the intended way of usage +		return FALSE; +	} +} + +static void +stardict_view_class_init (StardictViewClass *klass) +{ +	GObjectClass *object_class = G_OBJECT_CLASS (klass); +	object_class->finalize = stardict_view_finalize; + +	// TODO: handle mouse events for text selection +	// See https://wiki.gnome.org/HowDoI/CustomWidgets for some guidelines. +	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); +	widget_class->get_preferred_height = stardict_view_get_preferred_height; +	widget_class->get_preferred_width = stardict_view_get_preferred_width; +	widget_class->realize = stardict_view_realize; +	widget_class->draw = stardict_view_draw; +	widget_class->size_allocate = stardict_view_size_allocate; +	widget_class->screen_changed = stardict_view_screen_changed; +	widget_class->scroll_event = stardict_view_scroll_event; +} + +static void +stardict_view_init (G_GNUC_UNUSED StardictView *self) +{ +} + +// --- Public ------------------------------------------------------------------ + +GtkWidget * +stardict_view_new (void) +{ +	return GTK_WIDGET (g_object_new (STARDICT_TYPE_VIEW, NULL)); +} + +void +stardict_view_set_position (StardictView *self, +	StardictDict *dict, guint position) +{ +	g_return_if_fail (STARDICT_IS_VIEW (self)); +	g_return_if_fail (STARDICT_IS_DICT (dict)); + +	// Update the minimum size, if appropriate (almost never) +	if (!self->dict != !dict) +		gtk_widget_queue_resize (GTK_WIDGET (self)); + +	g_clear_object (&self->dict); +	self->dict = g_object_ref (dict); +	self->top_position = position; +	self->top_offset = 0; + +	reload (self); +} + +void +stardict_view_set_matched (StardictView *self, const gchar *matched) +{ +	g_return_if_fail (STARDICT_IS_VIEW (self)); + +	g_free (self->matched); +	self->matched = g_strdup (matched); +	reload (self); +} diff --git a/src/stardict-view.h b/src/stardict-view.h new file mode 100644 index 0000000..206238b --- /dev/null +++ b/src/stardict-view.h @@ -0,0 +1,34 @@ +/* + * StarDict GTK+ UI - dictionary view component + * + * Copyright (c) 2021, 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. + * + * 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. + * + */ + +#ifndef STARDICT_VIEW_H +#define STARDICT_VIEW_H + +#include <gtk/gtk.h> + +#include "stardict.h" + +#define STARDICT_TYPE_VIEW  (stardict_view_get_type ()) +G_DECLARE_FINAL_TYPE (StardictView, stardict_view, STARDICT, VIEW, GtkWidget) + +GtkWidget *stardict_view_new (void); +void stardict_view_set_position (StardictView *view, +	StardictDict *dict, guint position); +void stardict_view_set_matched (StardictView *view, const gchar *matched); + +#endif  // ! STARDICT_VIEW_H diff --git a/src/stardict.h b/src/stardict.h index 5ebccde..85fd396 100644 --- a/src/stardict.h +++ b/src/stardict.h @@ -198,7 +198,7 @@ struct stardict_entry_field  struct stardict_entry  {  	GObject         parent_instance; -	GList         * fields;             ///< List of StardictEntryField's +	GList         * fields;             ///< List of StardictEntryField-s  };  struct stardict_entry_class @@ -209,4 +209,4 @@ struct stardict_entry_class  GType stardict_entry_get_type (void);  const GList *stardict_entry_get_fields (StardictEntry *sde) G_GNUC_PURE; - #endif  // ! STARDICT_H +#endif  // ! STARDICT_H | 
