Compare commits
	
		
			3 Commits
		
	
	
		
			b0f1d3d6ea
			...
			ded899933c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ded899933c | |||
| 832842bf81 | |||
| 49072f9d01 | 
| @ -96,7 +96,7 @@ should be addressed before including the software in regular Linux and/or | |||||||
| BSD distributions: | BSD distributions: | ||||||
| 
 | 
 | ||||||
|  - The tab bar doesn't handle overflows well in the TUI. |  - 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. |  - Lacking configuration, standard StarDict locations should be scanned. | ||||||
| 
 | 
 | ||||||
| Given all issues with the file format, it might be better to start anew. | 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)); | 	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 | static void | ||||||
| on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text, | on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text, | ||||||
| 	G_GNUC_UNUSED gpointer data) | 	G_GNUC_UNUSED gpointer data) | ||||||
| @ -529,6 +538,8 @@ main (int argc, char *argv[]) | |||||||
| 	gtk_drag_dest_add_uri_targets (g.view); | 	gtk_drag_dest_add_uri_targets (g.view); | ||||||
| 	g_signal_connect (g.view, "drag-data-received", | 	g_signal_connect (g.view, "drag-data-received", | ||||||
| 		G_CALLBACK (on_drag_data_received), NULL); | 		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_widget_show_all (g.window); | ||||||
| 	gtk_main (); | 	gtk_main (); | ||||||
|  | |||||||
| @ -149,15 +149,51 @@ struct view_entry_render_ctx | |||||||
| 	PangoLayout *selection_layout; | 	PangoLayout *selection_layout; | ||||||
| 	int selection_begin; | 	int selection_begin; | ||||||
| 	int selection_end; | 	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 | static void | ||||||
| view_entry_render (ViewEntryRenderCtx *ctx, gdouble x, gdouble y, | view_entry_render (ViewEntryRenderCtx *ctx, gdouble x, gdouble y, | ||||||
| 	PangoLayout *layout) | 	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) | 	if (layout != ctx->selection_layout) | ||||||
| 		return; | 		goto out; | ||||||
| 
 | 
 | ||||||
| 	gtk_style_context_save (ctx->style); | 	gtk_style_context_save (ctx->style); | ||||||
| 	gtk_style_context_set_state (ctx->style, GTK_STATE_FLAG_SELECTED); | 	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), | 	int ranges[2] = { MIN (ctx->selection_begin, ctx->selection_end), | ||||||
| 		MAX (ctx->selection_begin, ctx->selection_end) }; | 		MAX (ctx->selection_begin, ctx->selection_end) }; | ||||||
| 	cairo_region_t *region | 	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); | 	gdk_cairo_region (ctx->cr, region); | ||||||
| 	cairo_clip (ctx->cr); | 	cairo_clip (ctx->cr); | ||||||
| 	cairo_region_destroy (region); | 	cairo_region_destroy (region); | ||||||
| 
 | 
 | ||||||
| 	gtk_render_background (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height); | 	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); | 	cairo_restore (ctx->cr); | ||||||
| 	gtk_style_context_restore (ctx->style); | 	gtk_style_context_restore (ctx->style); | ||||||
|  | out: | ||||||
|  | 	g_object_unref (adjusted); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static gint | static gint | ||||||
| @ -272,6 +310,10 @@ struct _StardictView | |||||||
| 	GWeakRef selection;                 ///< Selected PangoLayout, if any
 | 	GWeakRef selection;                 ///< Selected PangoLayout, if any
 | ||||||
| 	int selection_begin;                ///< Start index within `selection`
 | 	int selection_begin;                ///< Start index within `selection`
 | ||||||
| 	int selection_end;                  ///< End 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 * | static ViewEntry * | ||||||
| @ -283,6 +325,23 @@ make_entry (StardictView *self, StardictIterator *iterator) | |||||||
| 	return ve; | 	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 | static void | ||||||
| adjust_for_height (StardictView *self) | adjust_for_height (StardictView *self) | ||||||
| { | { | ||||||
| @ -314,6 +373,14 @@ adjust_for_height (StardictView *self) | |||||||
| 	} | 	} | ||||||
| 	g_object_unref (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)); | 	self->entries = g_list_concat (self->entries, g_list_reverse (append)); | ||||||
| 	gtk_widget_queue_draw (widget); | 	gtk_widget_queue_draw (widget); | ||||||
| } | } | ||||||
| @ -365,13 +432,13 @@ reload (StardictView *self) | |||||||
| { | { | ||||||
| 	GtkWidget *widget = GTK_WIDGET (self); | 	GtkWidget *widget = GTK_WIDGET (self); | ||||||
| 
 | 
 | ||||||
| 	// FIXME: this invalidates the selection, we'd need better identification
 |  | ||||||
| 	g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy); | 	g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy); | ||||||
| 	self->entries = NULL; | 	self->entries = NULL; | ||||||
| 	gtk_widget_queue_draw (widget); | 	gtk_widget_queue_draw (widget); | ||||||
| 
 | 
 | ||||||
| 	// For consistency, and the check in make_context_menu()
 | 	// For consistency, and the check in make_context_menu()
 | ||||||
| 	self->selection_begin = self->selection_end = -1; | 	self->selection_begin = self->selection_end = -1; | ||||||
|  | 	reset_hover (self); | ||||||
| 
 | 
 | ||||||
| 	if (gtk_widget_get_realized (widget) && self->dict) | 	if (gtk_widget_get_realized (widget) && self->dict) | ||||||
| 		adjust_for_height (self); | 		adjust_for_height (self); | ||||||
| @ -387,131 +454,7 @@ natural_row_size (GtkWidget *widget) | |||||||
| 	return height; | 	return height; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // --- Boilerplate -------------------------------------------------------------
 | // --- Figuring out where stuff is----------------------------------------------
 | ||||||
| 
 |  | ||||||
| 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; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Figure out which layout is at given widget coordinates, and translate them.
 | /// Figure out which layout is at given widget coordinates, and translate them.
 | ||||||
| static PangoLayout * | static PangoLayout * | ||||||
| @ -584,13 +527,270 @@ layout_coords (StardictView *self, PangoLayout *layout, int *x, int *y) | |||||||
| 	return FALSE; | 	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 | static void | ||||||
| stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation) | stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation) | ||||||
| { | { | ||||||
| 	GTK_WIDGET_CLASS (stardict_view_parent_class) | 	GTK_WIDGET_CLASS (stardict_view_parent_class) | ||||||
| 		->size_allocate (widget, allocation); | 		->size_allocate (widget, allocation); | ||||||
| 
 | 
 | ||||||
| 	reload (STARDICT_VIEW (widget)); | 	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 | static void | ||||||
| @ -635,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 | static void | ||||||
| publish_selection (StardictView *self, GdkAtom target) | publish_selection (StardictView *self, GdkAtom target) | ||||||
| { | { | ||||||
| 	PangoLayout *layout = g_weak_ref_get (&self->selection); | 	PangoLayout *layout = g_weak_ref_get (&self->selection); | ||||||
| 	if (!layout || self->selection_begin == self->selection_end) | 	if (!layout) | ||||||
| 		return; | 		return; | ||||||
| 
 | 
 | ||||||
| 	// Unlike GtkLabel, we don't place the selection in PRIMARY immediately.
 | 	// Unlike GtkLabel, we don't place the selection in PRIMARY immediately.
 | ||||||
| @ -667,7 +847,7 @@ publish_selection (StardictView *self, GdkAtom target) | |||||||
| 	int len = strlen (text), | 	int len = strlen (text), | ||||||
| 		s1 = MIN (self->selection_begin, self->selection_end), | 		s1 = MIN (self->selection_begin, self->selection_end), | ||||||
| 		s2 = MAX (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); | 		gtk_clipboard_set_text (gtk_clipboard_get (target), text + s1, s2 - s1); | ||||||
| 	g_object_unref (layout); | 	g_object_unref (layout); | ||||||
| } | } | ||||||
| @ -675,22 +855,9 @@ publish_selection (StardictView *self, GdkAtom target) | |||||||
| static void | static void | ||||||
| select_word_at (StardictView *self, int x, int y) | select_word_at (StardictView *self, int x, int y) | ||||||
| { | { | ||||||
| 	PangoLayout *layout = layout_at (self, &x, &y); | 	g_weak_ref_set (&self->selection, locate_word_at (self, | ||||||
| 	if (!layout) | 		x, y, &self->selection_begin, &self->selection_end)); | ||||||
| 		return; |  | ||||||
| 
 | 
 | ||||||
| 	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)); | 	gtk_widget_queue_draw (GTK_WIDGET (self)); | ||||||
| 	publish_selection (self, GDK_SELECTION_PRIMARY); | 	publish_selection (self, GDK_SELECTION_PRIMARY); | ||||||
| } | } | ||||||
| @ -729,7 +896,7 @@ make_context_menu (StardictView *self) | |||||||
| { | { | ||||||
| 	GtkWidget *copy = gtk_menu_item_new_with_mnemonic ("_Copy"); | 	GtkWidget *copy = gtk_menu_item_new_with_mnemonic ("_Copy"); | ||||||
| 	gtk_widget_set_sensitive (copy, | 	gtk_widget_set_sensitive (copy, | ||||||
| 		self->selection_begin < self->selection_end); | 		self->selection_begin != self->selection_end); | ||||||
| 	g_signal_connect_data (copy, "activate", | 	g_signal_connect_data (copy, "activate", | ||||||
| 		G_CALLBACK (on_copy_activate), g_object_ref (self), | 		G_CALLBACK (on_copy_activate), g_object_ref (self), | ||||||
| 		(GClosureNotify) g_object_unref, 0); | 		(GClosureNotify) g_object_unref, 0); | ||||||
| @ -779,6 +946,23 @@ stardict_view_button_press_event (GtkWidget *widget, GdkEventButton *event) | |||||||
| 		->button_press_event (widget, 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 | static void | ||||||
| on_drag_begin (GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x, | on_drag_begin (GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x, | ||||||
| 	G_GNUC_UNUSED gdouble start_y, gpointer user_data) | 	G_GNUC_UNUSED gdouble start_y, gpointer user_data) | ||||||
| @ -811,21 +995,49 @@ on_drag_update (G_GNUC_UNUSED GtkGestureDrag *drag, | |||||||
| 	self->drag_last_offset = offset_y; | 	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 | static void | ||||||
| on_select_begin (GtkGestureDrag *drag, gdouble start_x, gdouble start_y, | on_select_begin (GtkGestureDrag *drag, gdouble start_x, gdouble start_y, | ||||||
| 	gpointer user_data) | 	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
 | 	// Despite our two gestures not being grouped up, claiming one doesn't
 | ||||||
| 	// deny the other, and :exclusive isn't the opposite of :touch-only.
 | 	// deny the other, and :exclusive isn't the opposite of :touch-only.
 | ||||||
| 	// A non-NULL sequence indicates a touch event.
 | 	// A non-NULL sequence indicates a touch event.
 | ||||||
| 	GtkGesture *gesture = GTK_GESTURE (drag); |  | ||||||
| 	if (gtk_gesture_get_last_updated_sequence (gesture)) | 	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; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	StardictView *self = STARDICT_VIEW (user_data); |  | ||||||
| 	g_weak_ref_set (&self->selection, NULL); | 	g_weak_ref_set (&self->selection, NULL); | ||||||
| 	self->selection_begin = -1; | 	self->selection_begin = -1; | ||||||
| 	self->selection_end = -1; | 	self->selection_end = -1; | ||||||
| @ -889,6 +1101,10 @@ on_select_end (G_GNUC_UNUSED GtkGestureDrag *drag, | |||||||
| static void | static void | ||||||
| stardict_view_class_init (StardictViewClass *klass) | 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); | 	GObjectClass *object_class = G_OBJECT_CLASS (klass); | ||||||
| 	object_class->finalize = stardict_view_finalize; | 	object_class->finalize = stardict_view_finalize; | ||||||
| 
 | 
 | ||||||
| @ -896,11 +1112,15 @@ stardict_view_class_init (StardictViewClass *klass) | |||||||
| 	widget_class->get_preferred_height = stardict_view_get_preferred_height; | 	widget_class->get_preferred_height = stardict_view_get_preferred_height; | ||||||
| 	widget_class->get_preferred_width = stardict_view_get_preferred_width; | 	widget_class->get_preferred_width = stardict_view_get_preferred_width; | ||||||
| 	widget_class->realize = stardict_view_realize; | 	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->draw = stardict_view_draw; | ||||||
| 	widget_class->size_allocate = stardict_view_size_allocate; | 	widget_class->size_allocate = stardict_view_size_allocate; | ||||||
| 	widget_class->screen_changed = stardict_view_screen_changed; | 	widget_class->screen_changed = stardict_view_screen_changed; | ||||||
| 	widget_class->scroll_event = stardict_view_scroll_event; | 	widget_class->scroll_event = stardict_view_scroll_event; | ||||||
| 	widget_class->button_press_event = stardict_view_button_press_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"); | 	gtk_widget_class_set_css_name (widget_class, "stardict-view"); | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user