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 + * Copyright (c) 2020 - 2021, Přemysl Eric Janouch * * 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 @@ -85,99 +86,6 @@ init (gchar **filenames, GError **e) 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) { @@ -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 + * + * 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 "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, "%s%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 + * + * 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 + +#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