/* * StarDict GTK+ UI - dictionary view component * * Copyright (c) 2021 - 2022, 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 gsize word_matched; ///< Initial matching bytes of the word 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); GString *adjusted_word = g_string_new (word); 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: g_string_append_printf (adjusted_word, " /%s/", (const char *) field->data); default: // TODO: support more of them break; } fields = fields->next; } g_object_unref (entry); ViewEntry *ve = g_slice_alloc0 (sizeof *ve); ve->word = g_string_free (adjusted_word, FALSE); ve->word_matched = stardict_longest_common_collation_prefix (iterator->owner, word, matched); 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); gint db = pango_layout_get_baseline (ve->definition_layout); gint word_y = MAX (0, PANGO_PIXELS (db - wb)); gint defn_y = MAX (0, PANGO_PIXELS (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); } static GtkBorder view_entry_get_padding (GtkStyleContext *style) { GtkBorder padding = {}; GtkStateFlags state = gtk_style_context_get_state (style); gtk_style_context_get_padding (style, state, &padding); return padding; } typedef struct view_entry_render_ctx ViewEntryRenderCtx; // TODO: see if we can't think of a cleaner way of doing this struct view_entry_render_ctx { GtkStyleContext *style; cairo_t *cr; int width; int height; // Forwarded from StardictView PangoLayout *selection_layout; int selection_begin; int selection_end; PangoLayout *hover_layout; int hover_begin; int hover_end; }; static PangoLayout * view_entry_adjust_layout (ViewEntryRenderCtx *ctx, PangoLayout *layout) { if (layout != ctx->hover_layout) return g_object_ref (layout); layout = pango_layout_copy (layout); PangoAttrList *attrs = pango_layout_get_attributes (layout); attrs = attrs ? pango_attr_list_copy (attrs) : pango_attr_list_new (); PangoAttribute *u = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); u->start_index = ctx->hover_begin; u->end_index = ctx->hover_end; pango_attr_list_change (attrs, u); PangoAttribute *uc = pango_attr_underline_color_new (0, 0, 0xffff); uc->start_index = ctx->hover_begin; uc->end_index = ctx->hover_end; pango_attr_list_change (attrs, uc); PangoAttribute *c = pango_attr_foreground_new (0, 0, 0xffff); c->start_index = ctx->hover_begin; c->end_index = ctx->hover_end; pango_attr_list_change (attrs, c); pango_layout_set_attributes (layout, attrs); pango_attr_list_unref (attrs); return layout; } static void view_entry_render (ViewEntryRenderCtx *ctx, gdouble x, gdouble y, PangoLayout *layout) { PangoLayout *adjusted = view_entry_adjust_layout (ctx, layout); gtk_render_layout (ctx->style, ctx->cr, x, y, adjusted); if (layout != ctx->selection_layout) goto out; gtk_style_context_save (ctx->style); gtk_style_context_set_state (ctx->style, GTK_STATE_FLAG_SELECTED); cairo_save (ctx->cr); int ranges[2] = { MIN (ctx->selection_begin, ctx->selection_end), MAX (ctx->selection_begin, ctx->selection_end) }; cairo_region_t *region = gdk_pango_layout_get_clip_region (adjusted, x, y, ranges, 1); gdk_cairo_region (ctx->cr, region); cairo_clip (ctx->cr); cairo_region_destroy (region); gtk_render_background (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height); gtk_render_layout (ctx->style, ctx->cr, x, y, adjusted); cairo_restore (ctx->cr); gtk_style_context_restore (ctx->style); out: g_object_unref (adjusted); } static gint view_entry_draw (ViewEntry *ve, ViewEntryRenderCtx *ctx) { gint word_y = 0, defn_y = 0; ctx->height = view_entry_height (ve, &word_y, &defn_y); gtk_render_background (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height); gtk_render_frame (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height); // Top/bottom and left/right-dependent padding will not work, too much code GtkBorder padding = view_entry_get_padding (ctx->style); gtk_style_context_save (ctx->style); gtk_style_context_add_class (ctx->style, GTK_STYLE_CLASS_RIGHT); view_entry_render (ctx, ctx->width / 2 + padding.left, defn_y, ve->definition_layout); gtk_style_context_restore (ctx->style); gtk_style_context_save (ctx->style); gtk_style_context_add_class (ctx->style, GTK_STYLE_CLASS_LEFT); 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); view_entry_render (ctx, padding.left, word_y + PANGO_PIXELS (logical.y), ve->word_layout); } while (pango_layout_iter_next_line (iter)); pango_layout_iter_free (iter); gtk_style_context_restore (ctx->style); return ctx->height; } static void view_entry_rebuild_layouts (ViewEntry *ve, GtkWidget *widget) { PangoContext *pc = gtk_widget_get_pango_context (widget); GtkStyleContext *style = gtk_widget_get_style_context (widget); gint full_width = gtk_widget_get_allocated_width (widget); g_clear_object (&ve->word_layout); g_clear_object (&ve->definition_layout); GtkBorder padding = view_entry_get_padding (style); gint part_width = full_width / 2 - padding.left - padding.right; if (part_width < 1) return; // Left/right-dependent fonts aren't supported ve->word_layout = pango_layout_new (pc); pango_layout_set_text (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 * part_width); // gtk_css_style_get_pango_attributes() is completely inaccessible, // so the underline cannot be styled (GTK_STYLE_PROPERTY_FONT isn't enough) PangoAttrList *attrs = pango_attr_list_new (); PangoAttribute *u = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); u->end_index = ve->word_matched; pango_attr_list_insert (attrs, u); pango_layout_set_attributes (ve->word_layout, attrs); pango_attr_list_unref (attrs); // TODO: preferably pre-validate the layout with pango_parse_markup(), // so that it doesn't warn without indication on the frontend 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 * part_width); pango_layout_set_wrap (ve->definition_layout, PANGO_WRAP_WORD_CHAR); } // --- 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 gdouble drag_last_offset; ///< Last offset when dragging GList *entries; ///< ViewEntry-s within the view GtkGesture *selection_gesture; ///< Selection gesture GWeakRef selection; ///< Selected PangoLayout, if any int selection_begin; ///< Start index within `selection` int selection_end; ///< End index within `selection` GWeakRef hover; ///< Hovered PangoLayout, if any int hover_begin; ///< Word start index within `hover` int hover_end; ///< Word end index within `hover` }; static ViewEntry * make_entry (StardictView *self, StardictIterator *iterator) { const gchar *matched = self->matched ? self->matched : ""; ViewEntry *ve = view_entry_new (iterator, matched); view_entry_rebuild_layouts (ve, GTK_WIDGET (self)); return ve; } static void reset_hover (StardictView *self) { GtkWidget *widget = GTK_WIDGET (self); PangoLayout *hover = g_weak_ref_get (&self->hover); if (hover) { g_object_unref (hover); g_weak_ref_set (&self->hover, NULL); self->hover_begin = self->hover_end = -1; gtk_widget_queue_draw (widget); } if (gtk_widget_get_realized (widget)) gdk_window_set_cursor (gtk_widget_get_window (widget), NULL); } static void adjust_for_height (StardictView *self) { GtkWidget *widget = GTK_WIDGET (self); StardictIterator *iterator = stardict_iterator_new (self->dict, self->top_position); gint missing = gtk_widget_get_allocated_height (widget) + self->top_offset; for (GList *iter = self->entries, *next; next = g_list_next (iter), iter; iter = next) { if (missing > 0) missing -= 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); } GList *append = NULL; while (missing > 0 && stardict_iterator_is_valid (iterator)) { ViewEntry *ve = make_entry (self, iterator); missing -= view_entry_height (ve, NULL, NULL); append = g_list_prepend (append, ve); stardict_iterator_next (iterator); } g_object_unref (iterator); // Also handling this for adjust_for_offset(), which calls this. PangoLayout *selection = g_weak_ref_get (&self->selection); if (selection) g_object_unref (selection); else self->selection_begin = self->selection_end = -1; reset_hover (self); self->entries = g_list_concat (self->entries, g_list_reverse (append)); gtk_widget_queue_draw (widget); } static void adjust_for_offset (StardictView *self) { // 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 = make_entry (self, iterator); 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 adjust_for_height (self); } static void reload (StardictView *self) { GtkWidget *widget = GTK_WIDGET (self); g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy); self->entries = NULL; gtk_widget_queue_draw (widget); // For consistency, and the check in make_context_menu() self->selection_begin = self->selection_end = -1; reset_hover (self); if (gtk_widget_get_realized (widget) && self->dict) adjust_for_height (self); } static gint natural_row_size (GtkWidget *widget) { PangoLayout *layout = gtk_widget_create_pango_layout (widget, "X"); gint width = 0, height = 0; pango_layout_get_pixel_size (layout, &width, &height); g_object_unref (layout); return height; } // --- Figuring out where stuff is---------------------------------------------- /// Figure out which layout is at given widget coordinates, and translate them. static PangoLayout * layout_at (StardictView *self, int *x, int *y) { GtkWidget *widget = GTK_WIDGET (self); int width = gtk_widget_get_allocated_width (widget); // The algorithm here is a simplification of stardict_view_draw(). GtkStyleContext *style = gtk_widget_get_style_context (widget); GtkBorder padding = view_entry_get_padding (style); gint offset = -self->top_offset; for (GList *iter = self->entries; iter; iter = iter->next) { ViewEntry *ve = iter->data; if (G_UNLIKELY (*y < offset)) break; gint top_y = offset, word_y = 0, defn_y = 0; offset += view_entry_height (ve, &word_y, &defn_y); if (*y >= offset) continue; if (*x >= width / 2) { *x -= width / 2 + padding.left; *y -= top_y + defn_y; return ve->definition_layout; } else { *x -= padding.left; *y -= top_y + word_y; return ve->word_layout; } } return NULL; } /// Figure out a layout's coordinates. static gboolean layout_coords (StardictView *self, PangoLayout *layout, int *x, int *y) { GtkWidget *widget = GTK_WIDGET (self); int width = gtk_widget_get_allocated_width (widget); // The algorithm here is a simplification of stardict_view_draw(). GtkStyleContext *style = gtk_widget_get_style_context (widget); GtkBorder padding = view_entry_get_padding (style); gint offset = -self->top_offset; for (GList *iter = self->entries; iter; iter = iter->next) { ViewEntry *ve = iter->data; gint top_y = offset, word_y = 0, defn_y = 0; offset += view_entry_height (ve, &word_y, &defn_y); if (layout == ve->definition_layout) { *x = width / 2 + padding.left; *y = top_y + defn_y; return TRUE; } if (layout == ve->word_layout) { *x = padding.left; *y = top_y + word_y; return TRUE; } } return FALSE; } static int layout_index_at (PangoLayout *layout, int x, int y) { int index = 0, trailing = 0; (void) pango_layout_xy_to_index (layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, &trailing); const char *text = pango_layout_get_text (layout) + index; while (trailing--) { int len = g_utf8_next_char (text) - text; text += len; index += len; } return index; } static PangoLayout * locate_word_at (StardictView *self, int x, int y, int *beginpos, int *endpos) { *beginpos = -1; *endpos = -1; PangoLayout *layout = layout_at (self, &x, &y); if (!layout) return NULL; const char *text = pango_layout_get_text (layout), *p = NULL; const char *begin = text + layout_index_at (layout, x, y), *end = begin; while ((p = g_utf8_find_prev_char (text, begin)) && !g_unichar_isspace (g_utf8_get_char (p))) begin = p; gunichar c; while ((c = g_utf8_get_char (end)) && !g_unichar_isspace (c)) end = g_utf8_next_char (end); *beginpos = begin - text; *endpos = end - text; return layout; } // --- Boilerplate ------------------------------------------------------------- G_DEFINE_TYPE (StardictView, stardict_view, GTK_TYPE_WIDGET) enum { SEND, LAST_SIGNAL, }; static guint view_signals[LAST_SIGNAL]; 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_object_unref (self->selection_gesture); g_weak_ref_clear (&self->selection); 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) { // There isn't any value that would make any real sense if (!STARDICT_VIEW (widget)->dict) *natural = *minimum = 0; else *natural = *minimum = natural_row_size (widget); } static void stardict_view_get_preferred_width (GtkWidget *widget, gint *minimum, gint *natural) { GtkStyleContext *style = gtk_widget_get_style_context (widget); GtkBorder padding = view_entry_get_padding (style); *natural = *minimum = 2 * (padding.left + 1 * padding.right); } 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 | GDK_SMOOTH_SCROLL_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK, }; // We need this window to receive input events at all. 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 gtk_widget_register_window (widget, window); gtk_widget_set_window (widget, window); gtk_widget_set_realized (widget, TRUE); } static void reset_hover_for_event (StardictView *self, guint state, int x, int y) { reset_hover (self); if ((state &= gtk_accelerator_get_default_mod_mask ()) != GDK_CONTROL_MASK) return; GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self)); if (x < 0 || y < 0 || x >= gdk_window_get_width (window) || y >= gdk_window_get_height (window)) return; g_weak_ref_set (&self->hover, locate_word_at (self, x, y, &self->hover_begin, &self->hover_end)); gtk_widget_queue_draw (GTK_WIDGET (self)); GdkCursor *cursor = gdk_cursor_new_from_name (gdk_window_get_display (window), "pointer"); gdk_window_set_cursor (window, cursor); g_object_unref (cursor); } static void on_keymap_state_changed (G_GNUC_UNUSED GdkKeymap *keymap, StardictView *self) { GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self)); GdkSeat *seat = gdk_display_get_default_seat (display); GdkDevice *pointer = gdk_seat_get_pointer (seat); int x = -1, y = -1; GdkModifierType state = 0; GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self)); gdk_window_get_device_position (window, pointer, &x, &y, &state); reset_hover_for_event (self, state, x, y); } static void stardict_view_map (GtkWidget *widget) { GTK_WIDGET_CLASS (stardict_view_parent_class)->map (widget); GdkWindow *window = gtk_widget_get_window (widget); GdkDisplay *display = gdk_window_get_display (window); GdkKeymap *keymap = gdk_keymap_get_for_display (display); g_signal_connect (keymap, "state-changed", G_CALLBACK (on_keymap_state_changed), widget); } static void stardict_view_unmap (GtkWidget *widget) { GdkWindow *window = gtk_widget_get_window (widget); GdkDisplay *display = gdk_window_get_display (window); GdkKeymap *keymap = gdk_keymap_get_for_display (display); g_signal_handlers_disconnect_by_data (keymap, widget); GTK_WIDGET_CLASS (stardict_view_parent_class)->unmap (widget); } static gboolean stardict_view_draw (GtkWidget *widget, cairo_t *cr) { StardictView *self = STARDICT_VIEW (widget); GtkAllocation allocation; gtk_widget_get_allocation (widget, &allocation); GtkStyleContext *style = gtk_widget_get_style_context (widget); gtk_render_background (style, cr, 0, 0, allocation.width, allocation.height); gtk_render_frame (style, cr, 0, 0, allocation.width, allocation.height); ViewEntryRenderCtx ctx = { .style = style, .cr = cr, .width = allocation.width, .height = 0, .selection_layout = g_weak_ref_get (&self->selection), .selection_begin = self->selection_begin, .selection_end = self->selection_end, .hover_layout = g_weak_ref_get (&self->hover), .hover_begin = self->hover_begin, .hover_end = self->hover_end, }; gint offset = -self->top_offset; gint i = self->top_position; for (GList *iter = self->entries; iter; iter = iter->next) { // Style regions would be appropriate, if they weren't deprecated. // GTK+ CSS gadgets/nodes are an internal API. We don't want to turn // this widget into a container, to avoid needless complexity. // // gtk_style_context_{get,set}_path() may be misused by adding the same // GType with gtk_widget_path_append_type() and changing its name // using gtk_widget_path_iter_set_name()... but that is ugly. gtk_style_context_save (style); gtk_style_context_add_class (style, (i++ & 1) ? "even" : "odd"); 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, &ctx); cairo_restore (cr); gtk_style_context_restore (style); } g_clear_object (&ctx.selection_layout); g_clear_object (&ctx.hover_layout); return TRUE; } static void stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GTK_WIDGET_CLASS (stardict_view_parent_class) ->size_allocate (widget, allocation); StardictView *self = STARDICT_VIEW (widget); if (!gtk_widget_get_realized (widget) || !self->dict) return; PangoLayout *selection = g_weak_ref_get (&self->selection), **origin = NULL; for (GList *iter = self->entries; iter; iter = iter->next) { ViewEntry *ve = iter->data; if (selection && selection == ve->word_layout) origin = &ve->word_layout; if (selection && selection == ve->definition_layout) origin = &ve->definition_layout; } if (selection) g_object_unref (selection); for (GList *iter = self->entries; iter; iter = iter->next) view_entry_rebuild_layouts (iter->data, widget); if (origin) g_weak_ref_set (&self->selection, *origin); adjust_for_offset (self); } 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: stardict_view_scroll (self, GTK_SCROLL_STEPS, -3); return TRUE; case GDK_SCROLL_DOWN: stardict_view_scroll (self, GTK_SCROLL_STEPS, +3); return TRUE; case GDK_SCROLL_SMOOTH: { // On GDK/Wayland, the mouse wheel will typically create 1.5 deltas, // after dividing a 15 degree click angle from libinput by 10. // (Noticed on Arch + Sway, cannot reproduce on Ubuntu 22.04.) // On X11, as libinput(4) indicates, the delta will always be 1.0. double delta = CLAMP (event->delta_y, -1, +1); stardict_view_scroll (self, GTK_SCROLL_STEPS, 3 * delta); return TRUE; } default: return FALSE; } } static void publish_selection (StardictView *self, GdkAtom target) { PangoLayout *layout = g_weak_ref_get (&self->selection); if (!layout) return; // Unlike GtkLabel, we don't place the selection in PRIMARY immediately. const char *text = pango_layout_get_text (layout); int len = strlen (text), s1 = MIN (self->selection_begin, self->selection_end), s2 = MAX (self->selection_begin, self->selection_end); if (s1 != s2 && s1 >= 0 && s1 <= len && s2 >= 0 && s2 <= len) gtk_clipboard_set_text (gtk_clipboard_get (target), text + s1, s2 - s1); g_object_unref (layout); } static void select_word_at (StardictView *self, int x, int y) { g_weak_ref_set (&self->selection, locate_word_at (self, x, y, &self->selection_begin, &self->selection_end)); gtk_widget_queue_draw (GTK_WIDGET (self)); publish_selection (self, GDK_SELECTION_PRIMARY); } static void select_all_at (StardictView *self, int x, int y) { PangoLayout *layout = layout_at (self, &x, &y); if (!layout) return; g_weak_ref_set (&self->selection, layout); self->selection_begin = 0; self->selection_end = strlen (pango_layout_get_text (layout)); gtk_widget_queue_draw (GTK_WIDGET (self)); publish_selection (self, GDK_SELECTION_PRIMARY); } static void on_copy_activate (G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data) { publish_selection (STARDICT_VIEW (user_data), GDK_SELECTION_CLIPBOARD); } static gboolean destroy_widget_idle_source_func (GtkWidget *widget) { // The whole menu is deactivated /before/ any item is activated, // and a destroyed child item will not activate. gtk_widget_destroy (widget); return FALSE; } static GtkMenu * make_context_menu (StardictView *self) { GtkWidget *copy = gtk_menu_item_new_with_mnemonic ("_Copy"); gtk_widget_set_sensitive (copy, self->selection_begin != self->selection_end); g_signal_connect_data (copy, "activate", G_CALLBACK (on_copy_activate), g_object_ref (self), (GClosureNotify) g_object_unref, 0); GtkWidget *menu = gtk_menu_new (); gtk_menu_shell_append (GTK_MENU_SHELL (menu), copy); // As per GTK+ 3 Common Questions, 1.5. g_object_ref_sink (menu); g_signal_connect_swapped (menu, "deactivate", G_CALLBACK (g_idle_add), destroy_widget_idle_source_func); g_signal_connect (menu, "destroy", G_CALLBACK (g_object_unref), NULL); gtk_widget_show_all (menu); return GTK_MENU (menu); } static gboolean stardict_view_button_press_event (GtkWidget *widget, GdkEventButton *event) { StardictView *self = STARDICT_VIEW (widget); if (gdk_event_triggers_context_menu ((const GdkEvent *) event)) { gtk_menu_popup_at_pointer (make_context_menu (self), (const GdkEvent *) event); return GDK_EVENT_STOP; } if (event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { gtk_event_controller_reset ( GTK_EVENT_CONTROLLER (self->selection_gesture)); select_word_at (self, event->x, event->y); return GDK_EVENT_STOP; } if (event->type == GDK_3BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { gtk_event_controller_reset ( GTK_EVENT_CONTROLLER (self->selection_gesture)); select_all_at (self, event->x, event->y); return GDK_EVENT_STOP; } return GTK_WIDGET_CLASS (stardict_view_parent_class) ->button_press_event (widget, event); } static gboolean stardict_view_motion_notify_event (GtkWidget *widget, GdkEventMotion *event) { StardictView *self = STARDICT_VIEW (widget); reset_hover_for_event (self, event->state, event->x, event->y); return GTK_WIDGET_CLASS (stardict_view_parent_class) ->motion_notify_event (widget, event); } static gboolean stardict_view_leave_notify_event (GtkWidget *widget, G_GNUC_UNUSED GdkEventCrossing *event) { reset_hover (STARDICT_VIEW (widget)); return GDK_EVENT_PROPAGATE; } static void on_drag_begin (GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x, G_GNUC_UNUSED gdouble start_y, gpointer user_data) { GtkGesture *gesture = GTK_GESTURE (drag); GdkEventSequence *sequence = gtk_gesture_get_last_updated_sequence (gesture); GdkModifierType state = 0; const GdkEvent *last_event = gtk_gesture_get_last_event (gesture, sequence); (void) gdk_event_get_state (last_event, &state); if (state & gtk_accelerator_get_default_mod_mask ()) gtk_gesture_set_sequence_state (gesture, sequence, GTK_EVENT_SEQUENCE_DENIED); else { gtk_gesture_set_sequence_state (gesture, sequence, GTK_EVENT_SEQUENCE_CLAIMED); STARDICT_VIEW (user_data)->drag_last_offset = 0; } } static void on_drag_update (G_GNUC_UNUSED GtkGestureDrag *drag, G_GNUC_UNUSED gdouble offset_x, gdouble offset_y, gpointer user_data) { StardictView *self = STARDICT_VIEW (user_data); self->top_offset += self->drag_last_offset - offset_y; adjust_for_offset (self); self->drag_last_offset = offset_y; } static gboolean send_hover (StardictView *self) { PangoLayout *layout = g_weak_ref_get (&self->hover); if (!layout) return FALSE; const char *text = pango_layout_get_text (layout); int len = strlen (text), s1 = MIN (self->hover_begin, self->hover_end), s2 = MAX (self->hover_begin, self->hover_end); if (s1 != s2 && s1 >= 0 && s1 <= len && s2 >= 0 && s2 <= len) { gchar *word = g_strndup (text + s1, s2 - s1); g_signal_emit (self, view_signals[SEND], 0, word); g_free (word); } g_object_unref (layout); return TRUE; } static void on_select_begin (GtkGestureDrag *drag, gdouble start_x, gdouble start_y, gpointer user_data) { // We probably don't need to check modifiers and mouse position again. StardictView *self = STARDICT_VIEW (user_data); GtkGesture *gesture = GTK_GESTURE (drag); if (send_hover (self)) { gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); return; } // Despite our two gestures not being grouped up, claiming one doesn't // deny the other, and :exclusive isn't the opposite of :touch-only. // A non-NULL sequence indicates a touch event. if (gtk_gesture_get_last_updated_sequence (gesture)) { gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); return; } g_weak_ref_set (&self->selection, NULL); self->selection_begin = -1; self->selection_end = -1; gtk_widget_queue_draw (GTK_WIDGET (self)); int layout_x = start_x, layout_y = start_y; PangoLayout *layout = layout_at (self, &layout_x, &layout_y); if (!layout) { gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); return; } g_weak_ref_set (&self->selection, layout); self->selection_end = self->selection_begin = layout_index_at (layout, layout_x, layout_y); gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED); } static void on_select_update (GtkGestureDrag *drag, gdouble offset_x, gdouble offset_y, gpointer user_data) { GtkGesture *gesture = GTK_GESTURE (drag); StardictView *self = STARDICT_VIEW (user_data); PangoLayout *layout = g_weak_ref_get (&self->selection); if (!layout) { gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); return; } double start_x = 0, start_y = 0; (void) gtk_gesture_drag_get_start_point (drag, &start_x, &start_y); int x = 0, y = 0; if (!layout_coords (self, layout, &x, &y)) { g_warning ("internal error: weakly referenced layout not found"); gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); goto out; } self->selection_end = layout_index_at (layout, start_x + offset_x - x, start_y + offset_y - y); gtk_widget_queue_draw (GTK_WIDGET (self)); out: g_object_unref (layout); } static void on_select_end (G_GNUC_UNUSED GtkGestureDrag *drag, G_GNUC_UNUSED gdouble offset_x, G_GNUC_UNUSED gdouble offset_y, gpointer user_data) { publish_selection (STARDICT_VIEW (user_data), GDK_SELECTION_PRIMARY); } static void stardict_view_class_init (StardictViewClass *klass) { view_signals[SEND] = g_signal_new ("send", G_TYPE_FROM_CLASS (klass), 0, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = stardict_view_finalize; 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->map = stardict_view_map; widget_class->unmap = stardict_view_unmap; 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; widget_class->button_press_event = stardict_view_button_press_event; widget_class->motion_notify_event = stardict_view_motion_notify_event; widget_class->leave_notify_event = stardict_view_leave_notify_event; gtk_widget_class_set_css_name (widget_class, "stardict-view"); } static void stardict_view_init (StardictView *self) { g_weak_ref_init (&self->selection, NULL); self->selection_begin = -1; self->selection_end = -1; GtkGesture *drag = gtk_gesture_drag_new (GTK_WIDGET (self)); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (drag), TRUE); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (drag), GTK_PHASE_TARGET); g_object_set_data_full (G_OBJECT (self), "stardict-view-drag-gesture", drag, g_object_unref); g_signal_connect (drag, "drag-begin", G_CALLBACK (on_drag_begin), self); g_signal_connect (drag, "drag-update", G_CALLBACK (on_drag_update), self); self->selection_gesture = gtk_gesture_drag_new (GTK_WIDGET (self)); gtk_gesture_single_set_exclusive ( GTK_GESTURE_SINGLE (self->selection_gesture), TRUE); gtk_event_controller_set_propagation_phase ( GTK_EVENT_CONTROLLER (self->selection_gesture), GTK_PHASE_TARGET); g_signal_connect (self->selection_gesture, "drag-begin", G_CALLBACK (on_select_begin), self); g_signal_connect (self->selection_gesture, "drag-update", G_CALLBACK (on_select_update), self); g_signal_connect (self->selection_gesture, "drag-end", G_CALLBACK (on_select_end), 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 (dict == NULL || 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 = dict ? g_object_ref (dict) : NULL; 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); } void stardict_view_scroll (StardictView *self, GtkScrollStep step, gdouble amount) { g_return_if_fail (STARDICT_IS_VIEW (self)); GtkWidget *widget = GTK_WIDGET (self); switch (step) { case GTK_SCROLL_STEPS: self->top_offset += amount * natural_row_size (widget); break; case GTK_SCROLL_PAGES: self->top_offset += amount * gtk_widget_get_allocated_height (widget); break; default: break; } adjust_for_offset (self); }