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_finalize (GObject
*object
);
47 static void claws_spell_entry_destroy (GtkWidget
*object
);
48 static gint
claws_spell_entry_expose (GtkWidget
*widget
,
50 static gint
claws_spell_entry_button_press (GtkWidget
*widget
,
51 GdkEventButton
*event
);
52 static gboolean
claws_spell_entry_popup_menu (GtkWidget
*widget
,
53 ClawsSpellEntry
*entry
);
54 static void claws_spell_entry_populate_popup (ClawsSpellEntry
*entry
,
57 static void claws_spell_entry_changed (GtkEditable
*editable
,
59 static void claws_spell_entry_preedit_changed (GtkEntry
*entry
,
63 typedef struct _ClawsSpellEntryPriv
65 PangoAttrList
*attr_list
;
71 } ClawsSpellEntryPrivate
;
73 static GtkEntryClass
*parent_class
= NULL
;
76 G_DEFINE_TYPE_WITH_CODE(ClawsSpellEntry
, claws_spell_entry
, GTK_TYPE_ENTRY
,
77 G_ADD_PRIVATE(ClawsSpellEntry
))
80 static void claws_spell_entry_class_init(ClawsSpellEntryClass
*klass
)
82 GObjectClass
*g_object_class
;
83 GtkWidgetClass
*widget_class
;
85 parent_class
= g_type_class_peek_parent(klass
);
87 g_object_class
= G_OBJECT_CLASS(klass
);
88 g_object_class
->finalize
= claws_spell_entry_finalize
;
90 widget_class
= GTK_WIDGET_CLASS(klass
);
91 widget_class
->button_press_event
= claws_spell_entry_button_press
;
92 widget_class
->draw
= claws_spell_entry_expose
;
93 widget_class
->destroy
= claws_spell_entry_destroy
;
96 static void claws_spell_entry_init(ClawsSpellEntry
*entry
)
98 entry
->gtkaspell
= NULL
;
100 entry
->priv
= g_new0(ClawsSpellEntryPriv
, 1);
101 entry
->priv
->attr_list
= pango_attr_list_new();
102 entry
->priv
->preedit_length
= 0;
104 g_signal_connect(G_OBJECT(entry
), "popup-menu",
105 G_CALLBACK(claws_spell_entry_popup_menu
), entry
);
106 g_signal_connect(G_OBJECT(entry
), "populate-popup",
107 G_CALLBACK(claws_spell_entry_populate_popup
), NULL
);
108 g_signal_connect(G_OBJECT(entry
), "changed",
109 G_CALLBACK(claws_spell_entry_changed
), NULL
);
110 g_signal_connect(G_OBJECT(entry
), "preedit-changed",
111 G_CALLBACK(claws_spell_entry_preedit_changed
), NULL
);
114 static void claws_spell_entry_finalize(GObject
*object
)
116 ClawsSpellEntry
*entry
= CLAWS_SPELL_ENTRY(object
);
118 if (entry
->priv
->attr_list
)
119 pango_attr_list_unref(entry
->priv
->attr_list
);
120 if (entry
->priv
->words
)
121 g_strfreev(entry
->priv
->words
);
123 g_free(entry
->priv
->word_starts
);
124 g_free(entry
->priv
->word_ends
);
128 G_OBJECT_CLASS(parent_class
)->finalize(object
);
131 static void claws_spell_entry_destroy(GtkWidget
*object
)
133 GTK_WIDGET_CLASS(parent_class
)->destroy(object
);
136 GtkWidget
*claws_spell_entry_new(void)
138 return GTK_WIDGET( g_object_new(CLAWS_TYPE_SPELL_ENTRY
, NULL
) );
141 void claws_spell_entry_set_gtkaspell(ClawsSpellEntry
*entry
, GtkAspell
*gtkaspell
)
143 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
145 entry
->gtkaspell
= gtkaspell
;
148 static gint
claws_spell_entry_find_position (ClawsSpellEntry
*_entry
, gint x
)
151 PangoLayoutLine
*line
;
155 gint pos
, current_pos
;
158 GtkEntry
*entry
= GTK_ENTRY(_entry
);
160 g_object_get(entry
, "scroll-offset", &scroll_offset
, NULL
);
161 x
= x
+ scroll_offset
;
163 layout
= gtk_entry_get_layout(entry
);
164 text
= pango_layout_get_text(layout
);
165 g_object_get(entry
, "cursor-position", ¤t_pos
, NULL
);
166 cursor_index
= g_utf8_offset_to_pointer(text
, current_pos
) - text
;
168 line
= pango_layout_get_lines(layout
)->data
;
169 pango_layout_line_x_to_index(line
, x
* PANGO_SCALE
, &index
, &trailing
);
171 if (index
>= cursor_index
&& _entry
->priv
->preedit_length
) {
172 if (index
>= cursor_index
+ _entry
->priv
->preedit_length
) {
173 index
-= _entry
->priv
->preedit_length
;
175 index
= cursor_index
;
180 pos
= g_utf8_pointer_to_offset (text
, text
+ index
);
186 static void get_word_extents_from_position(ClawsSpellEntry
*entry
, gint
*start
,
187 gint
*end
, guint position
)
195 if (entry
->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; entry
->priv
->words
[i
]; i
++) {
202 if (bytes_pos
>= entry
->priv
->word_starts
[i
] &&
203 bytes_pos
<= entry
->priv
->word_ends
[i
]) {
204 *start
= entry
->priv
->word_starts
[i
];
205 *end
= entry
->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
));
231 start_pos
= entry
->gtkaspell
->start_pos
;
232 end_pos
= entry
->gtkaspell
->end_pos
;
234 cursor
= gtk_editable_get_position(GTK_EDITABLE(entry
));
235 /* is the cursor at the end? If so, restore it there */
236 if (g_utf8_strlen(text
, -1) == cursor
)
238 else if(cursor
< entry
->priv
->mark_character
||
239 cursor
> entry
->priv
->mark_character
)
240 cursor
= entry
->priv
->mark_character
;
242 gtk_editable_delete_text(GTK_EDITABLE(entry
), start_pos
, end_pos
);
243 gtk_editable_insert_text(GTK_EDITABLE(entry
), newword
, strlen(newword
),
245 gtk_editable_set_position(GTK_EDITABLE(entry
), cursor
);
249 static gboolean
word_misspelled(ClawsSpellEntry
*entry
, int start
, int end
)
254 word
= get_word(entry
, start
, end
);
255 if (word
== NULL
|| g_unichar_isdigit(word
[0])) {
261 ret
= gtkaspell_misspelled_test(entry
->gtkaspell
, word
);
267 static gboolean
is_word_end (GtkEntry
*entry
, const int offset
)
269 gchar
*p
= gtk_editable_get_chars(GTK_EDITABLE(entry
), offset
, offset
+1);
272 ch
= g_utf8_get_char(p
);
279 p
= gtk_editable_get_chars(GTK_EDITABLE(entry
), offset
+1, offset
+2);
280 ch
= g_utf8_get_char(p
);
283 return (g_unichar_isspace(ch
) || g_unichar_ispunct(ch
)
284 || g_unichar_isdigit(ch
));
287 return (g_unichar_isspace(ch
) || g_unichar_ispunct(ch
));
290 static void entry_strsplit_utf8(GtkEntry
*entry
, gchar
***set
, gint
**starts
, gint
**ends
)
293 PangoLogAttr
*log_attrs
;
295 gint n_attrs
, n_strings
, i
, j
;
297 layout
= gtk_entry_get_layout(GTK_ENTRY(entry
));
298 text
= gtk_entry_get_text(GTK_ENTRY(entry
));
299 pango_layout_get_log_attrs(layout
, &log_attrs
, &n_attrs
);
301 /* Find how many words we have */
303 for (i
= 0; i
< n_attrs
; i
++)
304 if (log_attrs
[i
].is_word_start
)
307 *set
= g_new0(gchar
*, n_strings
+ 1);
308 *starts
= g_new0(gint
, n_strings
);
309 *ends
= g_new0(gint
, n_strings
);
311 /* Copy out strings */
312 for (i
= 0, j
= 0; i
< n_attrs
; i
++) {
313 if (log_attrs
[i
].is_word_start
) {
317 /* Find the end of this string */
319 while (!is_word_end(entry
, cend
))
322 /* Copy sub-string */
323 start
= g_utf8_offset_to_pointer(text
, i
);
324 bytes
= (gint
) (g_utf8_offset_to_pointer(text
, cend
) - start
);
325 (*set
)[j
] = g_new0(gchar
, bytes
+ 1);
326 (*starts
)[j
] = (gint
) (start
- text
);
327 (*ends
)[j
] = (gint
) (start
- text
+ bytes
);
328 g_utf8_strncpy((*set
)[j
], start
, cend
- i
);
330 /* Move on to the next word */
338 static void insert_misspelled_marker(ClawsSpellEntry
*entry
, guint start
, guint end
)
340 GdkRGBA
*rgba
= &prefs_common
.color
[COL_MISSPELLED
];
341 guint16 red
= (guint16
)(rgba
->red
* 65535);
342 guint16 green
= (guint16
)(rgba
->green
* 65535);
343 guint16 blue
= (guint16
)(rgba
->blue
* 65535);
344 PangoAttribute
*fcolor
;
346 fcolor
= pango_attr_foreground_new(red
, green
, blue
);
347 fcolor
->start_index
= start
;
348 fcolor
->end_index
= end
;
350 pango_attr_list_insert(entry
->priv
->attr_list
, fcolor
);
353 static gboolean
check_word(ClawsSpellEntry
*entry
, int start
, int end
)
355 GtkAspell
*gtkaspell
= entry
->gtkaspell
;
356 PangoAttrIterator
*it
;
359 gchar
*text
= gtk_editable_get_chars(GTK_EDITABLE(entry
), 0, -1);
362 /* Check to see if we've got any attributes at this position.
363 * If so, free them, since we'll readd it if the word is misspelled */
364 it
= pango_attr_list_get_iterator(entry
->priv
->attr_list
);
368 pango_attr_iterator_range(it
, &s
, &e
);
370 GSList
*attrs
= pango_attr_iterator_get_attrs(it
);
371 g_slist_foreach(attrs
, (GFunc
) pango_attribute_destroy
, NULL
);
374 } while (pango_attr_iterator_next(it
));
375 pango_attr_iterator_destroy(it
);
377 if ((misspelled
= word_misspelled(entry
, start
, end
))) {
378 insert_misspelled_marker(entry
, start
, end
);
380 word
= get_word(entry
, start
, end
);
381 strncpy(gtkaspell
->theword
, (gchar
*)word
, GTKASPELLWORDSIZE
- 1);
382 gtkaspell
->theword
[GTKASPELLWORDSIZE
- 1] = 0;
383 gtkaspell
->start_pos
= g_utf8_pointer_to_offset(text
, (text
+start
));
384 gtkaspell
->end_pos
= g_utf8_pointer_to_offset(text
, (text
+end
));
385 gtkaspell_free_suggestions_list(gtkaspell
);
394 void claws_spell_entry_recheck_all(ClawsSpellEntry
*entry
)
396 GtkAllocation allocation
;
401 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
402 cm_return_if_fail(entry
->gtkaspell
!= NULL
);
404 if (entry
->priv
->words
== NULL
)
407 /* Remove all existing pango attributes. These will get readded as we check */
408 pango_attr_list_unref(entry
->priv
->attr_list
);
409 entry
->priv
->attr_list
= pango_attr_list_new();
411 /* Loop through words */
412 for (i
= 0; entry
->priv
->words
[i
]; i
++) {
413 length
= strlen(entry
->priv
->words
[i
]);
416 check_word(entry
, entry
->priv
->word_starts
[i
], entry
->priv
->word_ends
[i
]);
419 layout
= gtk_entry_get_layout(GTK_ENTRY(entry
));
420 pango_layout_set_attributes(layout
, entry
->priv
->attr_list
);
422 if (gtk_widget_get_realized(GTK_WIDGET(entry
))) {
423 rect
.x
= 0; rect
.y
= 0;
424 gtk_widget_get_allocation(GTK_WIDGET(entry
), &allocation
);
425 rect
.width
= allocation
.width
;
426 rect
.height
= allocation
.height
;
427 gdk_window_invalidate_rect(gtk_widget_get_window(GTK_WIDGET(entry
)),
432 static gint
claws_spell_entry_expose(GtkWidget
*widget
, cairo_t
*cr
)
434 ClawsSpellEntry
*entry
= CLAWS_SPELL_ENTRY(widget
);
435 GtkEntry
*gtk_entry
= GTK_ENTRY(widget
);
438 if (entry
->gtkaspell
!= NULL
) {
439 layout
= gtk_entry_get_layout(gtk_entry
);
440 pango_layout_set_attributes(layout
, entry
->priv
->attr_list
);
443 return GTK_WIDGET_CLASS(parent_class
)->draw (widget
, cr
);
446 static gint
claws_spell_entry_button_press(GtkWidget
*widget
, GdkEventButton
*event
)
448 ClawsSpellEntry
*entry
= CLAWS_SPELL_ENTRY(widget
);
451 pos
= claws_spell_entry_find_position(entry
, event
->x
);
452 entry
->priv
->mark_character
= pos
;
454 return GTK_WIDGET_CLASS(parent_class
)->button_press_event (widget
, event
);
457 static gboolean
claws_spell_entry_popup_menu(GtkWidget
*widget
, ClawsSpellEntry
*entry
)
459 entry
->priv
->mark_character
= gtk_editable_get_position (GTK_EDITABLE (entry
));
463 static void set_position(gpointer data
, gint pos
)
465 gtk_editable_set_position(GTK_EDITABLE(data
), pos
);
468 static gboolean
find_misspelled_cb(gpointer data
, gboolean forward
)
470 ClawsSpellEntry
*entry
= (ClawsSpellEntry
*)data
;
471 GtkAspell
*gtkaspell
= entry
->gtkaspell
;
472 gboolean misspelled
= FALSE
;
473 gint cursor
, minpos
, maxpos
, i
, words_len
= 0;
477 if (entry
->priv
->words
== NULL
)
480 gtkaspell
->orig_pos
= gtk_editable_get_position(GTK_EDITABLE(entry
));
481 text
= gtk_editable_get_chars(GTK_EDITABLE(entry
), 0, -1);
482 cursor
= g_utf8_offset_to_pointer(text
, gtkaspell
->orig_pos
) - text
;
484 if (gtk_editable_get_selection_bounds(GTK_EDITABLE(entry
), &start
, &end
)) {
485 minpos
= g_utf8_offset_to_pointer(text
, start
) - text
;
486 maxpos
= g_utf8_offset_to_pointer(text
, end
) - text
;
488 minpos
= forward
? cursor
: 0;
489 maxpos
= forward
? strlen(text
)-1 : cursor
;
493 while(entry
->priv
->words
[words_len
])
497 for(i
=0; i
< words_len
; i
++)
498 if (entry
->priv
->word_ends
[i
] > minpos
&&
499 (misspelled
= check_word(entry
,
500 entry
->priv
->word_starts
[i
],
501 entry
->priv
->word_ends
[i
])))
504 for(i
=words_len
-1; i
>= 0; i
--)
505 if (entry
->priv
->word_starts
[i
] < maxpos
&&
506 (misspelled
= check_word(entry
,
507 entry
->priv
->word_starts
[i
],
508 entry
->priv
->word_ends
[i
])))
515 static gboolean
check_word_cb(gpointer data
)
517 ClawsSpellEntry
*entry
= (ClawsSpellEntry
*)data
;
520 get_word_extents_from_position(entry
, &start
, &end
, entry
->priv
->mark_character
);
521 return check_word(entry
, start
, end
);
524 static void replace_word_cb(gpointer data
, const gchar
*newword
)
526 replace_word((ClawsSpellEntry
*) data
, newword
);
529 static void set_menu_pos(GtkMenu
*menu
, gint
*x
, gint
*y
,
530 gboolean
*push_in
, gpointer data
)
532 ClawsSpellEntry
*entry
= (ClawsSpellEntry
*) data
;
533 GtkAspell
*gtkaspell
= entry
->gtkaspell
;
534 gint pango_offset
, win_x
, win_y
, scr_x
, scr_y
, text_index
, entry_x
;
536 PangoLayout
*layout
= gtk_entry_get_layout(GTK_ENTRY(entry
));
537 PangoLayoutLine
*line
= pango_layout_get_lines(layout
)->data
;
539 gtk_widget_get_preferred_size(GTK_WIDGET(entry
), NULL
, NULL
);
541 /* screen -> compose window coords */
542 gdk_window_get_origin(gtk_widget_get_window(GTK_WIDGET(gtkaspell
->parent_window
)),
545 /* compose window -> subject entry coords */
546 gtk_widget_translate_coordinates(GTK_WIDGET(entry
),
547 gtkaspell
->parent_window
, 0, 0, &win_x
, &win_y
);
549 text
= gtk_editable_get_chars(GTK_EDITABLE(entry
), 0, -1);
550 text_index
= g_utf8_offset_to_pointer(text
, gtkaspell
->end_pos
) - text
;
553 pango_offset
= gtk_entry_text_index_to_layout_index(GTK_ENTRY(entry
),
555 pango_layout_line_index_to_x(line
, pango_offset
, TRUE
, &entry_x
);
558 void claws_spell_entry_context_set(ClawsSpellEntry
*entry
)
560 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
561 cm_return_if_fail(entry
->gtkaspell
!= NULL
);
563 entry
->gtkaspell
->ctx
.set_position
= set_position
;
564 entry
->gtkaspell
->ctx
.set_menu_pos
= set_menu_pos
;
565 entry
->gtkaspell
->ctx
.find_misspelled
= find_misspelled_cb
;
566 entry
->gtkaspell
->ctx
.check_word
= check_word_cb
;
567 entry
->gtkaspell
->ctx
.replace_word
= replace_word_cb
;
568 entry
->gtkaspell
->ctx
.data
= (gpointer
) entry
;
571 static void claws_spell_entry_populate_popup(ClawsSpellEntry
*entry
, GtkMenu
*menu
,
574 GtkAspell
*gtkaspell
= entry
->gtkaspell
;
578 if (gtkaspell
== NULL
)
581 get_word_extents_from_position(entry
, &start
, &end
, entry
->priv
->mark_character
);
583 if ((word
= get_word(entry
, start
, end
)) != NULL
) {
584 strncpy(gtkaspell
->theword
, word
, GTKASPELLWORDSIZE
- 1);
588 gtkaspell
->misspelled
= word_misspelled(entry
, start
, end
);
590 text
= gtk_editable_get_chars(GTK_EDITABLE(entry
), 0, -1);
591 gtkaspell
->start_pos
= g_utf8_pointer_to_offset(text
, (text
+start
));
592 gtkaspell
->end_pos
= g_utf8_pointer_to_offset(text
, (text
+end
));
595 claws_spell_entry_context_set(entry
);
596 gtkaspell_make_context_menu(menu
, gtkaspell
);
599 static void claws_spell_entry_changed(GtkEditable
*editable
, gpointer data
)
601 ClawsSpellEntry
*entry
= CLAWS_SPELL_ENTRY(editable
);
603 if (entry
->gtkaspell
== NULL
)
606 if (entry
->priv
->words
) {
607 g_strfreev(entry
->priv
->words
);
608 g_free(entry
->priv
->word_starts
);
609 g_free(entry
->priv
->word_ends
);
611 entry_strsplit_utf8(GTK_ENTRY(entry
), &entry
->priv
->words
,
612 &entry
->priv
->word_starts
, &entry
->priv
->word_ends
);
613 if(entry
->gtkaspell
->check_while_typing
== TRUE
)
614 claws_spell_entry_recheck_all(entry
);
617 static void claws_spell_entry_preedit_changed (GtkEntry
*_entry
,
621 ClawsSpellEntry
*entry
= CLAWS_SPELL_ENTRY(_entry
);
623 entry
->priv
->preedit_length
= preedit
!= NULL
? strlen(preedit
) : 0;
626 static void continue_check(gpointer
*data
)
628 ClawsSpellEntry
*entry
= (ClawsSpellEntry
*)data
;
629 GtkAspell
*gtkaspell
= entry
->gtkaspell
;
630 gint pos
= gtk_editable_get_position(GTK_EDITABLE(entry
));
632 if (gtkaspell
->misspelled
&& pos
< gtkaspell
->end_check_pos
)
633 gtkaspell
->misspelled
= gtkaspell_check_next_prev(gtkaspell
, TRUE
);
635 gtkaspell
->continue_check
= NULL
;
638 void claws_spell_entry_check_all(ClawsSpellEntry
*entry
)
643 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
644 cm_return_if_fail(entry
->gtkaspell
!= NULL
);
646 if (!gtk_editable_get_selection_bounds(GTK_EDITABLE(entry
), &start
, &end
)) {
647 text
= gtk_editable_get_chars(GTK_EDITABLE(entry
), 0, -1);
650 end
= g_utf8_strlen(text
, -1) - 1;
655 gtk_editable_set_position(GTK_EDITABLE(entry
), start
);
656 entry
->gtkaspell
->continue_check
= continue_check
;
657 entry
->gtkaspell
->end_check_pos
= end
;
659 claws_spell_entry_context_set(entry
);
660 entry
->gtkaspell
->misspelled
=
661 gtkaspell_check_next_prev(entry
->gtkaspell
, TRUE
);
664 void claws_spell_entry_check_backwards(ClawsSpellEntry
*entry
)
666 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
667 cm_return_if_fail(entry
->gtkaspell
!= NULL
);
669 entry
->gtkaspell
->continue_check
= NULL
;
670 claws_spell_entry_context_set(entry
);
671 gtkaspell_check_next_prev(entry
->gtkaspell
, FALSE
);
674 void claws_spell_entry_check_forwards_go(ClawsSpellEntry
*entry
)
676 cm_return_if_fail(CLAWS_IS_SPELL_ENTRY(entry
));
677 cm_return_if_fail(entry
->gtkaspell
!= NULL
);
679 entry
->gtkaspell
->continue_check
= NULL
;
680 claws_spell_entry_context_set(entry
);
681 gtkaspell_check_next_prev(entry
->gtkaspell
, TRUE
);
684 #endif /* USE_ENCHANT */