/* gdict-defbox.c - display widget for dictionary definitions * * Copyright (C) 2005-2006 Emmanuele Bassi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, */ /** * SECTION:gdict-defbox * @short_description: Display the list of definitions for a word * * The #GdictDefbox widget is a composite widget showing the list of * definitions for a word. It queries the passed #GdictContext and displays * the list of #GdictDefinitions obtained. * * It provides syntax highlighting, clickable links and an embedded find * bar. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include "gdict-defbox.h" #include "gdict-utils.h" #include "gdict-debug.h" #include "gdict-private.h" #include "gdict-enum-types.h" #include "gdict-marshal.h" #define QUERY_MARGIN 48 #define ERROR_MARGIN 24 typedef struct { GdictDefinition *definition; gint begin; } Definition; struct _GdictDefboxPrivate { GtkWidget *text_view; /* the "find" pane */ GtkWidget *find_pane; GtkWidget *find_entry; GtkWidget *find_next; GtkWidget *find_prev; GtkWidget *find_label; GtkWidget *progress_dialog; GtkTextBuffer *buffer; GdictContext *context; GSList *definitions; gchar *word; gchar *database; gchar *font_name; guint show_find : 1; guint is_searching : 1; guint is_hovering : 1; GdkCursor *busy_cursor; GdkCursor *hand_cursor; GdkCursor *regular_cursor; guint start_id; guint end_id; guint define_id; guint error_id; guint hide_timeout; GtkTextTag *link_tag; GtkTextTag *visited_link_tag; }; enum { PROP_0, PROP_CONTEXT, PROP_WORD, PROP_DATABASE, PROP_FONT_NAME, PROP_COUNT }; enum { SHOW_FIND, HIDE_FIND, FIND_NEXT, FIND_PREVIOUS, LINK_CLICKED, LAST_SIGNAL }; static guint gdict_defbox_signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE_WITH_PRIVATE (GdictDefbox, gdict_defbox, GTK_TYPE_BOX) static Definition * definition_new (void) { Definition *def; def = g_slice_new (Definition); def->definition = NULL; def->begin = -1; return def; } static void definition_free (Definition *def) { if (!def) return; gdict_definition_unref (def->definition); g_slice_free (Definition, def); } static void gdict_defbox_dispose (GObject *gobject) { GdictDefbox *defbox = GDICT_DEFBOX (gobject); GdictDefboxPrivate *priv = defbox->priv; if (priv->start_id) { g_signal_handler_disconnect (priv->context, priv->start_id); g_signal_handler_disconnect (priv->context, priv->end_id); g_signal_handler_disconnect (priv->context, priv->define_id); priv->start_id = 0; priv->end_id = 0; priv->define_id = 0; } if (priv->error_id) { g_signal_handler_disconnect (priv->context, priv->error_id); priv->error_id = 0; } if (priv->context) { g_object_unref (priv->context); priv->context = NULL; } if (priv->buffer) { g_object_unref (priv->buffer); priv->buffer = NULL; } if (priv->busy_cursor) { g_object_unref (priv->busy_cursor); priv->busy_cursor = NULL; } if (priv->hand_cursor) { g_object_unref (priv->hand_cursor); priv->hand_cursor = NULL; } if (priv->regular_cursor) { g_object_unref (priv->regular_cursor); priv->regular_cursor = NULL; } G_OBJECT_CLASS (gdict_defbox_parent_class)->dispose (gobject); } static void gdict_defbox_finalize (GObject *object) { GdictDefbox *defbox = GDICT_DEFBOX (object); GdictDefboxPrivate *priv = defbox->priv; g_free (priv->database); g_free (priv->word); g_free (priv->font_name); if (priv->definitions) { g_slist_free_full (priv->definitions, (GDestroyNotify) definition_free); priv->definitions = NULL; } G_OBJECT_CLASS (gdict_defbox_parent_class)->finalize (object); } static void set_gdict_context (GdictDefbox *defbox, GdictContext *context) { GdictDefboxPrivate *priv; g_assert (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; if (priv->context) { if (priv->start_id) { GDICT_NOTE (DEFBOX, "Removing old context handlers"); g_signal_handler_disconnect (priv->context, priv->start_id); g_signal_handler_disconnect (priv->context, priv->define_id); g_signal_handler_disconnect (priv->context, priv->end_id); priv->start_id = 0; priv->end_id = 0; priv->define_id = 0; } if (priv->error_id) { g_signal_handler_disconnect (priv->context, priv->error_id); priv->error_id = 0; } GDICT_NOTE (DEFBOX, "Removing old context"); g_object_unref (G_OBJECT (priv->context)); } if (!context) return; if (!GDICT_IS_CONTEXT (context)) { g_warning ("Object of type '%s' instead of a GdictContext\n", g_type_name (G_OBJECT_TYPE (context))); return; } GDICT_NOTE (DEFBOX, "Setting new context"); priv->context = context; g_object_ref (G_OBJECT (priv->context)); } static void gdict_defbox_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GdictDefbox *defbox = GDICT_DEFBOX (object); GdictDefboxPrivate *priv = defbox->priv; switch (prop_id) { case PROP_WORD: gdict_defbox_lookup (defbox, g_value_get_string (value)); break; case PROP_CONTEXT: set_gdict_context (defbox, g_value_get_object (value)); break; case PROP_DATABASE: g_free (priv->database); priv->database = g_strdup (g_value_get_string (value)); break; case PROP_FONT_NAME: gdict_defbox_set_font_name (defbox, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gdict_defbox_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GdictDefbox *defbox = GDICT_DEFBOX (object); GdictDefboxPrivate *priv = defbox->priv; switch (prop_id) { case PROP_WORD: g_value_set_string (value, priv->word); break; case PROP_CONTEXT: g_value_set_object (value, priv->context); break; case PROP_DATABASE: g_value_set_string (value, priv->database); break; case PROP_FONT_NAME: g_value_set_string (value, priv->font_name); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } /* * this code has been copied from gtksourceview; it's the implementation * for case-insensitive search in a GtkTextBuffer. this is non-trivial, as * searches on a utf-8 text stream involve a norm(casefold(norm(utf8))) * operation which can be costly on large buffers. luckily for us, it's * not the case on a set of definitions. */ #define GTK_TEXT_UNKNOWN_CHAR 0xFFFC /* this function acts like g_utf8_offset_to_pointer() except that if it finds a * decomposable character it consumes the decomposition length from the given * offset. So it's useful when the offset was calculated for the normalized * version of str, but we need a pointer to str itself. */ static const gchar * pointer_from_offset_skipping_decomp (const gchar *str, gint offset) { gchar *casefold, *normal; const gchar *p, *q; p = str; while (offset > 0) { q = g_utf8_next_char (p); casefold = g_utf8_casefold (p, q - p); normal = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); offset -= g_utf8_strlen (normal, -1); g_free (casefold); g_free (normal); p = q; } return p; } static gboolean exact_prefix_cmp (const gchar *string, const gchar *prefix, guint prefix_len) { GUnicodeType type; if (strncmp (string, prefix, prefix_len) != 0) return FALSE; if (string[prefix_len] == '\0') return TRUE; type = g_unichar_type (g_utf8_get_char (string + prefix_len)); /* If string contains prefix, check that prefix is not followed * by a unicode mark symbol, e.g. that trailing 'a' in prefix * is not part of two-char a-with-hat symbol in string. */ return type != G_UNICODE_SPACING_MARK && type != G_UNICODE_ENCLOSING_MARK && type != G_UNICODE_NON_SPACING_MARK; } static const gchar * utf8_strcasestr (const gchar *haystack, const gchar *needle) { gsize needle_len; gsize haystack_len; const gchar *ret = NULL; gchar *p; gchar *casefold; gchar *caseless_haystack; gint i; g_return_val_if_fail (haystack != NULL, NULL); g_return_val_if_fail (needle != NULL, NULL); casefold = g_utf8_casefold (haystack, -1); caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); needle_len = g_utf8_strlen (needle, -1); haystack_len = g_utf8_strlen (caseless_haystack, -1); if (needle_len == 0) { ret = (gchar *)haystack; goto finally_1; } if (haystack_len < needle_len) { ret = NULL; goto finally_1; } p = (gchar*)caseless_haystack; needle_len = strlen (needle); i = 0; while (*p) { if (exact_prefix_cmp (p, needle, needle_len)) { ret = pointer_from_offset_skipping_decomp (haystack, i); goto finally_1; } p = g_utf8_next_char (p); i++; } finally_1: g_free (caseless_haystack); return ret; } static const gchar * utf8_strrcasestr (const gchar *haystack, const gchar *needle) { gsize needle_len; gsize haystack_len; const gchar *ret = NULL; gchar *p; gchar *casefold; gchar *caseless_haystack; gint i; g_return_val_if_fail (haystack != NULL, NULL); g_return_val_if_fail (needle != NULL, NULL); casefold = g_utf8_casefold (haystack, -1); caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); needle_len = g_utf8_strlen (needle, -1); haystack_len = g_utf8_strlen (caseless_haystack, -1); if (needle_len == 0) { ret = (gchar *)haystack; goto finally_1; } if (haystack_len < needle_len) { ret = NULL; goto finally_1; } i = haystack_len - needle_len; p = g_utf8_offset_to_pointer (caseless_haystack, i); needle_len = strlen (needle); while (p >= caseless_haystack) { if (exact_prefix_cmp (p, needle, needle_len)) { ret = pointer_from_offset_skipping_decomp (haystack, i); goto finally_1; } p = g_utf8_prev_char (p); i--; } finally_1: g_free (caseless_haystack); return ret; } static gboolean utf8_caselessnmatch (const char *s1, const char *s2, gssize n1, gssize n2) { gchar *casefold; gchar *normalized_s1; gchar *normalized_s2; gint len_s1; gint len_s2; gboolean ret = FALSE; g_return_val_if_fail (s1 != NULL, FALSE); g_return_val_if_fail (s2 != NULL, FALSE); g_return_val_if_fail (n1 > 0, FALSE); g_return_val_if_fail (n2 > 0, FALSE); casefold = g_utf8_casefold (s1, n1); normalized_s1 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); casefold = g_utf8_casefold (s2, n2); normalized_s2 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); len_s1 = strlen (normalized_s1); len_s2 = strlen (normalized_s2); if (len_s1 < len_s2) goto finally_2; ret = (strncmp (normalized_s1, normalized_s2, len_s2) == 0); finally_2: g_free (normalized_s1); g_free (normalized_s2); return ret; } /* FIXME: total horror */ static gboolean char_is_invisible (const GtkTextIter *iter) { GSList *tags; gboolean invisible = FALSE; tags = gtk_text_iter_get_tags (iter); while (tags) { gboolean this_invisible, invisible_set; g_object_get (tags->data, "invisible", &this_invisible, "invisible-set", &invisible_set, NULL); if (invisible_set) invisible = this_invisible; tags = g_slist_delete_link (tags, tags); } return invisible; } static void forward_chars_with_skipping (GtkTextIter *iter, gint count, gboolean skip_invisible, gboolean skip_nontext, gboolean skip_decomp) { gint i; g_return_if_fail (count >= 0); i = count; while (i > 0) { gboolean ignored = FALSE; /* minimal workaround to avoid the infinite loop of bug #168247. * It doesn't fix the problemjust the symptom... */ if (gtk_text_iter_is_end (iter)) return; if (skip_nontext && gtk_text_iter_get_char (iter) == GTK_TEXT_UNKNOWN_CHAR) ignored = TRUE; /* FIXME: char_is_invisible() gets list of tags for each char there, and checks every tag. It doesn't sound like a good idea. */ if (!ignored && skip_invisible && char_is_invisible (iter)) ignored = TRUE; if (!ignored && skip_decomp) { /* being UTF8 correct sucks; this accounts for extra offsets coming from canonical decompositions of UTF8 characters (e.g. accented characters) which g_utf8_normalize() performs */ gchar *normal; gchar buffer[6]; gint buffer_len; buffer_len = g_unichar_to_utf8 (gtk_text_iter_get_char (iter), buffer); normal = g_utf8_normalize (buffer, buffer_len, G_NORMALIZE_NFD); i -= (g_utf8_strlen (normal, -1) - 1); g_free (normal); } gtk_text_iter_forward_char (iter); if (!ignored) --i; } } static gboolean lines_match (const GtkTextIter *start, const gchar **lines, gboolean visible_only, gboolean slice, GtkTextIter *match_start, GtkTextIter *match_end) { GtkTextIter next; gchar *line_text; const gchar *found; gint offset; if (*lines == NULL || **lines == '\0') { if (match_start) *match_start = *start; if (match_end) *match_end = *start; return TRUE; } next = *start; gtk_text_iter_forward_line (&next); /* No more text in buffer, but *lines is nonempty */ if (gtk_text_iter_equal (start, &next)) return FALSE; if (slice) { if (visible_only) line_text = gtk_text_iter_get_visible_slice (start, &next); else line_text = gtk_text_iter_get_slice (start, &next); } else { if (visible_only) line_text = gtk_text_iter_get_visible_text (start, &next); else line_text = gtk_text_iter_get_text (start, &next); } if (match_start) /* if this is the first line we're matching */ { found = utf8_strcasestr (line_text, *lines); } else { /* If it's not the first line, we have to match from the * start of the line. */ if (utf8_caselessnmatch (line_text, *lines, strlen (line_text), strlen (*lines))) found = line_text; else found = NULL; } if (found == NULL) { g_free (line_text); return FALSE; } /* Get offset to start of search string */ offset = g_utf8_strlen (line_text, found - line_text); next = *start; /* If match start needs to be returned, set it to the * start of the search string. */ forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE); if (match_start) { *match_start = next; } /* Go to end of search string */ forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE); g_free (line_text); ++lines; if (match_end) *match_end = next; /* pass NULL for match_start, since we don't need to find the * start again. */ return lines_match (&next, lines, visible_only, slice, NULL, match_end); } static gboolean backward_lines_match (const GtkTextIter *start, const gchar **lines, gboolean visible_only, gboolean slice, GtkTextIter *match_start, GtkTextIter *match_end) { GtkTextIter line, next; gchar *line_text; const gchar *found; gint offset; if (*lines == NULL || **lines == '\0') { if (match_start) *match_start = *start; if (match_end) *match_end = *start; return TRUE; } line = next = *start; if (gtk_text_iter_get_line_offset (&next) == 0) { if (!gtk_text_iter_backward_line (&next)) return FALSE; } else gtk_text_iter_set_line_offset (&next, 0); if (slice) { if (visible_only) line_text = gtk_text_iter_get_visible_slice (&next, &line); else line_text = gtk_text_iter_get_slice (&next, &line); } else { if (visible_only) line_text = gtk_text_iter_get_visible_text (&next, &line); else line_text = gtk_text_iter_get_text (&next, &line); } if (match_start) /* if this is the first line we're matching */ { found = utf8_strrcasestr (line_text, *lines); } else { /* If it's not the first line, we have to match from the * start of the line. */ if (utf8_caselessnmatch (line_text, *lines, strlen (line_text), strlen (*lines))) found = line_text; else found = NULL; } if (found == NULL) { g_free (line_text); return FALSE; } /* Get offset to start of search string */ offset = g_utf8_strlen (line_text, found - line_text); forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE); /* If match start needs to be returned, set it to the * start of the search string. */ if (match_start) { *match_start = next; } /* Go to end of search string */ forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE); g_free (line_text); ++lines; if (match_end) *match_end = next; /* try to match the rest of the lines forward, passing NULL * for match_start so lines_match will try to match the entire * line */ return lines_match (&next, lines, visible_only, slice, NULL, match_end); } /* strsplit () that retains the delimiter as part of the string. */ static gchar ** breakup_string (const char *string, const char *delimiter, gint max_tokens) { GSList *string_list = NULL, *slist; gchar **str_array, *s, *casefold, *new_string; guint i, n = 1; g_return_val_if_fail (string != NULL, NULL); g_return_val_if_fail (delimiter != NULL, NULL); if (max_tokens < 1) max_tokens = G_MAXINT; s = strstr (string, delimiter); if (s) { guint delimiter_len = strlen (delimiter); do { guint len; len = s - string + delimiter_len; new_string = g_new (gchar, len + 1); strncpy (new_string, string, len); new_string[len] = 0; casefold = g_utf8_casefold (new_string, -1); g_free (new_string); new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); string_list = g_slist_prepend (string_list, new_string); n++; string = s + delimiter_len; s = strstr (string, delimiter); } while (--max_tokens && s); } if (*string) { n++; casefold = g_utf8_casefold (string, -1); new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); g_free (casefold); string_list = g_slist_prepend (string_list, new_string); } str_array = g_new (gchar*, n); i = n - 1; str_array[i--] = NULL; for (slist = string_list; slist; slist = slist->next) str_array[i--] = slist->data; g_slist_free (string_list); return str_array; } static gboolean gdict_defbox_iter_forward_search (const GtkTextIter *iter, const gchar *str, GtkTextIter *match_start, GtkTextIter *match_end, const GtkTextIter *limit) { gchar **lines = NULL; GtkTextIter match; gboolean retval = FALSE; GtkTextIter search; g_return_val_if_fail (iter != NULL, FALSE); g_return_val_if_fail (str != NULL, FALSE); if (limit && gtk_text_iter_compare (iter, limit) >= 0) return FALSE; if (*str == '\0') { /* If we can move one char, return the empty string there */ match = *iter; if (gtk_text_iter_forward_char (&match)) { if (limit && gtk_text_iter_equal (&match, limit)) return FALSE; if (match_start) *match_start = match; if (match_end) *match_end = match; return TRUE; } else return FALSE; } /* locate all lines */ lines = breakup_string (str, "\n", -1); search = *iter; /* This loop has an inefficient worst-case, where * gtk_text_iter_get_text () is called repeatedly on * a single line. */ do { GtkTextIter end; gboolean res; if (limit && gtk_text_iter_compare (&search, limit) >= 0) break; res = lines_match (&search, (const gchar**)lines, TRUE, FALSE, &match, &end); if (res) { if (limit == NULL || (limit && gtk_text_iter_compare (&end, limit) <= 0)) { retval = TRUE; if (match_start) *match_start = match; if (match_end) *match_end = end; } break; } } while (gtk_text_iter_forward_line (&search)); g_strfreev ((gchar**) lines); return retval; } static gboolean gdict_defbox_iter_backward_search (const GtkTextIter *iter, const gchar *str, GtkTextIter *match_start, GtkTextIter *match_end, const GtkTextIter *limit) { gchar **lines = NULL; GtkTextIter match; gboolean retval = FALSE; GtkTextIter search; g_return_val_if_fail (iter != NULL, FALSE); g_return_val_if_fail (str != NULL, FALSE); if (limit && gtk_text_iter_compare (iter, limit) <= 0) return FALSE; if (*str == '\0') { /* If we can move one char, return the empty string there */ match = *iter; if (gtk_text_iter_backward_char (&match)) { if (limit && gtk_text_iter_equal (&match, limit)) return FALSE; if (match_start) *match_start = match; if (match_end) *match_end = match; return TRUE; } else return FALSE; } /* locate all lines */ lines = breakup_string (str, "\n", -1); search = *iter; /* This loop has an inefficient worst-case, where * gtk_text_iter_get_text () is called repeatedly on * a single line. */ while (TRUE) { GtkTextIter end; gboolean res; if (limit && gtk_text_iter_compare (&search, limit) <= 0) break; res = backward_lines_match (&search, (const gchar**)lines, TRUE, FALSE, &match, &end); if (res) { if (limit == NULL || (limit && gtk_text_iter_compare (&end, limit) > 0)) { retval = TRUE; if (match_start) *match_start = match; if (match_end) *match_end = end; } break; } if (gtk_text_iter_get_line_offset (&search) == 0) { if (!gtk_text_iter_backward_line (&search)) break; } else gtk_text_iter_set_line_offset (&search, 0); } g_strfreev ((gchar**) lines); return retval; } static gboolean gdict_defbox_find_backward (GdictDefbox *defbox, const gchar *text) { GdictDefboxPrivate *priv = defbox->priv; GtkTextIter start_iter, end_iter; GtkTextIter match_start, match_end; GtkTextIter iter; GtkTextMark *last_search; gboolean res; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); gtk_text_buffer_get_bounds (priv->buffer, &start_iter, &end_iter); /* if there already has been another result, begin from there */ last_search = gtk_text_buffer_get_mark (priv->buffer, "last-search-prev"); if (last_search) gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, last_search); else iter = end_iter; res = gdict_defbox_iter_backward_search (&iter, text, &match_start, &match_end, NULL); if (res) { gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (priv->text_view), &match_start, 0.0, TRUE, 0.0, 0.0); gtk_text_buffer_place_cursor (priv->buffer, &match_end); gtk_text_buffer_move_mark (priv->buffer, gtk_text_buffer_get_mark (priv->buffer, "selection_bound"), &match_start); gtk_text_buffer_create_mark (priv->buffer, "last-search-prev", &match_start, FALSE); gtk_text_buffer_create_mark (priv->buffer, "last-search-next", &match_end, FALSE); return TRUE; } return FALSE; } static gboolean hide_find_pane (gpointer user_data) { GdictDefbox *defbox = user_data; gtk_widget_hide (defbox->priv->find_pane); defbox->priv->show_find = FALSE; gtk_widget_grab_focus (defbox->priv->text_view); defbox->priv->hide_timeout = 0; return FALSE; } static void find_prev_clicked_cb (GtkWidget *widget, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; const gchar *text; gboolean found; gtk_widget_hide (priv->find_label); text = gtk_entry_get_text (GTK_ENTRY (priv->find_entry)); if (!text) return; found = gdict_defbox_find_backward (defbox, text); if (!found) { gchar *str; str = g_strconcat (" ", _("Not found"), "", NULL); gtk_label_set_markup (GTK_LABEL (priv->find_label), str); gtk_widget_show (priv->find_label); g_free (str); } if (priv->hide_timeout) { g_source_remove (priv->hide_timeout); priv->hide_timeout = g_timeout_add_seconds (5, hide_find_pane, defbox); } } static gboolean gdict_defbox_find_forward (GdictDefbox *defbox, const gchar *text, gboolean is_typing) { GdictDefboxPrivate *priv = defbox->priv; GtkTextIter start_iter, end_iter; GtkTextIter match_start, match_end; GtkTextIter iter; GtkTextMark *last_search; gboolean res; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); gtk_text_buffer_get_bounds (priv->buffer, &start_iter, &end_iter); if (!is_typing) { /* if there already has been another result, begin from there */ last_search = gtk_text_buffer_get_mark (priv->buffer, "last-search-next"); if (last_search) gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, last_search); else iter = start_iter; } else { last_search = gtk_text_buffer_get_mark (priv->buffer, "last-search-prev"); if (last_search) gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, last_search); else iter = start_iter; } res = gdict_defbox_iter_forward_search (&iter, text, &match_start, &match_end, NULL); if (res) { gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (priv->text_view), &match_start, 0.0, TRUE, 0.0, 0.0); gtk_text_buffer_place_cursor (priv->buffer, &match_end); gtk_text_buffer_move_mark (priv->buffer, gtk_text_buffer_get_mark (priv->buffer, "selection_bound"), &match_start); gtk_text_buffer_create_mark (priv->buffer, "last-search-prev", &match_start, FALSE); gtk_text_buffer_create_mark (priv->buffer, "last-search-next", &match_end, FALSE); return TRUE; } return FALSE; } static void find_next_clicked_cb (GtkWidget *widget, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; const gchar *text; gboolean found; gtk_widget_hide (priv->find_label); text = gtk_entry_get_text (GTK_ENTRY (priv->find_entry)); if (!text) return; found = gdict_defbox_find_forward (defbox, text, FALSE); if (!found) { gchar *str; str = g_strconcat (" ", _("Not found"), "", NULL); gtk_label_set_markup (GTK_LABEL (priv->find_label), str); gtk_widget_show (priv->find_label); g_free (str); } if (priv->hide_timeout) { g_source_remove (priv->hide_timeout); priv->hide_timeout = g_timeout_add_seconds (5, hide_find_pane, defbox); } } static void find_entry_changed_cb (GtkWidget *widget, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; gchar *text; gboolean found; gtk_widget_hide (priv->find_label); text = gtk_editable_get_chars (GTK_EDITABLE (widget), 0, -1); if (!text) return; found = gdict_defbox_find_forward (defbox, text, TRUE); if (!found) { gchar *str; str = g_strconcat (" ", _("Not found"), "", NULL); gtk_label_set_markup (GTK_LABEL (priv->find_label), str); gtk_widget_show (priv->find_label); g_free (str); } g_free (text); if (priv->hide_timeout) { g_source_remove (priv->hide_timeout); priv->hide_timeout = g_timeout_add_seconds (5, hide_find_pane, defbox); } } static void close_button_clicked (GtkButton *button, gpointer data) { GdictDefboxPrivate *priv = GDICT_DEFBOX (data)->priv; if (priv->hide_timeout) g_source_remove (priv->hide_timeout); (void) hide_find_pane (data); } static GtkWidget * create_find_pane (GdictDefbox *defbox) { GdictDefboxPrivate *priv; GtkWidget *find_pane; GtkWidget *label; GtkWidget *sep; GtkWidget *hbox1, *hbox2; GtkWidget *button; priv = defbox->priv; find_pane = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_container_set_border_width (GTK_CONTAINER (find_pane), 0); hbox1 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_box_pack_start (GTK_BOX (find_pane), hbox1, TRUE, TRUE, 0); gtk_widget_show (hbox1); button = gtk_button_new (); gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); gtk_button_set_image (GTK_BUTTON (button), gtk_image_new_from_icon_name ("window-close", GTK_ICON_SIZE_BUTTON)); g_signal_connect (button, "clicked", G_CALLBACK (close_button_clicked), defbox); gtk_box_pack_start (GTK_BOX (hbox1), button, FALSE, FALSE, 0); gtk_widget_show (button); hbox2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); gtk_box_pack_start (GTK_BOX (hbox1), hbox2, TRUE, TRUE, 0); gtk_widget_show (hbox2); label = gtk_label_new_with_mnemonic (_("F_ind:")); gtk_box_pack_start (GTK_BOX (hbox2), label, FALSE, FALSE, 0); priv->find_entry = gtk_entry_new (); g_signal_connect (priv->find_entry, "changed", G_CALLBACK (find_entry_changed_cb), defbox); gtk_box_pack_start (GTK_BOX (hbox2), priv->find_entry, TRUE, TRUE, 0); gtk_label_set_mnemonic_widget (GTK_LABEL (label), priv->find_entry); sep = gtk_separator_new (GTK_ORIENTATION_VERTICAL); gtk_box_pack_start (GTK_BOX (hbox1), sep, FALSE, FALSE, 0); gtk_widget_show (sep); priv->find_prev = gtk_button_new_with_mnemonic (_("_Previous")); gtk_button_set_image (GTK_BUTTON (priv->find_prev), gtk_image_new_from_icon_name ("go-previous", GTK_ICON_SIZE_MENU)); g_signal_connect (priv->find_prev, "clicked", G_CALLBACK (find_prev_clicked_cb), defbox); gtk_box_pack_start (GTK_BOX (hbox1), priv->find_prev, FALSE, FALSE, 0); priv->find_next = gtk_button_new_with_mnemonic (_("_Next")); gtk_button_set_image (GTK_BUTTON (priv->find_next), gtk_image_new_from_icon_name ("go-next", GTK_ICON_SIZE_MENU)); g_signal_connect (priv->find_next, "clicked", G_CALLBACK (find_next_clicked_cb), defbox); gtk_box_pack_start (GTK_BOX (hbox1), priv->find_next, FALSE, FALSE, 0); priv->find_label = gtk_label_new (NULL); gtk_label_set_use_markup (GTK_LABEL (priv->find_label), TRUE); gtk_box_pack_end (GTK_BOX (find_pane), priv->find_label, FALSE, FALSE, 0); gtk_widget_hide (priv->find_label); return find_pane; } static void gdict_defbox_init_tags (GdictDefbox *defbox) { GdictDefboxPrivate *priv = defbox->priv; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); gtk_text_buffer_create_tag (priv->buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL); gtk_text_buffer_create_tag (priv->buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag (priv->buffer, "underline", "underline", PANGO_UNDERLINE_SINGLE, NULL); gtk_text_buffer_create_tag (priv->buffer, "big", "scale", 1.6, NULL); gtk_text_buffer_create_tag (priv->buffer, "small", "scale", PANGO_SCALE_SMALL, NULL); { GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (defbox)); gboolean prefer_dark = FALSE; GdkRGBA rgba; /* HACK: we're hardcoding the Adwaita values because GtkTextTag * cannot be styled via CSS */ g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark, NULL); if (!prefer_dark) gdk_rgba_parse (&rgba, "#2a76c6"); else gdk_rgba_parse (&rgba, "#4a90d9"); priv->link_tag = gtk_text_buffer_create_tag (priv->buffer, "link", "underline", PANGO_UNDERLINE_SINGLE, "foreground-rgba", &rgba, NULL); if (!prefer_dark) gdk_rgba_parse (&rgba, "#215d9c"); else gdk_rgba_parse (&rgba, "#2a76c6"); priv->visited_link_tag = gtk_text_buffer_create_tag (priv->buffer, "visited-link", "underline", PANGO_UNDERLINE_SINGLE, "foreground-rgba", &rgba, NULL); } gtk_text_buffer_create_tag (priv->buffer, "phonetic", "foreground", "dark gray", NULL); gtk_text_buffer_create_tag (priv->buffer, "query-title", "left-margin", QUERY_MARGIN, "pixels-above-lines", 5, "pixels-below-lines", 20, NULL); gtk_text_buffer_create_tag (priv->buffer, "query-from", "foreground", "dark gray", "scale", PANGO_SCALE_SMALL, "left-margin", QUERY_MARGIN, "pixels-above-lines", 5, "pixels-below-lines", 10, NULL); gtk_text_buffer_create_tag (priv->buffer, "error-title", "foreground", "dark red", "left-margin", ERROR_MARGIN, NULL); gtk_text_buffer_create_tag (priv->buffer, "error-message", "left-margin", ERROR_MARGIN, NULL); } static void follow_if_is_link (GdictDefbox *defbox, GtkTextView *text_view, GtkTextIter *iter) { GSList *tags, *l; tags = gtk_text_iter_get_tags (iter); for (l = tags; l != NULL; l = l->next) { GtkTextTag *tag = l->data; gchar *name; g_object_get (G_OBJECT (tag), "name", &name, NULL); if (name && (strcmp (name, "link") == 0 || strcmp (name, "visited-link") == 0)) { GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view); GtkTextIter start, end; gchar *link_str; start = *iter; end = *iter; gtk_text_iter_backward_to_tag_toggle (&start, tag); gtk_text_iter_forward_to_tag_toggle (&end, tag); link_str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); g_signal_emit (defbox, gdict_defbox_signals[LINK_CLICKED], 0, link_str); g_free (link_str); g_free (name); break; } g_free (name); } g_slist_free (tags); } static gboolean defbox_event_after_cb (GtkWidget *text_view, GdkEvent *event, GdictDefbox *defbox) { GtkTextIter iter; GtkTextBuffer *buffer; GdkEventButton *button_event; gint bx, by; if (event->type != GDK_BUTTON_RELEASE) return FALSE; button_event = (GdkEventButton *) event; if (button_event->button != 1) return FALSE; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); if (gtk_text_buffer_get_has_selection (buffer)) return FALSE; gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view), GTK_TEXT_WINDOW_WIDGET, button_event->x, button_event->y, &bx, &by); gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (text_view), &iter, bx, by); follow_if_is_link (defbox, GTK_TEXT_VIEW (text_view), &iter); return FALSE; } static void set_cursor_if_appropriate (GdictDefbox *defbox, GtkTextView *text_view, gint x, gint y) { GdictDefboxPrivate *priv; GSList *tags, *l; GtkTextIter iter; gboolean hovering = FALSE; priv = defbox->priv; if (!priv->hand_cursor) { GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (defbox)); priv->hand_cursor = gdk_cursor_new_for_display (display, GDK_HAND2); } if (!priv->regular_cursor) { GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (defbox)); priv->regular_cursor = gdk_cursor_new_for_display (display, GDK_XTERM); } gtk_text_view_get_iter_at_location (text_view, &iter, x, y); tags = gtk_text_iter_get_tags (&iter); for (l = tags; l != NULL; l = l->next) { GtkTextTag *tag = l->data; gchar *name; g_object_get (G_OBJECT (tag), "name", &name, NULL); if (name && (strcmp (name, "link") == 0 || strcmp (name, "visited-link") == 0)) { hovering = TRUE; g_free (name); break; } g_free (name); } if (hovering != defbox->priv->is_hovering) { defbox->priv->is_hovering = hovering; if (defbox->priv->is_hovering) gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), defbox->priv->hand_cursor); else gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), defbox->priv->regular_cursor); } if (tags) g_slist_free (tags); } static gboolean defbox_motion_notify_cb (GtkWidget *text_view, GdkEventMotion *event, GdictDefbox *defbox) { gint bx, by; gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view), GTK_TEXT_WINDOW_WIDGET, event->x, event->y, &bx, &by); set_cursor_if_appropriate (defbox, GTK_TEXT_VIEW (text_view), bx, by); return FALSE; } static gboolean defbox_visibility_notify_cb (GtkWidget *text_view, GdkEventVisibility *event, GdictDefbox *defbox) { GdkDisplay *display; GdkSeat *seat; GdkDevice *pointer; gint wx, wy; gint bx, by; display = gdk_window_get_display (event->window); seat = gdk_display_get_default_seat (display); pointer = gdk_seat_get_pointer (seat); gdk_window_get_device_position (gtk_widget_get_window (text_view), pointer, &wx, &wy, NULL); gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view), GTK_TEXT_WINDOW_WIDGET, wx, wy, &bx, &by); set_cursor_if_appropriate (defbox, GTK_TEXT_VIEW (text_view), bx, by); return FALSE; } static GObject * gdict_defbox_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_params) { GdictDefbox *defbox; GdictDefboxPrivate *priv; GObject *object; GtkWidget *sw; object = G_OBJECT_CLASS (gdict_defbox_parent_class)->constructor (type, n_construct_properties, construct_params); defbox = GDICT_DEFBOX (object); priv = defbox->priv; sw = gtk_scrolled_window_new (NULL, NULL); gtk_widget_set_vexpand (sw, TRUE); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_IN); gtk_box_pack_start (GTK_BOX (defbox), sw, TRUE, TRUE, 0); gtk_widget_show (sw); priv->buffer = gtk_text_buffer_new (NULL); gdict_defbox_init_tags (defbox); priv->text_view = gtk_text_view_new_with_buffer (priv->buffer); gtk_text_view_set_editable (GTK_TEXT_VIEW (priv->text_view), FALSE); gtk_text_view_set_left_margin (GTK_TEXT_VIEW (priv->text_view), 4); gtk_container_add (GTK_CONTAINER (sw), priv->text_view); gtk_widget_show (priv->text_view); priv->find_pane = create_find_pane (defbox); gtk_box_pack_end (GTK_BOX (defbox), priv->find_pane, FALSE, FALSE, 0); /* stuff to make the link machinery work */ g_signal_connect (priv->text_view, "event-after", G_CALLBACK (defbox_event_after_cb), defbox); g_signal_connect (priv->text_view, "motion-notify-event", G_CALLBACK (defbox_motion_notify_cb), defbox); g_signal_connect (priv->text_view, "visibility-notify-event", G_CALLBACK (defbox_visibility_notify_cb), defbox); return object; } /* we override the GtkWidget::show_all method since we have widgets * we don't want to show, such as the find pane */ static void gdict_defbox_show_all (GtkWidget *widget) { GdictDefbox *defbox = GDICT_DEFBOX (widget); GdictDefboxPrivate *priv = defbox->priv; gtk_widget_show (widget); if (priv->show_find) gtk_widget_show_all (priv->find_pane); } static void gdict_defbox_real_show_find (GdictDefbox *defbox) { gtk_widget_show_all (defbox->priv->find_pane); defbox->priv->show_find = TRUE; gtk_widget_grab_focus (defbox->priv->find_entry); defbox->priv->hide_timeout = g_timeout_add_seconds (5, hide_find_pane, defbox); } static void gdict_defbox_real_find_next (GdictDefbox *defbox) { /* synthetize a "clicked" signal to the "next" button */ gtk_button_clicked (GTK_BUTTON (defbox->priv->find_next)); } static void gdict_defbox_real_find_previous (GdictDefbox *defbox) { /* synthetize a "clicked" signal to the "prev" button */ gtk_button_clicked (GTK_BUTTON (defbox->priv->find_prev)); } static void gdict_defbox_real_hide_find (GdictDefbox *defbox) { gtk_widget_hide (defbox->priv->find_pane); defbox->priv->show_find = FALSE; gtk_widget_grab_focus (defbox->priv->text_view); if (defbox->priv->hide_timeout) { g_source_remove (defbox->priv->hide_timeout); defbox->priv->hide_timeout = 0; } } static void gdict_defbox_class_init (GdictDefboxClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkBindingSet *binding_set; gobject_class->constructor = gdict_defbox_constructor; gobject_class->set_property = gdict_defbox_set_property; gobject_class->get_property = gdict_defbox_get_property; gobject_class->dispose = gdict_defbox_dispose; gobject_class->finalize = gdict_defbox_finalize; widget_class->show_all = gdict_defbox_show_all; /** * GdictDefbox:word: * * The word to look up. * * Since: 0.10 */ g_object_class_install_property (gobject_class, PROP_WORD, g_param_spec_string ("word", "Word", "The word to look up", NULL, G_PARAM_READWRITE)); /** * GdictDefbox:context: * * The #GdictContext object used to get the word definition. * * Since: 0.1 */ g_object_class_install_property (gobject_class, PROP_CONTEXT, g_param_spec_object ("context", "Context", "The GdictContext object used to get the word definition", GDICT_TYPE_CONTEXT, (G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT))); /** * GdictDefbox:database * * The database used by the #GdictDefbox bound to this object to get the word * definition. * * Since: 0.1 */ g_object_class_install_property (gobject_class, PROP_DATABASE, g_param_spec_string ("database", "Database", "The database used to query the GdictContext", GDICT_DEFAULT_DATABASE, (G_PARAM_READABLE | G_PARAM_WRITABLE))); /** * GdictDefbox:font-name * * The name of the font used by the #GdictDefbox to display the definitions. * use the same string you use for pango_font_description_from_string(). * * Since: 0.3 */ g_object_class_install_property (gobject_class, PROP_FONT_NAME, g_param_spec_string ("font-name", "Font Name", "The font to be used by the defbox", GDICT_DEFAULT_FONT_NAME, (G_PARAM_READABLE | G_PARAM_WRITABLE))); gdict_defbox_signals[SHOW_FIND] = g_signal_new ("show-find", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GdictDefboxClass, show_find), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); gdict_defbox_signals[FIND_PREVIOUS] = g_signal_new ("find-previous", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GdictDefboxClass, find_previous), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); gdict_defbox_signals[FIND_NEXT] = g_signal_new ("find-next", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GdictDefboxClass, find_next), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); gdict_defbox_signals[HIDE_FIND] = g_signal_new ("hide-find", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GdictDefboxClass, hide_find), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); gdict_defbox_signals[LINK_CLICKED] = g_signal_new ("link-clicked", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GdictDefboxClass, link_clicked), NULL, NULL, gdict_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); klass->show_find = gdict_defbox_real_show_find; klass->hide_find = gdict_defbox_real_hide_find; klass->find_next = gdict_defbox_real_find_next; klass->find_previous = gdict_defbox_real_find_previous; binding_set = gtk_binding_set_by_class (klass); gtk_binding_entry_add_signal (binding_set, GDK_KEY_f, GDK_CONTROL_MASK, "show-find", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_g, GDK_CONTROL_MASK, "find-next", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_g, GDK_SHIFT_MASK | GDK_CONTROL_MASK, "find-previous", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "hide-find", 0); } static void gdict_defbox_init (GdictDefbox *defbox) { GdictDefboxPrivate *priv; gtk_orientable_set_orientation (GTK_ORIENTABLE (defbox), GTK_ORIENTATION_VERTICAL); gtk_box_set_spacing (GTK_BOX (defbox), 6); priv = gdict_defbox_get_instance_private (defbox); defbox->priv = priv; priv->context = NULL; priv->database = g_strdup (GDICT_DEFAULT_DATABASE); priv->font_name = g_strdup (GDICT_DEFAULT_FONT_NAME); priv->word = NULL; priv->definitions = NULL; priv->busy_cursor = NULL; priv->hand_cursor = NULL; priv->regular_cursor = NULL; priv->show_find = FALSE; priv->is_searching = FALSE; priv->is_hovering = FALSE; priv->hide_timeout = 0; } /** * gdict_defbox_new: * * Creates a new #GdictDefbox widget. Use this widget to search for * a word using a #GdictContext, and to show the resulting definition(s). * You must set a #GdictContext for this widget using * gdict_defbox_set_context(). * * Return value: a new #GdictDefbox widget. * * Since: 0.1 */ GtkWidget * gdict_defbox_new (void) { return g_object_new (GDICT_TYPE_DEFBOX, NULL); } /** * gdict_defbox_new_with_context: * @context: a #GdictContext * * Creates a new #GdictDefbox widget. Use this widget to search for * a word using @context, and to show the resulting definition. * * Return value: a new #GdictDefbox widget. * * Since: 0.1 */ GtkWidget * gdict_defbox_new_with_context (GdictContext *context) { g_return_val_if_fail (GDICT_IS_CONTEXT (context), NULL); return g_object_new (GDICT_TYPE_DEFBOX, "context", context, NULL); } /** * gdict_defbox_set_context: * @defbox: a #GdictDefbox * @context: a #GdictContext * * Sets @context as the #GdictContext to be used by @defbox in order * to retrieve the definitions of a word. * * Since: 0.1 */ void gdict_defbox_set_context (GdictDefbox *defbox, GdictContext *context) { g_return_if_fail (GDICT_IS_DEFBOX (defbox)); g_return_if_fail (context == NULL || GDICT_IS_CONTEXT (context)); g_object_set (defbox, "context", context, NULL); } /** * gdict_defbox_get_context: * @defbox: a #GdictDefbox * * Gets the #GdictContext used by @defbox. * * Return value: a #GdictContext. * * Since: 0.1 */ GdictContext * gdict_defbox_get_context (GdictDefbox *defbox) { g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); return defbox->priv->context; } /** * gdict_defbox_set_database: * @defbox: a #GdictDefbox * @database: a database * * Sets @database as the database used by the #GdictContext bound to @defbox * to query for word definitions. * * Since: 0.1 */ void gdict_defbox_set_database (GdictDefbox *defbox, const gchar *database) { GdictDefboxPrivate *priv; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; g_free (priv->database); priv->database = g_strdup (database); g_object_notify (G_OBJECT (defbox), "database"); } /** * gdict_defbox_get_database: * @defbox: a #GdictDefbox * * Gets the database used by @defbox. See gdict_defbox_set_database(). * * Return value: the name of a database. The return string is owned by * the #GdictDefbox widget and should not be modified or freed. * * Since: 0.1 */ const gchar * gdict_defbox_get_database (GdictDefbox *defbox) { g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); return defbox->priv->database; } /** * gdict_defbox_get_word: * @defbox: a #GdictDefbox * * Retrieves the word being looked up. * * Return value: the word looked up, or %NULL. The returned string is * owned by the #GdictDefbox widget and should never be modified or * freed. * * Since: 0.12 */ const gchar * gdict_defbox_get_word (GdictDefbox *defbox) { g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); return defbox->priv->word; } /** * gdict_defbox_set_show_find: * @defbox: a #GdictDefbox * @show_find: %TRUE to show the find pane * * Whether @defbox should show the find pane. * * Since: 0.1 */ void gdict_defbox_set_show_find (GdictDefbox *defbox, gboolean show_find) { GdictDefboxPrivate *priv; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; if (priv->show_find == show_find) return; priv->show_find = show_find; if (priv->show_find) { gtk_widget_show_all (priv->find_pane); gtk_widget_grab_focus (priv->find_entry); if (!priv->hide_timeout) priv->hide_timeout = g_timeout_add_seconds (5, hide_find_pane, defbox); } else { gtk_widget_hide (priv->find_pane); if (priv->hide_timeout) { g_source_remove (priv->hide_timeout); priv->hide_timeout = 0; } } } /** * gdict_defbox_get_show_find: * @defbox: a #GdictDefbox * * Gets whether the find pane should be visible or not. * * Return value: %TRUE if the find pane is visible. * * Since: 0.1 */ gboolean gdict_defbox_get_show_find (GdictDefbox *defbox) { g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), FALSE); return (defbox->priv->show_find == TRUE); } static void lookup_start_cb (GdictContext *context, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; GdkWindow *window; priv->is_searching = TRUE; if (!priv->busy_cursor) { GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (defbox)); priv->busy_cursor = gdk_cursor_new_for_display (display, GDK_WATCH); } window = gtk_text_view_get_window (GTK_TEXT_VIEW (priv->text_view), GTK_TEXT_WINDOW_WIDGET); gdk_window_set_cursor (window, priv->busy_cursor); } static void lookup_end_cb (GdictContext *context, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; GtkTextBuffer *buffer; GtkTextIter start; GdkWindow *window; /* explicitely move the cursor to the beginning */ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); gtk_text_buffer_get_start_iter (buffer, &start); gtk_text_buffer_place_cursor (buffer, &start); window = gtk_text_view_get_window (GTK_TEXT_VIEW (priv->text_view), GTK_TEXT_WINDOW_WIDGET); gdk_window_set_cursor (window, NULL); priv->is_searching = FALSE; } static void gdict_defbox_insert_word (GdictDefbox *defbox, GtkTextIter *iter, const gchar *word) { GdictDefboxPrivate *priv; gchar *text; if (!word) return; g_assert (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); text = g_strdup_printf ("%s\n", word); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, iter, text, strlen (text), "big", "bold", "query-title", NULL); g_free (text); } /* escape a link string; links are expressed as "{...}". * the link with the '{}' removed is stored inside link_str, while * the returned value is a pointer to what follows the trailing '}'. * link_str is allocated and should be freed. */ static const gchar * escape_link (const gchar *str, gchar **link_str) { gsize str_len; GString *link_buf; const gchar *p; str_len = strlen (str); link_buf = g_string_sized_new (str_len - 2); for (p = str + 1; *p != '}'; p++) { link_buf = g_string_append_c (link_buf, *p); } if (link_str) *link_str = g_string_free (link_buf, FALSE); p++; return p; } static const gchar * escape_phonethic (const gchar *str, gchar **phon_str) { gsize str_len; GString *phon_buf; const gchar *p; str_len = strlen (str); phon_buf = g_string_sized_new (str_len - 2); for (p = str + 1; *p != '\\'; p++) { phon_buf = g_string_append_c (phon_buf, *p); } if (phon_str) *phon_str = g_string_free (phon_buf, FALSE); p++; return p; } static void gdict_defbox_insert_body (GdictDefbox *defbox, GtkTextIter *iter, const gchar *body) { GdictDefboxPrivate *priv; gchar **words; gint len, i; GtkTextIter end_iter; if (!body) return; g_assert (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); words = g_strsplit (body, " ", -1); len = g_strv_length (words); end_iter = *iter; for (i = 0; i < len; i++) { gchar *w = words[i]; gint w_len = strlen (w); gchar *begin, *end; if (w_len == 0) continue; begin = g_utf8_offset_to_pointer (w, 0); if (*begin == '{') { end = g_utf8_strrchr (w, -1, '}'); /* see this is a self contained link */ if (end && *end == '}') { const gchar *rest; gchar *link_str; rest = escape_link (w, &link_str); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &end_iter, link_str, -1, "link", NULL); gtk_text_buffer_insert (priv->buffer, &end_iter, rest, -1); gtk_text_buffer_get_end_iter (priv->buffer, &end_iter); gtk_text_buffer_insert (priv->buffer, &end_iter, " ", 1); g_free (link_str); continue; } else { /* uh-oh: the link ends in another word */ GString *buf; gchar *next; gint cur = i; buf = g_string_new (NULL); next = words[cur++]; while (next && (end = g_utf8_strrchr (next, -1, '}')) == NULL) { buf = g_string_append (buf, next); buf = g_string_append_c (buf, ' '); next = words[cur++]; } buf = g_string_append (buf, next); next = g_string_free (buf, FALSE); if (end && *end == '}') { const gchar *rest; gchar *link_str; rest = escape_link (next, &link_str); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &end_iter, link_str, -1, "link", NULL); gtk_text_buffer_insert (priv->buffer, &end_iter, rest, -1); gtk_text_buffer_insert (priv->buffer, &end_iter, " ", 1); g_free (link_str); } g_free (next); i = cur; continue; } } else if (*begin == '\\') { end = g_utf8_strrchr (w, -1, '\\'); if (end && *end == '\\') { const gchar *rest; gchar *phon; rest = escape_phonethic (w, &phon); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &end_iter, phon, -1, "italic", "phonetic", NULL); gtk_text_buffer_insert (priv->buffer, &end_iter, rest, -1); gtk_text_buffer_get_end_iter (priv->buffer, &end_iter); gtk_text_buffer_insert (priv->buffer, &end_iter, " ", -1); g_free (phon); continue; } } gtk_text_buffer_insert (priv->buffer, &end_iter, w, w_len); gtk_text_buffer_get_end_iter (priv->buffer, &end_iter); gtk_text_buffer_insert (priv->buffer, &end_iter, " ", 1); } gtk_text_buffer_get_end_iter (priv->buffer, &end_iter); gtk_text_buffer_insert (priv->buffer, &end_iter, "\n", 1); *iter = end_iter; g_strfreev (words); } static void gdict_defbox_insert_from (GdictDefbox *defbox, GtkTextIter *iter, const gchar *database) { GdictDefboxPrivate *priv; gchar *text; if (!database) return; g_assert (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); text = g_strdup_printf ("\t-- From %s\n\n", database); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, iter, text, strlen (text), "small", "query-from", NULL); g_free (text); } static void gdict_defbox_insert_error (GdictDefbox *defbox, GtkTextIter *iter, const gchar *title, const gchar *message) { GdictDefboxPrivate *priv; GtkTextMark *mark; GtkTextIter cur_iter; if (!title) return; g_assert (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; g_assert (GTK_IS_TEXT_BUFFER (priv->buffer)); mark = gtk_text_buffer_create_mark (priv->buffer, "block-cursor", iter, FALSE); gtk_text_buffer_get_iter_at_mark (priv->buffer, &cur_iter, mark); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &cur_iter, title, strlen (title), "error-title", "big", "bold", NULL); gtk_text_buffer_get_iter_at_mark (priv->buffer, &cur_iter, mark); gtk_text_buffer_insert (priv->buffer, &cur_iter, "\n\n", -1); gtk_text_buffer_get_iter_at_mark (priv->buffer, &cur_iter, mark); gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &cur_iter, message, strlen (message), "error-message", NULL); } static void definition_found_cb (GdictContext *context, GdictDefinition *definition, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; GtkTextIter iter; Definition *def; /* insert the word if this is the first definition */ if (!priv->definitions) { gtk_text_buffer_get_start_iter (priv->buffer, &iter); gdict_defbox_insert_word (defbox, &iter, gdict_definition_get_word (definition)); } def = definition_new (); gtk_text_buffer_get_end_iter (priv->buffer, &iter); def->begin = gtk_text_iter_get_offset (&iter); gdict_defbox_insert_body (defbox, &iter, gdict_definition_get_text (definition)); gtk_text_buffer_get_end_iter (priv->buffer, &iter); gdict_defbox_insert_from (defbox, &iter, gdict_definition_get_database (definition)); def->definition = gdict_definition_ref (definition); priv->definitions = g_slist_append (priv->definitions, def); } static void error_cb (GdictContext *context, const GError *error, gpointer user_data) { GdictDefbox *defbox = GDICT_DEFBOX (user_data); GdictDefboxPrivate *priv = defbox->priv; GtkTextIter iter; if (!error) return; gdict_defbox_clear (defbox); gtk_text_buffer_get_start_iter (priv->buffer, &iter); gdict_defbox_insert_error (defbox, &iter, _("Error while looking up definition"), error->message); g_free (priv->word); priv->word = NULL; defbox->priv->is_searching = FALSE; } /** * gdict_defbox_lookup: * @defbox: a #GdictDefbox * @word: the word to look up * * Searches @word inside the dictionary sources using the #GdictContext * provided when creating @defbox or set using gdict_defbox_set_context(). * * Since: 0.1 */ void gdict_defbox_lookup (GdictDefbox *defbox, const gchar *word) { GdictDefboxPrivate *priv; GError *define_error; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; if (!priv->context) { g_warning ("Attempting to look up `%s', but no GdictContext " "has been set. Use gdict_defbox_set_context() " "before invoking gdict_defbox_lookup().", word); return; } if (priv->is_searching) { _gdict_show_error_dialog (GTK_WIDGET (defbox), _("Another search is in progress"), _("Please wait until the current search ends.")); return; } gdict_defbox_clear (defbox); if (!priv->start_id) { priv->start_id = g_signal_connect (priv->context, "lookup-start", G_CALLBACK (lookup_start_cb), defbox); priv->define_id = g_signal_connect (priv->context, "definition-found", G_CALLBACK (definition_found_cb), defbox); priv->end_id = g_signal_connect (priv->context, "lookup-end", G_CALLBACK (lookup_end_cb), defbox); } if (!priv->error_id) priv->error_id = g_signal_connect (priv->context, "error", G_CALLBACK (error_cb), defbox); priv->word = g_strdup (word); g_object_notify (G_OBJECT (defbox), "word"); define_error = NULL; gdict_context_define_word (priv->context, priv->database, word, &define_error); if (define_error) { GtkTextIter iter; gtk_text_buffer_get_start_iter (priv->buffer, &iter); gdict_defbox_insert_error (defbox, &iter, _("Error while retrieving the definition"), define_error->message); g_error_free (define_error); } } /** * gdict_defbox_clear: * @defbox: a @GdictDefbox * * Clears the buffer of @defbox * * Since: 0.1 */ void gdict_defbox_clear (GdictDefbox *defbox) { GdictDefboxPrivate *priv; GtkTextIter start, end; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; /* destroy previously found definitions */ if (priv->definitions) { g_slist_free_full (priv->definitions, (GDestroyNotify) definition_free); priv->definitions = NULL; } gtk_text_buffer_get_bounds (priv->buffer, &start, &end); gtk_text_buffer_delete (priv->buffer, &start, &end); } /** * gdict_defbox_find_next: * @defbox: a #GdictDefbox * * Emits the "find-next" signal. * * Since: 0.1 */ void gdict_defbox_find_next (GdictDefbox *defbox) { g_return_if_fail (GDICT_IS_DEFBOX (defbox)); g_signal_emit (defbox, gdict_defbox_signals[FIND_NEXT], 0); } /** * gdict_defbox_find_previous: * @defbox: a #GdictDefbox * * Emits the "find-previous" signal. * * Since: 0.1 */ void gdict_defbox_find_previous (GdictDefbox *defbox) { g_return_if_fail (GDICT_IS_DEFBOX (defbox)); g_signal_emit (defbox, gdict_defbox_signals[FIND_PREVIOUS], 0); } /** * gdict_defbox_select_all: * @defbox: a #GdictDefbox * * Selects all the text displayed by @defbox * * Since: 0.1 */ void gdict_defbox_select_all (GdictDefbox *defbox) { GdictDefboxPrivate *priv; GtkTextBuffer *buffer; GtkTextIter start, end; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); gtk_text_buffer_get_bounds (buffer, &start, &end); gtk_text_buffer_select_range (buffer, &start, &end); } /** * gdict_defbox_copy_to_clipboard: * @defbox: a #GdictDefbox * @clipboard: a #GtkClipboard * * Copies the selected text inside @defbox into @clipboard. * * Since: 0.1 */ void gdict_defbox_copy_to_clipboard (GdictDefbox *defbox, GtkClipboard *clipboard) { GdictDefboxPrivate *priv; GtkTextBuffer *buffer; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); g_return_if_fail (GTK_IS_CLIPBOARD (clipboard)); priv = defbox->priv; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); gtk_text_buffer_copy_clipboard (buffer, clipboard); } /** * gdict_defbox_count_definitions: * @defbox: a #GdictDefbox * * Gets the number of definitions displayed by @defbox * * Return value: the number of definitions. * * Since: 0.1 */ gint gdict_defbox_count_definitions (GdictDefbox *defbox) { GdictDefboxPrivate *priv; g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), -1); priv = defbox->priv; if (!priv->definitions) return -1; return g_slist_length (priv->definitions); } /** * gdict_defbox_jump_to_definition: * @defbox: a #GdictDefbox * @number: the definition to jump to * * Scrolls to the definition identified by @number. If @number is -1, * jumps to the last definition. * * Since: 0.1 */ void gdict_defbox_jump_to_definition (GdictDefbox *defbox, gint number) { GdictDefboxPrivate *priv; gint count; Definition *def; GtkTextBuffer *buffer; GtkTextIter def_start; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); count = gdict_defbox_count_definitions (defbox) - 1; if (count == -1) return; if ((number == -1) || (number > count)) number = count; priv = defbox->priv; def = (Definition *) g_slist_nth_data (priv->definitions, number); if (!def) return; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); gtk_text_buffer_get_iter_at_offset (buffer, &def_start, def->begin); gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (priv->text_view), &def_start, 0.0, TRUE, 0.0, 0.0); } /** * gdict_defbox_get_text: * @defbox: a #GdictDefbox * @length: return location for the text length or %NULL * * Gets the full contents of @defbox. * * Return value: a newly allocated string containing the text displayed by * @defbox. * * Since: 0.1 */ gchar * gdict_defbox_get_text (GdictDefbox *defbox, gsize *length) { GdictDefboxPrivate *priv; GtkTextBuffer *buffer; GtkTextIter start, end; gchar *retval; g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); priv = defbox->priv; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); gtk_text_buffer_get_bounds (buffer, &start, &end); retval = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); if (length) *length = strlen (retval); return retval; } /** * gdict_defbox_set_font_name: * @defbox: a #GdictDefbox * @font_name: a font description, or %NULL * * Sets @font_name as the font for @defbox. It calls internally * pango_font_description_from_string() and gtk_widget_modify_font(). * * Passing %NULL for @font_name will reset any previously set font. * * Since: 0.3.0 */ void gdict_defbox_set_font_name (GdictDefbox *defbox, const gchar *font_name) { GdictDefboxPrivate *priv; PangoFontDescription *font_desc; g_return_if_fail (GDICT_IS_DEFBOX (defbox)); priv = defbox->priv; if (font_name) { font_desc = pango_font_description_from_string (font_name); g_return_if_fail (font_desc != NULL); } else font_desc = NULL; gtk_widget_override_font (priv->text_view, font_desc); if (font_desc) pango_font_description_free (font_desc); g_free (priv->font_name); priv->font_name = g_strdup (font_name); } /** * gdict_defbox_get_font_name: * @defbox: a #GdictDefbox * * Retrieves the font currently used by @defbox. * * Return value: a font name. The returned string is owned by @defbox and * should not be modified or freed. * * Since: 0.3 */ const gchar * gdict_defbox_get_font_name (GdictDefbox *defbox) { g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); return defbox->priv->font_name; } /** * gdict_defbox_get_selected_word: * @defbox: a #GdictDefbox * * Retrieves the selected word from the defbox widget * * Return value: a newly allocated string containing the selected * word. Use g_free() when done using it. * * Since: 0.12 */ gchar * gdict_defbox_get_selected_word (GdictDefbox *defbox) { GdictDefboxPrivate *priv; GtkTextBuffer *buffer; g_return_val_if_fail (GDICT_IS_DEFBOX (defbox), NULL); priv = defbox->priv; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->text_view)); if (!gtk_text_buffer_get_has_selection (buffer)) return NULL; else { GtkTextIter start, end; gchar *retval; gtk_text_buffer_get_selection_bounds (buffer, &start, &end); retval = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); return retval; } }