aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/sdgtk.c144
-rw-r--r--src/sdtui.c3
-rw-r--r--src/stardict-view.c526
-rw-r--r--src/stardict-view.h34
-rw-r--r--src/stardict.h4
5 files changed, 582 insertions, 129 deletions
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 ("&lt;%s&gt;", 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