aboutsummaryrefslogtreecommitdiff
path: root/src/stardict-view.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/stardict-view.c')
-rw-r--r--src/stardict-view.c441
1 files changed, 317 insertions, 124 deletions
diff --git a/src/stardict-view.c b/src/stardict-view.c
index d073b16..056c240 100644
--- a/src/stardict-view.c
+++ b/src/stardict-view.c
@@ -149,15 +149,51 @@ struct view_entry_render_ctx
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)
{
- gtk_render_layout (ctx->style, ctx->cr, x, y, layout);
+ PangoLayout *adjusted = view_entry_adjust_layout (ctx, layout);
+ gtk_render_layout (ctx->style, ctx->cr, x, y, adjusted);
if (layout != ctx->selection_layout)
- return;
+ goto out;
gtk_style_context_save (ctx->style);
gtk_style_context_set_state (ctx->style, GTK_STATE_FLAG_SELECTED);
@@ -166,16 +202,18 @@ view_entry_render (ViewEntryRenderCtx *ctx, gdouble x, gdouble y,
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 (layout, x, y, ranges, 1);
+ = 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, layout);
+ 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
@@ -272,6 +310,10 @@ struct _StardictView
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 *
@@ -284,6 +326,23 @@ make_entry (StardictView *self, StardictIterator *iterator)
}
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);
@@ -314,15 +373,16 @@ adjust_for_height (StardictView *self)
}
g_object_unref (iterator);
- self->entries = g_list_concat (self->entries, g_list_reverse (append));
- gtk_widget_queue_draw (widget);
-
// 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
@@ -378,6 +438,7 @@ reload (StardictView *self)
// 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);
@@ -393,10 +454,133 @@ natural_row_size (GtkWidget *widget)
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)
{
@@ -454,11 +638,11 @@ stardict_view_realize (GtkWidget *widget)
.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_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.
- // 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);
@@ -468,6 +652,61 @@ stardict_view_realize (GtkWidget *widget)
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;
+
+ 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));
+
+ GdkWindow *window = gtk_widget_get_window (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)
{
@@ -488,9 +727,13 @@ stardict_view_draw (GtkWidget *widget, cairo_t *cr)
.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;
@@ -516,80 +759,10 @@ stardict_view_draw (GtkWidget *widget, cairo_t *cr)
gtk_style_context_restore (style);
}
g_clear_object (&ctx.selection_layout);
+ g_clear_object (&ctx.hover_layout);
return TRUE;
}
-/// 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 void
stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
@@ -662,31 +835,11 @@ stardict_view_scroll_event (GtkWidget *widget, GdkEventScroll *event)
}
}
-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 void
publish_selection (StardictView *self, GdkAtom target)
{
PangoLayout *layout = g_weak_ref_get (&self->selection);
- if (!layout || self->selection_begin == self->selection_end)
+ if (!layout)
return;
// Unlike GtkLabel, we don't place the selection in PRIMARY immediately.
@@ -694,7 +847,7 @@ publish_selection (StardictView *self, GdkAtom target)
int len = strlen (text),
s1 = MIN (self->selection_begin, self->selection_end),
s2 = MAX (self->selection_begin, self->selection_end);
- if (s1 >= 0 && s1 <= len && s2 >= 0 && s2 <= len)
+ 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);
}
@@ -702,22 +855,9 @@ publish_selection (StardictView *self, GdkAtom target)
static void
select_word_at (StardictView *self, int x, int y)
{
- PangoLayout *layout = layout_at (self, &x, &y);
- if (!layout)
- return;
+ g_weak_ref_set (&self->selection, locate_word_at (self,
+ x, y, &self->selection_begin, &self->selection_end));
- 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);
-
- g_weak_ref_set (&self->selection, layout);
- self->selection_begin = begin - text;
- self->selection_end = end - text;
gtk_widget_queue_draw (GTK_WIDGET (self));
publish_selection (self, GDK_SELECTION_PRIMARY);
}
@@ -779,7 +919,7 @@ 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))
+ if (gdk_event_triggers_context_menu ((const GdkEvent *) event))
{
gtk_menu_popup_at_pointer (make_context_menu (self),
(const GdkEvent *) event);
@@ -806,6 +946,23 @@ stardict_view_button_press_event (GtkWidget *widget, GdkEventButton *event)
->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)
@@ -838,21 +995,49 @@ on_drag_update (G_GNUC_UNUSED GtkGestureDrag *drag,
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.
- GtkGesture *gesture = GTK_GESTURE (drag);
if (gtk_gesture_get_last_updated_sequence (gesture))
{
- gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
- StardictView *self = STARDICT_VIEW (user_data);
g_weak_ref_set (&self->selection, NULL);
self->selection_begin = -1;
self->selection_end = -1;
@@ -862,7 +1047,7 @@ on_select_begin (GtkGestureDrag *drag, gdouble start_x, gdouble start_y,
PangoLayout *layout = layout_at (self, &layout_x, &layout_y);
if (!layout)
{
- gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
@@ -881,7 +1066,7 @@ on_select_update (GtkGestureDrag *drag, gdouble offset_x, gdouble offset_y,
PangoLayout *layout = g_weak_ref_get (&self->selection);
if (!layout)
{
- gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
@@ -892,7 +1077,7 @@ on_select_update (GtkGestureDrag *drag, gdouble offset_x, gdouble offset_y,
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);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
goto out;
}
@@ -916,6 +1101,10 @@ on_select_end (G_GNUC_UNUSED GtkGestureDrag *drag,
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;
@@ -923,11 +1112,15 @@ stardict_view_class_init (StardictViewClass *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");
}