2 * @file libsexy/sexy-icon-entry.c Entry widget
4 * @Copyright (C) 2004-2006 Christian Hammond.
5 * Some of this code is from gtkspell, Copyright (C) 2002 Evan Martin.
6 * Adapted for Claws Mail (c) 2009-2012 Pawel Pekala and the Claws Mail team
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 #include "claws-features.h"
31 #include <glib/gi18n.h>
34 #include <gdk/gdkkeysyms.h>
39 #include "spell_entry.h"
40 #include "prefs_common.h"
45 static void claws_spell_entry_init (ClawsSpellEntry *entry);
46 static void claws_spell_entry_destroy (GtkObject *object);
47 static gint claws_spell_entry_expose (GtkWidget *widget,
48 GdkEventExpose *event);
49 static gint claws_spell_entry_button_press (GtkWidget *widget,
50 GdkEventButton *event);
51 static gboolean claws_spell_entry_popup_menu (GtkWidget *widget,
52 ClawsSpellEntry *entry);
53 static void claws_spell_entry_populate_popup (ClawsSpellEntry *entry,
56 static void claws_spell_entry_changed (GtkEditable *editable,
58 static void claws_spell_entry_preedit_changed (GtkEntry *entry,
64 PangoAttrList *attr_list;
70 } ClawsSpellEntryPrivate;
72 #define CLAWS_SPELL_ENTRY_GET_PRIVATE(entry) \
73 G_TYPE_INSTANCE_GET_PRIVATE (entry, CLAWS_TYPE_SPELL_ENTRY, \
74 ClawsSpellEntryPrivate)
76 static GtkEntryClass *parent_class = NULL;
78 #if !GLIB_CHECK_VERSION(2,58, 0)
79 G_DEFINE_TYPE(ClawsSpellEntry, claws_spell_entry, GTK_TYPE_ENTRY)
81 G_DEFINE_TYPE_WITH_CODE(ClawsSpellEntry, claws_spell_entry, GTK_TYPE_ENTRY,
82 G_ADD_PRIVATE(ClawsSpellEntry))
86 static void claws_spell_entry_class_init(ClawsSpellEntryClass *klass)
88 #if !GLIB_CHECK_VERSION(2,58, 0)
89 GObjectClass *g_object_class;
91 GtkObjectClass *gtk_object_class;
92 GtkWidgetClass *widget_class;
94 parent_class = g_type_class_peek_parent(klass);
96 gtk_object_class = GTK_OBJECT_CLASS(klass);
97 gtk_object_class->destroy = claws_spell_entry_destroy;
99 widget_class = GTK_WIDGET_CLASS(klass);
100 widget_class->button_press_event = claws_spell_entry_button_press;
101 widget_class->expose_event = claws_spell_entry_expose;
103 #if !GLIB_CHECK_VERSION(2,58, 0)
104 g_object_class = G_OBJECT_CLASS(klass);
105 g_type_class_add_private(g_object_class,
106 sizeof(ClawsSpellEntryPrivate));
110 static void claws_spell_entry_init(ClawsSpellEntry *entry)
112 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
114 entry->gtkaspell = NULL;
116 priv->attr_list = pango_attr_list_new();
117 priv->preedit_length = 0;
119 g_signal_connect(G_OBJECT(entry), "popup-menu",
120 G_CALLBACK(claws_spell_entry_popup_menu), entry);
121 g_signal_connect(G_OBJECT(entry), "populate-popup",
122 G_CALLBACK(claws_spell_entry_populate_popup), NULL);
123 g_signal_connect(G_OBJECT(entry), "changed",
124 G_CALLBACK(claws_spell_entry_changed), NULL);
125 g_signal_connect(G_OBJECT(entry), "preedit-changed",
126 G_CALLBACK(claws_spell_entry_preedit_changed), NULL);
129 static void claws_spell_entry_destroy(GtkObject *object)
131 GTK_OBJECT_CLASS(parent_class)->destroy(object);
134 GtkWidget *claws_spell_entry_new(void)
136 return GTK_WIDGET( g_object_new(CLAWS_TYPE_SPELL_ENTRY, NULL) );
139 void claws_spell_entry_set_gtkaspell(ClawsSpellEntry *entry, GtkAspell *gtkaspell)
141 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
143 entry->gtkaspell = gtkaspell;
146 static gint claws_spell_entry_find_position (ClawsSpellEntry *_entry, gint x)
149 PangoLayoutLine *line;
153 gint pos, current_pos;
156 GtkEntry *entry = GTK_ENTRY(_entry);
157 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
159 g_object_get(entry, "scroll-offset", &scroll_offset, NULL);
160 x = x + scroll_offset;
162 layout = gtk_entry_get_layout(entry);
163 text = pango_layout_get_text(layout);
164 g_object_get(entry, "cursor-position", ¤t_pos, NULL);
165 cursor_index = g_utf8_offset_to_pointer(text, current_pos) - text;
167 line = pango_layout_get_lines(layout)->data;
168 pango_layout_line_x_to_index(line, x * PANGO_SCALE, &index, &trailing);
170 if (index >= cursor_index && priv->preedit_length) {
171 if (index >= cursor_index + priv->preedit_length) {
172 index -= priv->preedit_length;
174 index = cursor_index;
179 pos = g_utf8_pointer_to_offset (text, text + index);
185 static void get_word_extents_from_position(ClawsSpellEntry *entry, gint *start,
186 gint *end, guint position)
190 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
195 if (priv->words == NULL)
198 text = gtk_entry_get_text(GTK_ENTRY(entry));
199 bytes_pos = (gint) (g_utf8_offset_to_pointer(text, position) - text);
201 for (i = 0; priv->words[i]; i++) {
202 if (bytes_pos >= priv->word_starts[i] &&
203 bytes_pos <= priv->word_ends[i]) {
204 *start = priv->word_starts[i];
205 *end = priv->word_ends[i];
211 static gchar *get_word(ClawsSpellEntry *entry, const int start, const int end)
219 text = gtk_entry_get_text(GTK_ENTRY(entry));
220 word = g_new0(gchar, end - start + 2);
221 g_strlcpy(word, text + start, end - start + 1);
226 static void replace_word(ClawsSpellEntry *entry, const gchar *newword)
228 gint cursor, start_pos, end_pos;
229 const gchar *text = gtk_entry_get_text(GTK_ENTRY(entry));
230 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
232 start_pos = entry->gtkaspell->start_pos;
233 end_pos = entry->gtkaspell->end_pos;
235 cursor = gtk_editable_get_position(GTK_EDITABLE(entry));
236 /* is the cursor at the end? If so, restore it there */
237 if (g_utf8_strlen(text, -1) == cursor)
239 else if(cursor < priv->mark_character ||
240 cursor > priv->mark_character)
241 cursor = priv->mark_character;
243 gtk_editable_delete_text(GTK_EDITABLE(entry), start_pos, end_pos);
244 gtk_editable_insert_text(GTK_EDITABLE(entry), newword, strlen(newword),
246 gtk_editable_set_position(GTK_EDITABLE(entry), cursor);
250 static gboolean word_misspelled(ClawsSpellEntry *entry, int start, int end)
255 word = get_word(entry, start, end);
256 if (word == NULL || g_unichar_isdigit(word[0])) {
262 ret = gtkaspell_misspelled_test(entry->gtkaspell, word);
268 static gboolean is_word_end (GtkEntry *entry, const int offset)
270 gchar *p = gtk_editable_get_chars(GTK_EDITABLE(entry), offset, offset+1);
273 ch = g_utf8_get_char(p);
280 p = gtk_editable_get_chars(GTK_EDITABLE(entry), offset+1, offset+2);
281 ch = g_utf8_get_char(p);
284 return (g_unichar_isspace(ch) || g_unichar_ispunct(ch)
285 || g_unichar_isdigit(ch));
288 return (g_unichar_isspace(ch) || g_unichar_ispunct(ch));
291 static void entry_strsplit_utf8(GtkEntry *entry, gchar ***set, gint **starts, gint **ends)
294 PangoLogAttr *log_attrs;
296 gint n_attrs, n_strings, i, j;
298 layout = gtk_entry_get_layout(GTK_ENTRY(entry));
299 text = gtk_entry_get_text(GTK_ENTRY(entry));
300 pango_layout_get_log_attrs(layout, &log_attrs, &n_attrs);
302 /* Find how many words we have */
304 for (i = 0; i < n_attrs; i++)
305 if (log_attrs[i].is_word_start)
308 *set = g_new0(gchar *, n_strings + 1);
309 *starts = g_new0(gint, n_strings);
310 *ends = g_new0(gint, n_strings);
312 /* Copy out strings */
313 for (i = 0, j = 0; i < n_attrs; i++) {
314 if (log_attrs[i].is_word_start) {
318 /* Find the end of this string */
320 while (!is_word_end(entry, cend))
323 /* Copy sub-string */
324 start = g_utf8_offset_to_pointer(text, i);
325 bytes = (gint) (g_utf8_offset_to_pointer(text, cend) - start);
326 (*set)[j] = g_new0(gchar, bytes + 1);
327 (*starts)[j] = (gint) (start - text);
328 (*ends)[j] = (gint) (start - text + bytes);
329 g_utf8_strncpy((*set)[j], start, cend - i);
331 /* Move on to the next word */
339 static void insert_misspelled_marker(ClawsSpellEntry *entry, guint start, guint end)
341 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
342 guint16 red = (guint16) (((gdouble)((prefs_common.color[COL_MISSPELLED] &
343 0xff0000) >> 16) / 255.0) * 65535.0);
344 guint16 green = (guint16) (((gdouble)((prefs_common.color[COL_MISSPELLED] &
345 0x00ff00) >> 8) / 255.0) * 65535.0);
346 guint16 blue = (guint16) (((gdouble) (prefs_common.color[COL_MISSPELLED] &
347 0x0000ff) / 255.0) * 65535.0);
348 PangoAttribute *fcolor, *ucolor, *unline;
350 if(prefs_common.color[COL_MISSPELLED] != 0) {
351 fcolor = pango_attr_foreground_new(red, green, blue);
352 fcolor->start_index = start;
353 fcolor->end_index = end;
355 pango_attr_list_insert(priv->attr_list, fcolor);
357 ucolor = pango_attr_underline_color_new (65535, 0, 0);
358 unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
360 ucolor->start_index = start;
361 unline->start_index = start;
363 ucolor->end_index = end;
364 unline->end_index = end;
366 pango_attr_list_insert (priv->attr_list, ucolor);
367 pango_attr_list_insert (priv->attr_list, unline);
371 static gboolean check_word(ClawsSpellEntry *entry, int start, int end)
373 GtkAspell *gtkaspell = entry->gtkaspell;
374 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
375 PangoAttrIterator *it;
378 gchar *text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
381 /* Check to see if we've got any attributes at this position.
382 * If so, free them, since we'll readd it if the word is misspelled */
383 it = pango_attr_list_get_iterator(priv->attr_list);
387 pango_attr_iterator_range(it, &s, &e);
389 GSList *attrs = pango_attr_iterator_get_attrs(it);
390 g_slist_foreach(attrs, (GFunc) pango_attribute_destroy, NULL);
393 } while (pango_attr_iterator_next(it));
394 pango_attr_iterator_destroy(it);
396 if ((misspelled = word_misspelled(entry, start, end))) {
397 insert_misspelled_marker(entry, start, end);
399 word = get_word(entry, start, end);
400 strncpy(gtkaspell->theword, (gchar *)word, GTKASPELLWORDSIZE - 1);
401 gtkaspell->theword[GTKASPELLWORDSIZE - 1] = 0;
402 gtkaspell->start_pos = g_utf8_pointer_to_offset(text, (text+start));
403 gtkaspell->end_pos = g_utf8_pointer_to_offset(text, (text+end));
404 gtkaspell_free_suggestions_list(gtkaspell);
413 void claws_spell_entry_recheck_all(ClawsSpellEntry *entry)
415 GtkAllocation allocation;
418 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
421 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
422 cm_return_if_fail(entry->gtkaspell != NULL);
424 if (priv->words == NULL)
427 /* Remove all existing pango attributes. These will get readded as we check */
428 pango_attr_list_unref(priv->attr_list);
429 priv->attr_list = pango_attr_list_new();
431 /* Loop through words */
432 for (i = 0; priv->words[i]; i++) {
433 length = strlen(priv->words[i]);
436 check_word(entry, priv->word_starts[i], priv->word_ends[i]);
439 layout = gtk_entry_get_layout(GTK_ENTRY(entry));
440 pango_layout_set_attributes(layout, priv->attr_list);
442 if (gtk_widget_get_realized(GTK_WIDGET(entry))) {
443 rect.x = 0; rect.y = 0;
444 gtk_widget_get_allocation(GTK_WIDGET(entry), &allocation);
445 rect.width = allocation.width;
446 rect.height = allocation.height;
447 gdk_window_invalidate_rect(gtk_widget_get_window(GTK_WIDGET(entry)),
452 static gint claws_spell_entry_expose(GtkWidget *widget, GdkEventExpose *event)
454 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(widget);
455 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
456 GtkEntry *gtk_entry = GTK_ENTRY(widget);
459 if (entry->gtkaspell != NULL) {
460 layout = gtk_entry_get_layout(gtk_entry);
461 pango_layout_set_attributes(layout, priv->attr_list);
464 return GTK_WIDGET_CLASS(parent_class)->expose_event (widget, event);
467 static gint claws_spell_entry_button_press(GtkWidget *widget, GdkEventButton *event)
469 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(widget);
470 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
473 pos = claws_spell_entry_find_position(entry, event->x);
474 priv->mark_character = pos;
476 return GTK_WIDGET_CLASS(parent_class)->button_press_event (widget, event);
479 static gboolean claws_spell_entry_popup_menu(GtkWidget *widget, ClawsSpellEntry *entry)
481 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
483 priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry));
487 static void set_position(gpointer data, gint pos)
489 gtk_editable_set_position(GTK_EDITABLE(data), pos);
492 static gboolean find_misspelled_cb(gpointer data, gboolean forward)
494 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
495 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
496 GtkAspell *gtkaspell = entry->gtkaspell;
497 gboolean misspelled = FALSE;
498 gint cursor, minpos, maxpos, i, words_len = 0;
502 if (priv->words == NULL)
505 gtkaspell->orig_pos = gtk_editable_get_position(GTK_EDITABLE(entry));
506 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
507 cursor = g_utf8_offset_to_pointer(text, gtkaspell->orig_pos) - text;
509 if (gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &start, &end)) {
510 minpos = g_utf8_offset_to_pointer(text, start) - text;
511 maxpos = g_utf8_offset_to_pointer(text, end) - text;
513 minpos = forward ? cursor : 0;
514 maxpos = forward ? strlen(text)-1 : cursor;
518 while(priv->words[words_len])
522 for(i=0; i < words_len; i++)
523 if (priv->word_ends[i] > minpos &&
524 (misspelled = check_word(entry,
525 priv->word_starts[i],
526 priv->word_ends[i])))
529 for(i=words_len-1; i >= 0; i--)
530 if (priv->word_starts[i] < maxpos &&
531 (misspelled = check_word(entry,
532 priv->word_starts[i],
533 priv->word_ends[i])))
540 static gboolean check_word_cb(gpointer data)
542 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
543 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
546 get_word_extents_from_position(entry, &start, &end, priv->mark_character);
547 return check_word(entry, start, end);
550 static void replace_word_cb(gpointer data, const gchar *newword)
552 replace_word((ClawsSpellEntry *) data, newword);
555 static void set_menu_pos(GtkMenu *menu, gint *x, gint *y,
556 gboolean *push_in, gpointer data)
558 ClawsSpellEntry *entry = (ClawsSpellEntry *) data;
559 GtkAspell *gtkaspell = entry->gtkaspell;
560 gint pango_offset, win_x, win_y, scr_x, scr_y, text_index, entry_x;
562 GtkRequisition subject_rq;
563 PangoLayout *layout = gtk_entry_get_layout(GTK_ENTRY(entry));
564 PangoLayoutLine *line = pango_layout_get_lines(layout)->data;
566 gtk_widget_get_child_requisition(GTK_WIDGET(entry), &subject_rq);
568 /* screen -> compose window coords */
569 gdk_window_get_origin(gtk_widget_get_window(GTK_WIDGET(gtkaspell->parent_window)),
572 /* compose window -> subject entry coords */
573 gtk_widget_translate_coordinates(GTK_WIDGET(entry),
574 gtkaspell->parent_window, 0, 0, &win_x, &win_y);
576 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
577 text_index = g_utf8_offset_to_pointer(text, gtkaspell->end_pos) - text;
580 pango_offset = gtk_entry_text_index_to_layout_index(GTK_ENTRY(entry),
582 pango_layout_line_index_to_x(line, pango_offset, TRUE, &entry_x);
584 *x = scr_x + win_x + PANGO_PIXELS(entry_x) + 8;
585 *y = scr_y + win_y + subject_rq.height;
588 void claws_spell_entry_context_set(ClawsSpellEntry *entry)
590 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
591 cm_return_if_fail(entry->gtkaspell != NULL);
593 entry->gtkaspell->ctx.set_position = set_position;
594 entry->gtkaspell->ctx.set_menu_pos = set_menu_pos;
595 entry->gtkaspell->ctx.find_misspelled = find_misspelled_cb;
596 entry->gtkaspell->ctx.check_word = check_word_cb;
597 entry->gtkaspell->ctx.replace_word = replace_word_cb;
598 entry->gtkaspell->ctx.data = (gpointer) entry;
601 static void claws_spell_entry_populate_popup(ClawsSpellEntry *entry, GtkMenu *menu,
604 GtkAspell *gtkaspell = entry->gtkaspell;
605 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
609 if (gtkaspell == NULL)
612 get_word_extents_from_position(entry, &start, &end, priv->mark_character);
614 if ((word = get_word(entry, start, end)) != NULL) {
615 strncpy(gtkaspell->theword, word, GTKASPELLWORDSIZE - 1);
619 gtkaspell->misspelled = word_misspelled(entry, start, end);
621 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
622 gtkaspell->start_pos = g_utf8_pointer_to_offset(text, (text+start));
623 gtkaspell->end_pos = g_utf8_pointer_to_offset(text, (text+end));
626 claws_spell_entry_context_set(entry);
627 gtkaspell_make_context_menu(menu, gtkaspell);
630 static void claws_spell_entry_changed(GtkEditable *editable, gpointer data)
632 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(editable);
633 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
635 if (entry->gtkaspell == NULL)
639 g_strfreev(priv->words);
640 g_free(priv->word_starts);
641 g_free(priv->word_ends);
643 entry_strsplit_utf8(GTK_ENTRY(entry), &priv->words,
644 &priv->word_starts, &priv->word_ends);
645 if(entry->gtkaspell->check_while_typing == TRUE)
646 claws_spell_entry_recheck_all(entry);
649 static void claws_spell_entry_preedit_changed (GtkEntry *_entry,
653 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(_entry);
654 ClawsSpellEntryPrivate *priv = CLAWS_SPELL_ENTRY_GET_PRIVATE(entry);
656 priv->preedit_length = preedit != NULL ? strlen(preedit) : 0;
659 static void continue_check(gpointer *data)
661 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
662 GtkAspell *gtkaspell = entry->gtkaspell;
663 gint pos = gtk_editable_get_position(GTK_EDITABLE(entry));
665 if (gtkaspell->misspelled && pos < gtkaspell->end_check_pos)
666 gtkaspell->misspelled = gtkaspell_check_next_prev(gtkaspell, TRUE);
668 gtkaspell->continue_check = NULL;
671 void claws_spell_entry_check_all(ClawsSpellEntry *entry)
676 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
677 cm_return_if_fail(entry->gtkaspell != NULL);
679 if (!gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &start, &end)) {
680 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
683 end = g_utf8_strlen(text, -1) - 1;
688 gtk_editable_set_position(GTK_EDITABLE(entry), start);
689 entry->gtkaspell->continue_check = continue_check;
690 entry->gtkaspell->end_check_pos = end;
692 claws_spell_entry_context_set(entry);
693 entry->gtkaspell->misspelled =
694 gtkaspell_check_next_prev(entry->gtkaspell, TRUE);
697 void claws_spell_entry_check_backwards(ClawsSpellEntry *entry)
699 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
700 cm_return_if_fail(entry->gtkaspell != NULL);
702 entry->gtkaspell->continue_check = NULL;
703 claws_spell_entry_context_set(entry);
704 gtkaspell_check_next_prev(entry->gtkaspell, FALSE);
707 void claws_spell_entry_check_forwards_go(ClawsSpellEntry *entry)
709 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
710 cm_return_if_fail(entry->gtkaspell != NULL);
712 entry->gtkaspell->continue_check = NULL;
713 claws_spell_entry_context_set(entry);
714 gtkaspell_check_next_prev(entry->gtkaspell, TRUE);
717 #endif /* USE_ENCHANT */