sdgui: make Ctrl+click put hovered word in search
This commit is contained in:
		@@ -96,7 +96,7 @@ should be addressed before including the software in regular Linux and/or
 | 
			
		||||
BSD distributions:
 | 
			
		||||
 | 
			
		||||
 - The tab bar doesn't handle overflows well in the TUI.
 | 
			
		||||
 - The GUI has a few usability issues, and is awkward to configure.
 | 
			
		||||
 - The GUI is awkward to configure.
 | 
			
		||||
 - Lacking configuration, standard StarDict locations should be scanned.
 | 
			
		||||
 | 
			
		||||
Given all issues with the file format, it might be better to start anew.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/sdgui.c
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/sdgui.c
									
									
									
									
									
								
							@@ -127,6 +127,15 @@ on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
 | 
			
		||||
	search (g_ptr_array_index (g.dictionaries, g.dictionary));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
on_send (G_GNUC_UNUSED StardictView *view,
 | 
			
		||||
	const char *word, G_GNUC_UNUSED gpointer data)
 | 
			
		||||
{
 | 
			
		||||
	GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
 | 
			
		||||
	gtk_entry_buffer_set_text (buf, word, -1);
 | 
			
		||||
	gtk_editable_select_region (GTK_EDITABLE (g.entry), 0, -1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
 | 
			
		||||
	G_GNUC_UNUSED gpointer data)
 | 
			
		||||
@@ -529,6 +538,8 @@ main (int argc, char *argv[])
 | 
			
		||||
	gtk_drag_dest_add_uri_targets (g.view);
 | 
			
		||||
	g_signal_connect (g.view, "drag-data-received",
 | 
			
		||||
		G_CALLBACK (on_drag_data_received), NULL);
 | 
			
		||||
	g_signal_connect (g.view, "send",
 | 
			
		||||
		G_CALLBACK (on_send), NULL);
 | 
			
		||||
 | 
			
		||||
	gtk_widget_show_all (g.window);
 | 
			
		||||
	gtk_main ();
 | 
			
		||||
 
 | 
			
		||||
@@ -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 *
 | 
			
		||||
@@ -283,6 +325,23 @@ make_entry (StardictView *self, StardictIterator *iterator)
 | 
			
		||||
	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)
 | 
			
		||||
{
 | 
			
		||||
@@ -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,131 +454,7 @@ natural_row_size (GtkWidget *widget)
 | 
			
		||||
	return height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 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_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,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
	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);
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
	return TRUE;
 | 
			
		||||
}
 | 
			
		||||
// --- Figuring out where stuff is----------------------------------------------
 | 
			
		||||
 | 
			
		||||
/// Figure out which layout is at given widget coordinates, and translate them.
 | 
			
		||||
static PangoLayout *
 | 
			
		||||
@@ -590,6 +527,242 @@ layout_coords (StardictView *self, PangoLayout *layout, int *x, int *y)
 | 
			
		||||
	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;
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
{
 | 
			
		||||
	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)
 | 
			
		||||
{
 | 
			
		||||
@@ -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)
 | 
			
		||||
{
 | 
			
		||||
	// 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.
 | 
			
		||||
	// We probably don't need to check modifiers and mouse position again.
 | 
			
		||||
	StardictView *self = STARDICT_VIEW (user_data);
 | 
			
		||||
	GtkGesture *gesture = GTK_GESTURE (drag);
 | 
			
		||||
	if (gtk_gesture_get_last_updated_sequence (gesture))
 | 
			
		||||
	if (send_hover (self))
 | 
			
		||||
	{
 | 
			
		||||
		gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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");
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user