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>
40 #include "spell_entry.h"
41 #include "prefs_common.h"
46 static void claws_spell_entry_init (ClawsSpellEntry *entry);
47 static void claws_spell_entry_finalize (GObject *object);
48 #if !GTK_CHECK_VERSION(3, 0, 0)
49 static void claws_spell_entry_destroy (GtkObject *object);
50 static gint claws_spell_entry_expose (GtkWidget *widget,
51 GdkEventExpose *event);
53 static void claws_spell_entry_destroy (GtkWidget *object);
54 static gint claws_spell_entry_expose (GtkWidget *widget,
57 static gint claws_spell_entry_button_press (GtkWidget *widget,
58 GdkEventButton *event);
59 static gboolean claws_spell_entry_popup_menu (GtkWidget *widget,
60 ClawsSpellEntry *entry);
61 static void claws_spell_entry_populate_popup (ClawsSpellEntry *entry,
64 static void claws_spell_entry_changed (GtkEditable *editable,
67 struct _ClawsSpellEntryPriv
69 PangoAttrList *attr_list;
76 static GtkEntryClass *parent_class = NULL;
79 G_DEFINE_TYPE(ClawsSpellEntry, claws_spell_entry, GTK_TYPE_ENTRY)
82 static void claws_spell_entry_class_init(ClawsSpellEntryClass *klass)
84 GObjectClass *g_object_class;
85 #if !GTK_CHECK_VERSION(3, 0, 0)
86 GtkObjectClass *gtk_object_class;
88 GtkWidgetClass *widget_class;
90 parent_class = g_type_class_peek_parent(klass);
92 g_object_class = G_OBJECT_CLASS(klass);
93 g_object_class->finalize = claws_spell_entry_finalize;
95 #if !GTK_CHECK_VERSION(3, 0, 0)
96 gtk_object_class = GTK_OBJECT_CLASS(klass);
97 gtk_object_class->destroy = claws_spell_entry_destroy;
100 widget_class = GTK_WIDGET_CLASS(klass);
101 widget_class->button_press_event = claws_spell_entry_button_press;
102 #if !GTK_CHECK_VERSION(3, 0, 0)
103 widget_class->expose_event = claws_spell_entry_expose;
105 widget_class->draw = claws_spell_entry_expose;
106 widget_class->destroy = claws_spell_entry_destroy;
109 g_type_class_add_private(g_object_class,
110 sizeof(struct _ClawsSpellEntryPriv));
113 static void claws_spell_entry_init(ClawsSpellEntry *entry)
115 entry->gtkaspell = NULL;
117 entry->priv = g_new0(ClawsSpellEntryPriv, 1);
118 entry->priv->attr_list = pango_attr_list_new();
120 g_signal_connect(G_OBJECT(entry), "popup-menu",
121 G_CALLBACK(claws_spell_entry_popup_menu), entry);
122 g_signal_connect(G_OBJECT(entry), "populate-popup",
123 G_CALLBACK(claws_spell_entry_populate_popup), NULL);
124 g_signal_connect(G_OBJECT(entry), "changed",
125 G_CALLBACK(claws_spell_entry_changed), NULL);
128 static void claws_spell_entry_finalize(GObject *object)
130 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(object);
132 if (entry->priv->attr_list)
133 pango_attr_list_unref(entry->priv->attr_list);
134 if (entry->priv->words)
135 g_strfreev(entry->priv->words);
137 g_free(entry->priv->word_starts);
138 g_free(entry->priv->word_ends);
142 G_OBJECT_CLASS(parent_class)->finalize(object);
145 #if !GTK_CHECK_VERSION(3, 0, 0)
146 static void claws_spell_entry_destroy(GtkObject *object)
148 GTK_OBJECT_CLASS(parent_class)->destroy(object);
151 static void claws_spell_entry_destroy(GtkWidget *object)
153 GTK_WIDGET_CLASS(parent_class)->destroy(object);
157 GtkWidget *claws_spell_entry_new(void)
159 return GTK_WIDGET( g_object_new(CLAWS_TYPE_SPELL_ENTRY, NULL) );
162 void claws_spell_entry_set_gtkaspell(ClawsSpellEntry *entry, GtkAspell *gtkaspell)
164 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
166 entry->gtkaspell = gtkaspell;
169 static gint gtk_entry_find_position (GtkEntry *entry, gint x)
172 PangoLayoutLine *line;
176 gint pos, current_pos;
179 x = x + entry->scroll_offset;
181 layout = gtk_entry_get_layout(entry);
182 text = pango_layout_get_text(layout);
183 g_object_get(entry, "cursor-position", ¤t_pos, NULL);
184 cursor_index = g_utf8_offset_to_pointer(text, current_pos) - text;
186 line = pango_layout_get_lines(layout)->data;
187 pango_layout_line_x_to_index(line, x * PANGO_SCALE, &index, &trailing);
189 if (index >= cursor_index && entry->preedit_length) {
190 if (index >= cursor_index + entry->preedit_length) {
191 index -= entry->preedit_length;
193 index = cursor_index;
198 pos = g_utf8_pointer_to_offset (text, text + index);
204 static void get_word_extents_from_position(ClawsSpellEntry *entry, gint *start,
205 gint *end, guint position)
213 if (entry->priv->words == NULL)
216 text = gtk_entry_get_text(GTK_ENTRY(entry));
217 bytes_pos = (gint) (g_utf8_offset_to_pointer(text, position) - text);
219 for (i = 0; entry->priv->words[i]; i++) {
220 if (bytes_pos >= entry->priv->word_starts[i] &&
221 bytes_pos <= entry->priv->word_ends[i]) {
222 *start = entry->priv->word_starts[i];
223 *end = entry->priv->word_ends[i];
229 static gchar *get_word(ClawsSpellEntry *entry, const int start, const int end)
237 text = gtk_entry_get_text(GTK_ENTRY(entry));
238 word = g_new0(gchar, end - start + 2);
239 g_strlcpy(word, text + start, end - start + 1);
244 static void replace_word(ClawsSpellEntry *entry, const gchar *newword)
246 gint cursor, start_pos, end_pos;
247 const gchar *text = gtk_entry_get_text(GTK_ENTRY(entry));
249 start_pos = entry->gtkaspell->start_pos;
250 end_pos = entry->gtkaspell->end_pos;
252 cursor = gtk_editable_get_position(GTK_EDITABLE(entry));
253 /* is the cursor at the end? If so, restore it there */
254 if (g_utf8_strlen(text, -1) == cursor)
256 else if(cursor < entry->priv->mark_character ||
257 cursor > entry->priv->mark_character)
258 cursor = entry->priv->mark_character;
260 gtk_editable_delete_text(GTK_EDITABLE(entry), start_pos, end_pos);
261 gtk_editable_insert_text(GTK_EDITABLE(entry), newword, strlen(newword),
263 gtk_editable_set_position(GTK_EDITABLE(entry), cursor);
267 static gboolean word_misspelled(ClawsSpellEntry *entry, int start, int end)
272 word = get_word(entry, start, end);
273 if (word == NULL || g_unichar_isdigit(word[0]))
276 ret = gtkaspell_misspelled_test(entry->gtkaspell, word);
282 static gboolean is_word_end (GtkEntry *entry, const int offset)
284 gchar *p = gtk_editable_get_chars(GTK_EDITABLE(entry), offset, offset+1);
287 ch = g_utf8_get_char(p);
294 p = gtk_editable_get_chars(GTK_EDITABLE(entry), offset+1, offset+2);
295 ch = g_utf8_get_char(p);
298 return (g_unichar_isspace(ch) || g_unichar_ispunct(ch)
299 || g_unichar_isdigit(ch));
302 return (g_unichar_isspace(ch) || g_unichar_ispunct(ch));
305 static void entry_strsplit_utf8(GtkEntry *entry, gchar ***set, gint **starts, gint **ends)
308 PangoLogAttr *log_attrs;
310 gint n_attrs, n_strings, i, j;
312 layout = gtk_entry_get_layout(GTK_ENTRY(entry));
313 text = gtk_entry_get_text(GTK_ENTRY(entry));
314 pango_layout_get_log_attrs(layout, &log_attrs, &n_attrs);
316 /* Find how many words we have */
318 for (i = 0; i < n_attrs; i++)
319 if (log_attrs[i].is_word_start)
322 *set = g_new0(gchar *, n_strings + 1);
323 *starts = g_new0(gint, n_strings);
324 *ends = g_new0(gint, n_strings);
326 /* Copy out strings */
327 for (i = 0, j = 0; i < n_attrs; i++) {
328 if (log_attrs[i].is_word_start) {
332 /* Find the end of this string */
334 while (!is_word_end(entry, cend))
337 /* Copy sub-string */
338 start = g_utf8_offset_to_pointer(text, i);
339 bytes = (gint) (g_utf8_offset_to_pointer(text, cend) - start);
340 (*set)[j] = g_new0(gchar, bytes + 1);
341 (*starts)[j] = (gint) (start - text);
342 (*ends)[j] = (gint) (start - text + bytes);
343 g_utf8_strncpy((*set)[j], start, cend - i);
345 /* Move on to the next word */
353 static void insert_misspelled_marker(ClawsSpellEntry *entry, guint start, guint end)
355 guint16 red = (guint16) (((gdouble)((prefs_common.color[COL_MISSPELLED] &
356 0xff0000) >> 16) / 255.0) * 65535.0);
357 guint16 green = (guint16) (((gdouble)((prefs_common.color[COL_MISSPELLED] &
358 0x00ff00) >> 8) / 255.0) * 65535.0);
359 guint16 blue = (guint16) (((gdouble) (prefs_common.color[COL_MISSPELLED] &
360 0x0000ff) / 255.0) * 65535.0);
361 PangoAttribute *fcolor, *ucolor, *unline;
363 if(prefs_common.color[COL_MISSPELLED] != 0) {
364 fcolor = pango_attr_foreground_new(red, green, blue);
365 fcolor->start_index = start;
366 fcolor->end_index = end;
368 pango_attr_list_insert(entry->priv->attr_list, fcolor);
370 ucolor = pango_attr_underline_color_new (65535, 0, 0);
371 unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
373 ucolor->start_index = start;
374 unline->start_index = start;
376 ucolor->end_index = end;
377 unline->end_index = end;
379 pango_attr_list_insert (entry->priv->attr_list, ucolor);
380 pango_attr_list_insert (entry->priv->attr_list, unline);
384 static gboolean check_word(ClawsSpellEntry *entry, int start, int end)
386 GtkAspell *gtkaspell = entry->gtkaspell;
387 PangoAttrIterator *it;
390 gchar *text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
393 /* Check to see if we've got any attributes at this position.
394 * If so, free them, since we'll readd it if the word is misspelled */
395 it = pango_attr_list_get_iterator(entry->priv->attr_list);
399 pango_attr_iterator_range(it, &s, &e);
401 GSList *attrs = pango_attr_iterator_get_attrs(it);
402 g_slist_foreach(attrs, (GFunc) pango_attribute_destroy, NULL);
405 } while (pango_attr_iterator_next(it));
406 pango_attr_iterator_destroy(it);
408 if ((misspelled = word_misspelled(entry, start, end))) {
409 insert_misspelled_marker(entry, start, end);
411 word = get_word(entry, start, end);
412 strncpy(gtkaspell->theword, (gchar *)word, GTKASPELLWORDSIZE - 1);
413 gtkaspell->theword[GTKASPELLWORDSIZE - 1] = 0;
414 gtkaspell->start_pos = g_utf8_pointer_to_offset(text, (text+start));
415 gtkaspell->end_pos = g_utf8_pointer_to_offset(text, (text+end));
416 gtkaspell_free_suggestions_list(gtkaspell);
425 void claws_spell_entry_recheck_all(ClawsSpellEntry *entry)
427 GtkAllocation allocation;
432 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
433 cm_return_if_fail(entry->gtkaspell != NULL);
435 if (entry->priv->words == NULL)
438 /* Remove all existing pango attributes. These will get readded as we check */
439 pango_attr_list_unref(entry->priv->attr_list);
440 entry->priv->attr_list = pango_attr_list_new();
442 /* Loop through words */
443 for (i = 0; entry->priv->words[i]; i++) {
444 length = strlen(entry->priv->words[i]);
447 check_word(entry, entry->priv->word_starts[i], entry->priv->word_ends[i]);
450 layout = gtk_entry_get_layout(GTK_ENTRY(entry));
451 pango_layout_set_attributes(layout, entry->priv->attr_list);
453 if (gtk_widget_get_realized(GTK_WIDGET(entry))) {
454 rect.x = 0; rect.y = 0;
455 gtk_widget_get_allocation(GTK_WIDGET(entry), &allocation);
456 rect.width = allocation.width;
457 rect.height = allocation.height;
458 gdk_window_invalidate_rect(gtk_widget_get_window(GTK_WIDGET(entry)),
463 #if !GTK_CHECK_VERSION(3, 0, 0)
464 static gint claws_spell_entry_expose(GtkWidget *widget, GdkEventExpose *event)
466 static gint claws_spell_entry_expose(GtkWidget *widget, cairo_t *cr)
469 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(widget);
470 GtkEntry *gtk_entry = GTK_ENTRY(widget);
473 if (entry->gtkaspell != NULL) {
474 layout = gtk_entry_get_layout(gtk_entry);
475 pango_layout_set_attributes(layout, entry->priv->attr_list);
478 #if !GTK_CHECK_VERSION(3, 0, 0)
479 return GTK_WIDGET_CLASS(parent_class)->expose_event (widget, event);
481 return GTK_WIDGET_CLASS(parent_class)->draw (widget, cr);
485 static gint claws_spell_entry_button_press(GtkWidget *widget, GdkEventButton *event)
487 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(widget);
488 GtkEntry *gtk_entry = GTK_ENTRY(widget);
491 pos = gtk_entry_find_position(gtk_entry, event->x);
492 entry->priv->mark_character = pos;
494 return GTK_WIDGET_CLASS(parent_class)->button_press_event (widget, event);
497 static gboolean claws_spell_entry_popup_menu(GtkWidget *widget, ClawsSpellEntry *entry)
499 entry->priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry));
503 static void set_position(gpointer data, gint pos)
505 gtk_editable_set_position(GTK_EDITABLE(data), pos);
508 static gboolean find_misspelled_cb(gpointer data, gboolean forward)
510 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
511 GtkAspell *gtkaspell = entry->gtkaspell;
512 gboolean misspelled = FALSE;
513 gint cursor, minpos, maxpos, i, words_len = 0;
517 if (entry->priv->words == NULL)
520 gtkaspell->orig_pos = gtk_editable_get_position(GTK_EDITABLE(entry));
521 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
522 cursor = g_utf8_offset_to_pointer(text, gtkaspell->orig_pos) - text;
524 if (gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &start, &end)) {
525 minpos = g_utf8_offset_to_pointer(text, start) - text;
526 maxpos = g_utf8_offset_to_pointer(text, end) - text;
528 minpos = forward ? cursor : 0;
529 maxpos = forward ? strlen(text)-1 : cursor;
533 while(entry->priv->words[words_len])
537 for(i=0; i < words_len; i++)
538 if (entry->priv->word_ends[i] > minpos &&
539 (misspelled = check_word(entry,
540 entry->priv->word_starts[i],
541 entry->priv->word_ends[i])))
544 for(i=words_len-1; i >= 0; i--)
545 if (entry->priv->word_starts[i] < maxpos &&
546 (misspelled = check_word(entry,
547 entry->priv->word_starts[i],
548 entry->priv->word_ends[i])))
555 static gboolean check_word_cb(gpointer data)
557 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
560 get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character);
561 return check_word(entry, start, end);
564 static void replace_word_cb(gpointer data, const gchar *newword)
566 replace_word((ClawsSpellEntry *) data, newword);
569 static void set_menu_pos(GtkMenu *menu, gint *x, gint *y,
570 gboolean *push_in, gpointer data)
572 ClawsSpellEntry *entry = (ClawsSpellEntry *) data;
573 GtkAspell *gtkaspell = entry->gtkaspell;
574 gint pango_offset, win_x, win_y, scr_x, scr_y, text_index, entry_x;
576 GtkRequisition subject_rq;
577 PangoLayout *layout = gtk_entry_get_layout(GTK_ENTRY(entry));
578 PangoLayoutLine *line = pango_layout_get_lines(layout)->data;
580 gtk_widget_get_child_requisition(GTK_WIDGET(entry), &subject_rq);
582 /* screen -> compose window coords */
583 gdk_window_get_origin(gtk_widget_get_window(GTK_WIDGET(gtkaspell->parent_window)),
586 /* compose window -> subject entry coords */
587 gtk_widget_translate_coordinates(GTK_WIDGET(entry),
588 gtkaspell->parent_window, 0, 0, &win_x, &win_y);
590 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
591 text_index = g_utf8_offset_to_pointer(text, gtkaspell->end_pos) - text;
594 pango_offset = gtk_entry_text_index_to_layout_index(GTK_ENTRY(entry),
596 pango_layout_line_index_to_x(line, pango_offset, TRUE, &entry_x);
598 *x = scr_x + win_x + PANGO_PIXELS(entry_x) + 8;
599 *y = scr_y + win_y + subject_rq.height;
602 void claws_spell_entry_context_set(ClawsSpellEntry *entry)
604 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
605 cm_return_if_fail(entry->gtkaspell != NULL);
607 entry->gtkaspell->ctx.set_position = set_position;
608 entry->gtkaspell->ctx.set_menu_pos = set_menu_pos;
609 entry->gtkaspell->ctx.find_misspelled = find_misspelled_cb;
610 entry->gtkaspell->ctx.check_word = check_word_cb;
611 entry->gtkaspell->ctx.replace_word = replace_word_cb;
612 entry->gtkaspell->ctx.data = (gpointer) entry;
615 static void claws_spell_entry_populate_popup(ClawsSpellEntry *entry, GtkMenu *menu,
618 GtkAspell *gtkaspell = entry->gtkaspell;
622 if (gtkaspell == NULL)
625 get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character);
627 if ((word = get_word(entry, start, end)) != NULL) {
628 strncpy(gtkaspell->theword, word, GTKASPELLWORDSIZE - 1);
632 gtkaspell->misspelled = word_misspelled(entry, start, end);
634 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
635 gtkaspell->start_pos = g_utf8_pointer_to_offset(text, (text+start));
636 gtkaspell->end_pos = g_utf8_pointer_to_offset(text, (text+end));
639 claws_spell_entry_context_set(entry);
640 gtkaspell_make_context_menu(menu, gtkaspell);
643 static void claws_spell_entry_changed(GtkEditable *editable, gpointer data)
645 ClawsSpellEntry *entry = CLAWS_SPELL_ENTRY(editable);
647 if (entry->gtkaspell == NULL)
650 if (entry->priv->words) {
651 g_strfreev(entry->priv->words);
652 g_free(entry->priv->word_starts);
653 g_free(entry->priv->word_ends);
655 entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words,
656 &entry->priv->word_starts, &entry->priv->word_ends);
657 if(entry->gtkaspell->check_while_typing == TRUE)
658 claws_spell_entry_recheck_all(entry);
661 static void continue_check(gpointer *data)
663 ClawsSpellEntry *entry = (ClawsSpellEntry *)data;
664 GtkAspell *gtkaspell = entry->gtkaspell;
665 gint pos = gtk_editable_get_position(GTK_EDITABLE(entry));
667 if (gtkaspell->misspelled && pos < gtkaspell->end_check_pos)
668 gtkaspell->misspelled = gtkaspell_check_next_prev(gtkaspell, TRUE);
670 gtkaspell->continue_check = NULL;
673 void claws_spell_entry_check_all(ClawsSpellEntry *entry)
678 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
679 cm_return_if_fail(entry->gtkaspell != NULL);
681 if (!gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), &start, &end)) {
682 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
685 end = g_utf8_strlen(text, -1) - 1;
690 gtk_editable_set_position(GTK_EDITABLE(entry), start);
691 entry->gtkaspell->continue_check = continue_check;
692 entry->gtkaspell->end_check_pos = end;
694 claws_spell_entry_context_set(entry);
695 entry->gtkaspell->misspelled =
696 gtkaspell_check_next_prev(entry->gtkaspell, TRUE);
699 void claws_spell_entry_check_backwards(ClawsSpellEntry *entry)
701 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
702 cm_return_if_fail(entry->gtkaspell != NULL);
704 entry->gtkaspell->continue_check = NULL;
705 claws_spell_entry_context_set(entry);
706 gtkaspell_check_next_prev(entry->gtkaspell, FALSE);
709 void claws_spell_entry_check_forwards_go(ClawsSpellEntry *entry)
711 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry));
712 cm_return_if_fail(entry->gtkaspell != NULL);
714 entry->gtkaspell->continue_check = NULL;
715 claws_spell_entry_context_set(entry);
716 gtkaspell_check_next_prev(entry->gtkaspell, TRUE);
719 #endif /* USE_ENCHANT */