1 /* gbp-spell-widget.c
2 *
3 * Copyright 2016 Sebastien Lafargue <slafargue@gnome.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "gbp-spell-widget"
22
23 #include <dazzle.h>
24 #include <libide-editor.h>
25 #include <glib/gi18n.h>
26 #include <gspell/gspell.h>
27
28 #include "gbp-spell-dict.h"
29 #include "gbp-spell-language-popover.h"
30 #include "gbp-spell-navigator.h"
31 #include "gbp-spell-private.h"
32 #include "gbp-spell-widget.h"
33
34 G_DEFINE_FINAL_TYPE (GbpSpellWidget, gbp_spell_widget, GTK_TYPE_BIN)
35
36 #define CHECK_WORD_INTERVAL_MIN 100
37 #define DICT_CHECK_WORD_INTERVAL_MIN 100
38 #define WORD_ENTRY_MAX_SUGGESTIONS 6
39
40 enum {
41 PROP_0,
42 PROP_EDITOR,
43 N_PROPS
44 };
45
46 static GParamSpec *properties [N_PROPS];
47
48 static void
clear_suggestions_box(GbpSpellWidget * self)49 clear_suggestions_box (GbpSpellWidget *self)
50 {
51 g_assert (GBP_IS_SPELL_WIDGET (self));
52
53 gtk_container_foreach (GTK_CONTAINER (self->suggestions_box),
54 (GtkCallback)gtk_widget_destroy,
55 NULL);
56 }
57
58 static void
update_global_sensiblility(GbpSpellWidget * self,gboolean sensibility)59 update_global_sensiblility (GbpSpellWidget *self,
60 gboolean sensibility)
61 {
62 g_assert (GBP_IS_SPELL_WIDGET (self));
63
64 gtk_entry_set_text (self->word_entry, "");
65 clear_suggestions_box (self);
66 _gbp_spell_widget_update_actions (self);
67 }
68
69 GtkWidget *
_gbp_spell_widget_get_entry(GbpSpellWidget * self)70 _gbp_spell_widget_get_entry (GbpSpellWidget *self)
71 {
72 g_return_val_if_fail (GBP_IS_SPELL_WIDGET (self), NULL);
73
74 return GTK_WIDGET (self->word_entry);
75 }
76
77 static GtkWidget *
create_suggestion_row(GbpSpellWidget * self,const gchar * word)78 create_suggestion_row (GbpSpellWidget *self,
79 const gchar *word)
80 {
81 g_assert (GBP_IS_SPELL_WIDGET (self));
82 g_assert (!dzl_str_empty0 (word));
83
84 return g_object_new (GTK_TYPE_LABEL,
85 "label", word,
86 "visible", TRUE,
87 "xalign", 0.0f,
88 NULL);
89 }
90
91 static void
fill_suggestions_box(GbpSpellWidget * self,const gchar * word,gchar ** first_result)92 fill_suggestions_box (GbpSpellWidget *self,
93 const gchar *word,
94 gchar **first_result)
95 {
96 GspellChecker *checker;
97 GSList *suggestions = NULL;
98 GtkWidget *item;
99
100 g_assert (GBP_IS_SPELL_WIDGET (self));
101 g_assert (first_result != NULL);
102
103 *first_result = NULL;
104
105 clear_suggestions_box (self);
106
107 if (dzl_str_empty0 (word))
108 {
109 gtk_widget_set_sensitive (GTK_WIDGET (self->suggestions_box), FALSE);
110 return;
111 }
112
113 if (self->editor_page_addin != NULL)
114 {
115 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
116 suggestions = gspell_checker_get_suggestions (checker, word, -1);
117 }
118
119 if (suggestions == NULL)
120 {
121 gtk_label_set_text (GTK_LABEL (self->placeholder), _("No suggestions"));
122 gtk_widget_set_sensitive (GTK_WIDGET (self->suggestions_box), FALSE);
123 }
124 else
125 {
126 *first_result = g_strdup (suggestions->data);
127
128 gtk_widget_set_sensitive (GTK_WIDGET (self->suggestions_box), TRUE);
129
130 for (const GSList *iter = suggestions; iter; iter = iter->next)
131 {
132 const gchar *iter_word = iter->data;
133 item = create_suggestion_row (self, iter_word);
134 gtk_list_box_insert (self->suggestions_box, item, -1);
135 }
136
137 g_slist_free_full (suggestions, g_free);
138 }
139 }
140
141 static void
update_count_label(GbpSpellWidget * self)142 update_count_label (GbpSpellWidget *self)
143 {
144 GspellNavigator *navigator;
145 const gchar *word;
146 guint count;
147
148 g_assert (GBP_IS_SPELL_WIDGET (self));
149
150 if (self->editor_page_addin == NULL)
151 return;
152
153 navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
154 word = gtk_label_get_text (self->word_label);
155 count = gbp_spell_navigator_get_count (GBP_SPELL_NAVIGATOR (navigator), word);
156
157 if (count > 0)
158 {
159 g_autofree gchar *count_text = NULL;
160
161 if (count > 1000)
162 count_text = g_strdup (">1000");
163 else
164 count_text = g_strdup_printf ("%i", count);
165
166 gtk_label_set_text (self->count_label, count_text);
167 gtk_widget_set_visible (GTK_WIDGET (self->count_box), TRUE);
168 }
169 else
170 gtk_widget_set_visible (GTK_WIDGET (self->count_box), TRUE);
171
172 self->current_word_count = count;
173
174 _gbp_spell_widget_update_actions (self);
175 }
176
177 gboolean
_gbp_spell_widget_move_next_word(GbpSpellWidget * self)178 _gbp_spell_widget_move_next_word (GbpSpellWidget *self)
179 {
180 g_autofree gchar *word = NULL;
181 g_autofree gchar *first_result = NULL;
182 g_autoptr(GError) error = NULL;
183 GspellNavigator *navigator;
184 GtkListBoxRow *row;
185 gboolean ret = FALSE;
186
187 g_assert (GBP_IS_SPELL_WIDGET (self));
188
189 if (self->editor_page_addin == NULL)
190 return FALSE;
191
192 navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
193
194 if ((ret = gspell_navigator_goto_next (navigator, &word, NULL, &error)))
195 {
196 gtk_label_set_text (self->word_label, word);
197 update_count_label (self);
198
199 fill_suggestions_box (self, word, &first_result);
200
201 if (!dzl_str_empty0 (first_result))
202 {
203 row = gtk_list_box_get_row_at_index (self->suggestions_box, 0);
204 gtk_list_box_select_row (self->suggestions_box, row);
205 }
206 }
207 else
208 {
209 if (error != NULL)
210 gtk_label_set_text (GTK_LABEL (self->placeholder), error->message);
211
212 self->spellchecking_status = FALSE;
213
214 gtk_label_set_text (GTK_LABEL (self->placeholder), _("Completed spell checking"));
215 update_global_sensiblility (self, FALSE);
216 }
217
218 _gbp_spell_widget_update_actions (self);
219
220 return ret;
221 }
222
223 static gboolean
check_word_timeout_cb(GbpSpellWidget * self)224 check_word_timeout_cb (GbpSpellWidget *self)
225 {
226 g_autoptr(GError) error = NULL;
227 GspellChecker *checker;
228 const gchar *icon_name = "";
229 const gchar *word;
230 gboolean ret = TRUE;
231
232 g_assert (GBP_IS_SPELL_WIDGET (self));
233 g_assert (self->editor_page_addin != NULL);
234
235 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
236
237 self->check_word_state = CHECK_WORD_CHECKING;
238
239 word = gtk_entry_get_text (self->word_entry);
240
241 if (!dzl_str_empty0 (word))
242 {
243 /* FIXME: suggestions can give a multiple-words suggestion
244 * that failed to the checkword test, ex: auto tools
245 */
246 ret = gspell_checker_check_word (checker, word, -1, &error);
247 if (error != NULL)
248 {
249 g_message ("check error:%s\n", error->message);
250 }
251 }
252
253 if (!ret)
254 {
255 icon_name = "dialog-warning-symbolic";
256 gtk_widget_set_tooltip_text (GTK_WIDGET (self->word_entry),
257 _("The word is not in the dictionary"));
258 }
259 else
260 gtk_widget_set_tooltip_text (GTK_WIDGET (self->word_entry), NULL);
261
262 gtk_entry_set_icon_from_icon_name (self->word_entry,
263 GTK_ENTRY_ICON_SECONDARY,
264 icon_name);
265
266 self->check_word_state = CHECK_WORD_NONE;
267 self->is_word_entry_valid = ret;
268
269 self->check_word_timeout_id = 0;
270
271 if (self->is_check_word_invalid == TRUE)
272 {
273 self->check_word_timeout_id = g_timeout_add_full (G_PRIORITY_LOW,
274 CHECK_WORD_INTERVAL_MIN,
275 (GSourceFunc)check_word_timeout_cb,
276 g_object_ref (self),
277 g_object_unref);
278 self->check_word_state = CHECK_WORD_IDLE;
279 self->is_check_word_invalid = FALSE;
280 }
281
282 return G_SOURCE_REMOVE;
283 }
284
285 static void
gbp_spell_widget__word_entry_changed_cb(GbpSpellWidget * self,GtkEntry * entry)286 gbp_spell_widget__word_entry_changed_cb (GbpSpellWidget *self,
287 GtkEntry *entry)
288 {
289 const gchar *word;
290
291 g_assert (GBP_IS_SPELL_WIDGET (self));
292 g_assert (GTK_IS_ENTRY (entry));
293
294 _gbp_spell_widget_update_actions (self);
295
296 word = gtk_entry_get_text (self->word_entry);
297 if (dzl_str_empty0 (word) && self->spellchecking_status == TRUE)
298 {
299 word = gtk_label_get_text (self->word_label);
300 gtk_entry_set_text (GTK_ENTRY (self->dict_word_entry), word);
301 }
302 else
303 gtk_entry_set_text (GTK_ENTRY (self->dict_word_entry), word);
304
305 if (self->check_word_state == CHECK_WORD_CHECKING)
306 {
307 self->is_check_word_invalid = TRUE;
308 return;
309 }
310
311 dzl_clear_source (&self->check_word_timeout_id);
312
313 if (self->editor_page_addin != NULL)
314 {
315 self->check_word_timeout_id = g_timeout_add_full (G_PRIORITY_LOW,
316 CHECK_WORD_INTERVAL_MIN,
317 (GSourceFunc)check_word_timeout_cb,
318 g_object_ref (self),
319 g_object_unref);
320 self->check_word_state = CHECK_WORD_IDLE;
321 }
322 }
323
324 static void
gbp_spell_widget__row_selected_cb(GbpSpellWidget * self,GtkListBoxRow * row,GtkListBox * listbox)325 gbp_spell_widget__row_selected_cb (GbpSpellWidget *self,
326 GtkListBoxRow *row,
327 GtkListBox *listbox)
328 {
329 const gchar *word;
330 GtkLabel *label;
331
332 g_assert (GBP_IS_SPELL_WIDGET (self));
333 g_assert (GTK_IS_LIST_BOX_ROW (row) || row == NULL);
334 g_assert (GTK_IS_LIST_BOX (listbox));
335
336 if (row != NULL)
337 {
338 label = GTK_LABEL (gtk_bin_get_child (GTK_BIN (row)));
339 word = gtk_label_get_text (label);
340
341 g_signal_handlers_block_by_func (self->word_entry, gbp_spell_widget__word_entry_changed_cb, self);
342
343 gtk_entry_set_text (self->word_entry, word);
344 gtk_editable_set_position (GTK_EDITABLE (self->word_entry), -1);
345 _gbp_spell_widget_update_actions (self);
346
347 g_signal_handlers_unblock_by_func (self->word_entry, gbp_spell_widget__word_entry_changed_cb, self);
348 }
349 }
350
351 static void
gbp_spell_widget__row_activated_cb(GbpSpellWidget * self,GtkListBoxRow * row,GtkListBox * listbox)352 gbp_spell_widget__row_activated_cb (GbpSpellWidget *self,
353 GtkListBoxRow *row,
354 GtkListBox *listbox)
355 {
356 g_assert (GBP_IS_SPELL_WIDGET (self));
357 g_assert (GTK_IS_LIST_BOX_ROW (row));
358 g_assert (GTK_IS_LIST_BOX (listbox));
359
360 if (row != NULL)
361 _gbp_spell_widget_change (self, FALSE);
362 }
363
364 static void
gbp_spell_widget__words_counted_cb(GbpSpellWidget * self,GParamSpec * pspec,GspellNavigator * navigator)365 gbp_spell_widget__words_counted_cb (GbpSpellWidget *self,
366 GParamSpec *pspec,
367 GspellNavigator *navigator)
368 {
369 g_assert (GBP_IS_SPELL_WIDGET (self));
370 g_assert (GSPELL_IS_NAVIGATOR (navigator));
371
372 update_count_label (self);
373 }
374
375 static GtkListBoxRow *
get_next_row_to_focus(GtkListBox * listbox,GtkListBoxRow * row)376 get_next_row_to_focus (GtkListBox *listbox,
377 GtkListBoxRow *row)
378 {
379 g_autoptr(GList) children = NULL;
380 gint index;
381 gint new_index;
382 gint len;
383
384 g_assert (GTK_IS_LIST_BOX (listbox));
385 g_assert (GTK_IS_LIST_BOX_ROW (row));
386
387 children = gtk_container_get_children (GTK_CONTAINER (listbox));
388 if (0 == (len = g_list_length (children)))
389 return NULL;
390
391 index = gtk_list_box_row_get_index (row);
392 if (index < len - 1)
393 new_index = index + 1;
394 else if (index == len - 1 && len > 1)
395 new_index = index - 1;
396 else
397 return NULL;
398
399 return gtk_list_box_get_row_at_index (listbox, new_index);
400 }
401
402 static gboolean
dict_check_word_timeout_cb(GbpSpellWidget * self)403 dict_check_word_timeout_cb (GbpSpellWidget *self)
404 {
405 g_autofree gchar *tooltip = NULL;
406 GspellChecker *checker;
407 const gchar *icon_name = "";
408 const gchar *word;
409 gboolean valid = FALSE;
410
411 g_assert (GBP_IS_SPELL_WIDGET (self));
412
413 if (self->editor_page_addin == NULL)
414 {
415 /* lost our chance */
416 self->dict_check_word_timeout_id = 0;
417 return G_SOURCE_REMOVE;
418 }
419
420 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
421
422 self->dict_check_word_state = CHECK_WORD_CHECKING;
423
424 word = gtk_entry_get_text (GTK_ENTRY (self->dict_word_entry));
425 if (!dzl_str_empty0 (word))
426 {
427 if (gbp_spell_dict_personal_contains (self->dict, word))
428 gtk_widget_set_tooltip_text (self->dict_word_entry, _("This word is already in the personal dictionary"));
429 else if (gspell_checker_check_word (checker, word, -1, NULL))
430 {
431 tooltip = g_strdup_printf (_("This word is already in the %s dictionary"), gspell_language_get_name (self->language));
432 gtk_widget_set_tooltip_text (self->dict_word_entry, tooltip);
433 }
434 else
435 {
436 valid = TRUE;
437 gtk_widget_set_tooltip_text (self->dict_word_entry, NULL);
438 }
439
440 if (!valid)
441 icon_name = "dialog-warning-symbolic";
442 }
443
444 gtk_widget_set_sensitive (GTK_WIDGET (self->dict_add_button), valid);
445 gtk_entry_set_icon_from_icon_name (GTK_ENTRY (self->dict_word_entry),
446 GTK_ENTRY_ICON_SECONDARY,
447 icon_name);
448
449 self->dict_check_word_state = CHECK_WORD_NONE;
450
451 self->dict_check_word_timeout_id = 0;
452 if (self->is_dict_check_word_invalid == TRUE)
453 {
454 self->dict_check_word_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT,
455 DICT_CHECK_WORD_INTERVAL_MIN,
456 (GSourceFunc)dict_check_word_timeout_cb,
457 self,
458 NULL);
459 self->dict_check_word_state = CHECK_WORD_IDLE;
460 self->is_dict_check_word_invalid = FALSE;
461 }
462
463 return G_SOURCE_REMOVE;
464 }
465
466 static void
gbp_spell_widget__dict_word_entry_changed_cb(GbpSpellWidget * self,GtkEntry * dict_word_entry)467 gbp_spell_widget__dict_word_entry_changed_cb (GbpSpellWidget *self,
468 GtkEntry *dict_word_entry)
469 {
470 g_assert (GBP_IS_SPELL_WIDGET (self));
471 g_assert (GTK_IS_ENTRY (dict_word_entry));
472
473 if (self->dict_check_word_state == CHECK_WORD_CHECKING)
474 {
475 self->is_dict_check_word_invalid = TRUE;
476 return;
477 }
478
479 if (self->dict_check_word_state == CHECK_WORD_IDLE)
480 {
481 g_source_remove (self->dict_check_word_timeout_id);
482 self->dict_check_word_timeout_id = 0;
483 }
484
485 self->dict_check_word_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT,
486 CHECK_WORD_INTERVAL_MIN,
487 (GSourceFunc)dict_check_word_timeout_cb,
488 self,
489 NULL);
490 self->dict_check_word_state = CHECK_WORD_IDLE;
491 }
492
493 static void
remove_dict_row(GbpSpellWidget * self,GtkListBox * listbox,GtkListBoxRow * row)494 remove_dict_row (GbpSpellWidget *self,
495 GtkListBox *listbox,
496 GtkListBoxRow *row)
497 {
498 GtkListBoxRow *next_row;
499 gchar *word;
500 gboolean exist;
501
502 g_assert (GBP_IS_SPELL_WIDGET (self));
503 g_assert (GTK_IS_LIST_BOX (listbox));
504 g_assert (GTK_IS_LIST_BOX_ROW (row));
505
506 word = g_object_get_data (G_OBJECT (row), "word");
507 exist = gbp_spell_dict_remove_word_from_personal (self->dict, word);
508 if (!exist)
509 g_warning ("The word %s do not exist in the personnal dictionary", word);
510
511 if (row == gtk_list_box_get_selected_row (listbox))
512 {
513 if (NULL != (next_row = get_next_row_to_focus (listbox, row)))
514 {
515 gtk_widget_grab_focus (GTK_WIDGET (next_row));
516 gtk_list_box_select_row (listbox, next_row);
517 }
518 else
519 gtk_widget_grab_focus (GTK_WIDGET (self->word_entry));
520 }
521
522 gtk_container_remove (GTK_CONTAINER (self->dict_words_list), GTK_WIDGET (row));
523 gbp_spell_widget__dict_word_entry_changed_cb (self, GTK_ENTRY (self->dict_word_entry));
524 }
525
526 static void
dict_close_button_clicked_cb(GbpSpellWidget * self,GtkButton * button)527 dict_close_button_clicked_cb (GbpSpellWidget *self,
528 GtkButton *button)
529 {
530 GtkWidget *row;
531
532 g_assert (GBP_IS_SPELL_WIDGET (self));
533 g_assert (GTK_IS_BUTTON (button));
534
535 if (NULL != (row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW)))
536 remove_dict_row (self, GTK_LIST_BOX (self->dict_words_list), GTK_LIST_BOX_ROW (row));
537 }
538
539 static gboolean
dict_row_key_pressed_event_cb(GbpSpellWidget * self,GdkEventKey * event,GtkListBox * listbox)540 dict_row_key_pressed_event_cb (GbpSpellWidget *self,
541 GdkEventKey *event,
542 GtkListBox *listbox)
543 {
544 GtkListBoxRow *row;
545
546 g_assert (GBP_IS_SPELL_WIDGET (self));
547 g_assert (event != NULL);
548 g_assert (GTK_IS_LIST_BOX (listbox));
549
550 if (event->keyval == GDK_KEY_Delete &&
551 NULL != (row = gtk_list_box_get_selected_row (listbox)))
552 {
553 remove_dict_row (self, GTK_LIST_BOX (self->dict_words_list), GTK_LIST_BOX_ROW (row));
554 return GDK_EVENT_STOP;
555 }
556
557 return GDK_EVENT_PROPAGATE;
558 }
559
560 static GtkWidget *
dict_create_word_row(GbpSpellWidget * self,const gchar * word)561 dict_create_word_row (GbpSpellWidget *self,
562 const gchar *word)
563 {
564 GtkWidget *row;
565 GtkWidget *box;
566 GtkWidget *label;
567 GtkWidget *button;
568 GtkStyleContext *style_context;
569
570 g_assert (GBP_IS_SPELL_WIDGET (self));
571 g_assert (!dzl_str_empty0 (word));
572
573 label = g_object_new (GTK_TYPE_LABEL,
574 "label", word,
575 "halign", GTK_ALIGN_START,
576 "visible", TRUE,
577 NULL);
578
579 button = gtk_button_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_BUTTON);
580 gtk_widget_set_visible (button, TRUE);
581 gtk_widget_set_can_focus (button, FALSE);
582 g_signal_connect_swapped (button,
583 "clicked",
584 G_CALLBACK (dict_close_button_clicked_cb),
585 self);
586
587 style_context = gtk_widget_get_style_context (button);
588 gtk_style_context_add_class (style_context, "close");
589
590 box = g_object_new (GTK_TYPE_BOX,
591 "orientation", GTK_ORIENTATION_HORIZONTAL,
592 "expand", TRUE,
593 "spacing", 6,
594 "visible", TRUE,
595 NULL);
596
597 gtk_box_pack_start (GTK_BOX (box), label, TRUE, TRUE, 0);
598 gtk_box_pack_end (GTK_BOX (box), button, FALSE, FALSE, 0);
599
600 row = gtk_list_box_row_new ();
601 gtk_widget_set_visible (row, TRUE);
602
603 gtk_container_add (GTK_CONTAINER (row), box);
604 g_object_set_data_full (G_OBJECT (row), "word", g_strdup (word), g_free);
605
606 return row;
607 }
608
609 static gboolean
check_dict_available(GbpSpellWidget * self)610 check_dict_available (GbpSpellWidget *self)
611 {
612 g_assert (GBP_IS_SPELL_WIDGET (self));
613
614 return (self->editor_page_addin != NULL && self->language != NULL);
615 }
616
617 static void
gbp_spell_widget__add_button_clicked_cb(GbpSpellWidget * self,GtkButton * button)618 gbp_spell_widget__add_button_clicked_cb (GbpSpellWidget *self,
619 GtkButton *button)
620 {
621 const gchar *word;
622 GtkWidget *item;
623 GtkWidget *toplevel;
624 GtkWidget *focused_widget;
625
626 g_assert (GBP_IS_SPELL_WIDGET (self));
627 g_assert (GTK_IS_BUTTON (button));
628
629 word = gtk_entry_get_text (GTK_ENTRY (self->dict_word_entry));
630
631 /* TODO: check if word already in dict */
632 if (check_dict_available (self) && !dzl_str_empty0 (word))
633 {
634 if (!gbp_spell_dict_add_word_to_personal (self->dict, word))
635 return;
636
637 item = dict_create_word_row (self, word);
638 gtk_list_box_insert (GTK_LIST_BOX (self->dict_words_list), item, 0);
639
640 toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
641 if (GTK_IS_WINDOW (toplevel) &&
642 NULL != (focused_widget = gtk_window_get_focus (GTK_WINDOW (toplevel))))
643 {
644 if (focused_widget != GTK_WIDGET (self->word_entry) &&
645 focused_widget != self->dict_word_entry)
646 gtk_widget_grab_focus (self->dict_word_entry);
647 }
648
649 gtk_entry_set_text (GTK_ENTRY (self->dict_word_entry), "");
650 }
651 }
652
653 static void
dict_clean_listbox(GbpSpellWidget * self)654 dict_clean_listbox (GbpSpellWidget *self)
655 {
656 GList *children;
657
658 g_assert (GBP_IS_SPELL_WIDGET (self));
659
660 children = gtk_container_get_children (GTK_CONTAINER (self->dict_words_list));
661 for (GList *l = children; l != NULL; l = g_list_next (l))
662 gtk_widget_destroy (GTK_WIDGET (l->data));
663 }
664
665 static void
dict_fill_listbox(GbpSpellWidget * self,GPtrArray * words_array)666 dict_fill_listbox (GbpSpellWidget *self,
667 GPtrArray *words_array)
668 {
669 const gchar *word;
670 GtkWidget *item;
671 guint len;
672
673 g_assert (GBP_IS_SPELL_WIDGET (self));
674 g_assert (words_array != NULL);
675
676 dict_clean_listbox (self);
677
678 len = words_array->len;
679 for (guint i = 0; i < len; ++i)
680 {
681 word = g_ptr_array_index (words_array, i);
682 item = dict_create_word_row (self, word);
683 gtk_list_box_insert (GTK_LIST_BOX (self->dict_words_list), item, -1);
684 }
685 }
686
687 static void
gbp_spell_widget__language_notify_cb(GbpSpellWidget * self,GParamSpec * pspec,GtkButton * language_chooser_button)688 gbp_spell_widget__language_notify_cb (GbpSpellWidget *self,
689 GParamSpec *pspec,
690 GtkButton *language_chooser_button)
691 {
692 const GspellLanguage *current_language;
693 const GspellLanguage *spell_language;
694 g_autofree gchar *word = NULL;
695 g_autofree gchar *first_result = NULL;
696 GspellNavigator *navigator;
697 GspellChecker *checker;
698 GtkListBoxRow *row;
699
700 g_assert (GBP_IS_SPELL_WIDGET (self));
701 g_assert (GTK_IS_BUTTON (language_chooser_button));
702
703 if (self->editor_page_addin == NULL)
704 return;
705
706 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
707 navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
708
709 current_language = gspell_checker_get_language (checker);
710 spell_language = gspell_language_chooser_get_language (GSPELL_LANGUAGE_CHOOSER (language_chooser_button));
711 if (gspell_language_compare (current_language, spell_language) != 0)
712 {
713 gspell_checker_set_language (checker, spell_language);
714 fill_suggestions_box (self, word, &first_result);
715 if (!dzl_str_empty0 (first_result))
716 {
717 row = gtk_list_box_get_row_at_index (self->suggestions_box, 0);
718 gtk_list_box_select_row (self->suggestions_box, row);
719 }
720
721 g_clear_pointer (&self->words_array, g_ptr_array_unref);
722
723 if (current_language == NULL)
724 {
725 dict_clean_listbox (self);
726 gtk_widget_set_sensitive (GTK_WIDGET (self->dict_add_button), FALSE);
727 gtk_widget_set_sensitive (GTK_WIDGET (self->dict_words_list), FALSE);
728
729 return;
730 }
731
732 gbp_spell_widget__dict_word_entry_changed_cb (self, GTK_ENTRY (self->dict_word_entry));
733 gtk_widget_set_sensitive (GTK_WIDGET (self->dict_words_list), TRUE);
734
735 gbp_spell_navigator_goto_word_start (GBP_SPELL_NAVIGATOR (navigator));
736
737 _gbp_spell_widget_move_next_word (self);
738 }
739 }
740
741 static void
gbp_spell_widget__word_entry_suggestion_activate(GbpSpellWidget * self,GtkMenuItem * item)742 gbp_spell_widget__word_entry_suggestion_activate (GbpSpellWidget *self,
743 GtkMenuItem *item)
744 {
745 gchar *word;
746
747 g_assert (GBP_IS_SPELL_WIDGET (self));
748 g_assert (GTK_IS_MENU_ITEM (item));
749
750 word = g_object_get_data (G_OBJECT (item), "word");
751
752 g_signal_handlers_block_by_func (self->word_entry, gbp_spell_widget__word_entry_changed_cb, self);
753
754 gtk_entry_set_text (self->word_entry, word);
755 gtk_editable_set_position (GTK_EDITABLE (self->word_entry), -1);
756 _gbp_spell_widget_update_actions (self);
757
758 g_signal_handlers_unblock_by_func (self->word_entry, gbp_spell_widget__word_entry_changed_cb, self);
759 }
760
761 static void
gbp_spell_widget__populate_popup_cb(GbpSpellWidget * self,GtkWidget * popup,GtkEntry * entry)762 gbp_spell_widget__populate_popup_cb (GbpSpellWidget *self,
763 GtkWidget *popup,
764 GtkEntry *entry)
765 {
766 GSList *suggestions = NULL;
767 GspellChecker *checker;
768 const gchar *text;
769 GtkWidget *item;
770 guint count = 0;
771
772 g_assert (GBP_IS_SPELL_WIDGET (self));
773 g_assert (GTK_IS_WIDGET (popup));
774 g_assert (GTK_IS_ENTRY (entry));
775
776 if (self->editor_page_addin == NULL)
777 return;
778
779 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
780 text = gtk_entry_get_text (entry);
781
782 if (!self->is_word_entry_valid && !dzl_str_empty0 (text))
783 suggestions = gspell_checker_get_suggestions (checker, text, -1);
784
785 if (suggestions == NULL)
786 return;
787
788 item = g_object_new (GTK_TYPE_SEPARATOR_MENU_ITEM,
789 "visible", TRUE,
790 NULL);
791 gtk_menu_shell_prepend (GTK_MENU_SHELL (popup), item);
792
793 suggestions = g_slist_reverse (suggestions);
794
795 for (const GSList *iter = suggestions; iter; iter = iter->next)
796 {
797 const gchar *word = iter->data;
798
799 item = g_object_new (GTK_TYPE_MENU_ITEM,
800 "label", word,
801 "visible", TRUE,
802 NULL);
803 g_object_set_data (G_OBJECT (item), "word", g_strdup (word));
804 gtk_menu_shell_prepend (GTK_MENU_SHELL (popup), item);
805 g_signal_connect_object (item,
806 "activate",
807 G_CALLBACK (gbp_spell_widget__word_entry_suggestion_activate),
808 self,
809 G_CONNECT_SWAPPED);
810
811 if (++count >= WORD_ENTRY_MAX_SUGGESTIONS)
812 break;
813 }
814
815 g_slist_free_full (suggestions, g_free);
816 }
817
818 static void
gbp_spell_widget__dict__loaded_cb(GbpSpellWidget * self,GbpSpellDict * dict)819 gbp_spell_widget__dict__loaded_cb (GbpSpellWidget *self,
820 GbpSpellDict *dict)
821 {
822 g_assert (GBP_IS_SPELL_WIDGET (self));
823 g_assert (GBP_IS_SPELL_DICT (dict));
824
825 self->words_array = gbp_spell_dict_get_words (self->dict);
826 dict_fill_listbox (self, self->words_array);
827 g_clear_pointer (&self->words_array, g_ptr_array_unref);
828 }
829
830 static void
gbp_spell_widget__word_label_notify_cb(GbpSpellWidget * self,GParamSpec * pspec,GtkLabel * word_label)831 gbp_spell_widget__word_label_notify_cb (GbpSpellWidget *self,
832 GParamSpec *pspec,
833 GtkLabel *word_label)
834 {
835 const gchar *text;
836
837 g_assert (GBP_IS_SPELL_WIDGET (self));
838 g_assert (GTK_IS_LABEL (word_label));
839
840 if (self->spellchecking_status == TRUE)
841 text = gtk_label_get_text (word_label);
842 else
843 text = "";
844
845 gtk_entry_set_text (GTK_ENTRY (self->dict_word_entry), text);
846 }
847
848 static void
gbp_spell_widget__close_button_clicked_cb(GbpSpellWidget * self,GtkButton * close_button)849 gbp_spell_widget__close_button_clicked_cb (GbpSpellWidget *self,
850 GtkButton *close_button)
851 {
852 g_assert (GBP_IS_SPELL_WIDGET (self));
853 g_assert (GTK_IS_BUTTON (close_button));
854
855 gbp_spell_widget_set_editor (self, NULL);
856 }
857
858 static void
gbp_spell_widget_constructed(GObject * object)859 gbp_spell_widget_constructed (GObject *object)
860 {
861 GbpSpellWidget *self = (GbpSpellWidget *)object;
862
863 _gbp_spell_widget_init_actions (self);
864 gbp_spell_widget__word_entry_changed_cb (self, self->word_entry);
865
866 g_signal_connect_swapped (self->word_entry,
867 "changed",
868 G_CALLBACK (gbp_spell_widget__word_entry_changed_cb),
869 self);
870
871 g_signal_connect_swapped (self->word_entry,
872 "populate-popup",
873 G_CALLBACK (gbp_spell_widget__populate_popup_cb),
874 self);
875
876 g_signal_connect_swapped (self->suggestions_box,
877 "row-selected",
878 G_CALLBACK (gbp_spell_widget__row_selected_cb),
879 self);
880
881 g_signal_connect_swapped (self->suggestions_box,
882 "row-activated",
883 G_CALLBACK (gbp_spell_widget__row_activated_cb),
884 self);
885
886 g_signal_connect_object (self->language_chooser_button,
887 "notify::language",
888 G_CALLBACK (gbp_spell_widget__language_notify_cb),
889 self,
890 G_CONNECT_SWAPPED);
891
892 g_signal_connect_swapped (self->dict_add_button,
893 "clicked",
894 G_CALLBACK (gbp_spell_widget__add_button_clicked_cb),
895 self);
896
897 g_signal_connect_swapped (self->dict_word_entry,
898 "changed",
899 G_CALLBACK (gbp_spell_widget__dict_word_entry_changed_cb),
900 self);
901
902 g_signal_connect_swapped (self->close_button,
903 "clicked",
904 G_CALLBACK (gbp_spell_widget__close_button_clicked_cb),
905 self);
906
907 self->placeholder = gtk_label_new (NULL);
908 gtk_widget_set_visible (self->placeholder, TRUE);
909 gtk_list_box_set_placeholder (self->suggestions_box, self->placeholder);
910
911 g_signal_connect_swapped (self->dict,
912 "loaded",
913 G_CALLBACK (gbp_spell_widget__dict__loaded_cb),
914 self);
915
916 g_signal_connect_object (self->word_label,
917 "notify::label",
918 G_CALLBACK (gbp_spell_widget__word_label_notify_cb),
919 self,
920 G_CONNECT_SWAPPED);
921 }
922
923 static void
gbp_spell_widget_bind_addin(GbpSpellWidget * self,GbpSpellEditorPageAddin * editor_page_addin,DzlSignalGroup * editor_page_addin_signals)924 gbp_spell_widget_bind_addin (GbpSpellWidget *self,
925 GbpSpellEditorPageAddin *editor_page_addin,
926 DzlSignalGroup *editor_page_addin_signals)
927 {
928 GspellChecker *checker;
929
930 g_assert (GBP_IS_SPELL_WIDGET (self));
931 g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (editor_page_addin));
932 g_assert (DZL_IS_SIGNAL_GROUP (editor_page_addin_signals));
933 g_assert (self->editor_page_addin == NULL);
934
935 self->editor_page_addin = g_object_ref (editor_page_addin);
936
937 gbp_spell_editor_page_addin_begin_checking (editor_page_addin);
938
939 checker = gbp_spell_editor_page_addin_get_checker (editor_page_addin);
940 gbp_spell_dict_set_checker (self->dict, checker);
941
942 self->language = gspell_checker_get_language (checker);
943 gspell_language_chooser_set_language (GSPELL_LANGUAGE_CHOOSER (self->language_chooser_button), self->language);
944
945 self->spellchecking_status = TRUE;
946
947 _gbp_spell_widget_move_next_word (self);
948 }
949
950 static void
gbp_spell_widget_unbind_addin(GbpSpellWidget * self,DzlSignalGroup * editor_page_addin_signals)951 gbp_spell_widget_unbind_addin (GbpSpellWidget *self,
952 DzlSignalGroup *editor_page_addin_signals)
953 {
954 g_assert (GBP_IS_SPELL_WIDGET (self));
955 g_assert (DZL_IS_SIGNAL_GROUP (editor_page_addin_signals));
956
957 if (self->editor_page_addin != NULL)
958 {
959 gbp_spell_editor_page_addin_end_checking (self->editor_page_addin);
960 gbp_spell_dict_set_checker (self->dict, NULL);
961 self->language = NULL;
962 gspell_language_chooser_set_language (GSPELL_LANGUAGE_CHOOSER (self->language_chooser_button), NULL);
963
964 g_clear_object (&self->editor_page_addin);
965
966 _gbp_spell_widget_update_actions (self);
967 }
968 }
969
970 static void
gbp_spell_widget_destroy(GtkWidget * widget)971 gbp_spell_widget_destroy (GtkWidget *widget)
972 {
973 GbpSpellWidget *self = (GbpSpellWidget *)widget;
974
975 g_assert (GBP_IS_SPELL_WIDGET (self));
976
977 dzl_clear_source (&self->check_word_timeout_id);
978 dzl_clear_source (&self->dict_check_word_timeout_id);
979
980 if (self->editor != NULL)
981 gbp_spell_widget_set_editor (self, NULL);
982
983 self->language = NULL;
984
985 /* Ensure reference holding things are released */
986 g_clear_object (&self->editor);
987 g_clear_object (&self->editor_page_addin);
988 g_clear_object (&self->editor_page_addin_signals);
989 g_clear_object (&self->dict);
990 g_clear_pointer (&self->words_array, g_ptr_array_unref);
991
992 GTK_WIDGET_CLASS (gbp_spell_widget_parent_class)->destroy (widget);
993 }
994
995 static void
gbp_spell_widget_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)996 gbp_spell_widget_get_property (GObject *object,
997 guint prop_id,
998 GValue *value,
999 GParamSpec *pspec)
1000 {
1001 GbpSpellWidget *self = GBP_SPELL_WIDGET (object);
1002
1003 switch (prop_id)
1004 {
1005 case PROP_EDITOR:
1006 g_value_set_object (value, gbp_spell_widget_get_editor (self));
1007 break;
1008
1009 default:
1010 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1011 }
1012 }
1013
1014 static void
gbp_spell_widget_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)1015 gbp_spell_widget_set_property (GObject *object,
1016 guint prop_id,
1017 const GValue *value,
1018 GParamSpec *pspec)
1019 {
1020 GbpSpellWidget *self = GBP_SPELL_WIDGET (object);
1021
1022 switch (prop_id)
1023 {
1024 case PROP_EDITOR:
1025 gbp_spell_widget_set_editor (self, g_value_get_object (value));
1026 break;
1027
1028 default:
1029 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1030 }
1031 }
1032
1033 static void
gbp_spell_widget_class_init(GbpSpellWidgetClass * klass)1034 gbp_spell_widget_class_init (GbpSpellWidgetClass *klass)
1035 {
1036 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1037 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1038
1039 object_class->constructed = gbp_spell_widget_constructed;
1040 object_class->get_property = gbp_spell_widget_get_property;
1041 object_class->set_property = gbp_spell_widget_set_property;
1042
1043 widget_class->destroy = gbp_spell_widget_destroy;
1044
1045 properties [PROP_EDITOR] =
1046 g_param_spec_object ("editor", NULL, NULL,
1047 IDE_TYPE_EDITOR_PAGE,
1048 (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
1049
1050 g_object_class_install_properties (object_class, N_PROPS, properties);
1051
1052 gtk_widget_class_set_template_from_resource (widget_class, "/plugins/spellcheck/gbp-spell-widget.ui");
1053
1054 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, word_label);
1055 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, count_label);
1056 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, word_entry);
1057 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, language_chooser_button);
1058 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, suggestions_box);
1059 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, dict_word_entry);
1060 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, dict_add_button);
1061 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, dict_words_list);
1062 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, count_box);
1063 gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, close_button);
1064
1065 g_type_ensure (GBP_TYPE_SPELL_LANGUAGE_POPOVER);
1066 }
1067
1068 static void
gbp_spell_widget_init(GbpSpellWidget * self)1069 gbp_spell_widget_init (GbpSpellWidget *self)
1070 {
1071 self->dict = gbp_spell_dict_new (NULL);
1072
1073 gtk_widget_init_template (GTK_WIDGET (self));
1074
1075 g_signal_connect_swapped (self->dict_words_list,
1076 "key-press-event",
1077 G_CALLBACK (dict_row_key_pressed_event_cb),
1078 self);
1079
1080 self->editor_page_addin_signals = dzl_signal_group_new (GBP_TYPE_SPELL_EDITOR_PAGE_ADDIN);
1081
1082 g_signal_connect_swapped (self->editor_page_addin_signals,
1083 "bind",
1084 G_CALLBACK (gbp_spell_widget_bind_addin),
1085 self);
1086
1087 g_signal_connect_swapped (self->editor_page_addin_signals,
1088 "unbind",
1089 G_CALLBACK (gbp_spell_widget_unbind_addin),
1090 self);
1091 }
1092
1093 /**
1094 * gbp_spell_widget_get_editor:
1095 * @self: a #GbpSpellWidget
1096 *
1097 * Gets the editor that is currently being spellchecked.
1098 *
1099 * Returns: (nullable) (transfer none): An #IdeEditorPage or %NULL
1100 *
1101 * Since: 3.26
1102 */
1103 IdeEditorPage *
gbp_spell_widget_get_editor(GbpSpellWidget * self)1104 gbp_spell_widget_get_editor (GbpSpellWidget *self)
1105 {
1106 g_return_val_if_fail (GBP_IS_SPELL_WIDGET (self), NULL);
1107
1108 return self->editor;
1109 }
1110
1111 void
gbp_spell_widget_set_editor(GbpSpellWidget * self,IdeEditorPage * editor)1112 gbp_spell_widget_set_editor (GbpSpellWidget *self,
1113 IdeEditorPage *editor)
1114 {
1115 GspellNavigator *navigator;
1116
1117 g_return_if_fail (GBP_IS_SPELL_WIDGET (self));
1118 g_return_if_fail (!editor || IDE_IS_EDITOR_PAGE (editor));
1119
1120 if (g_set_object (&self->editor, editor))
1121 {
1122 IdeEditorPageAddin *addin = NULL;
1123
1124 if (editor != NULL)
1125 {
1126 addin = ide_editor_page_addin_find_by_module_name (editor, "spellcheck");
1127 navigator = gbp_spell_editor_page_addin_get_navigator (GBP_SPELL_EDITOR_PAGE_ADDIN (addin));
1128 g_signal_connect_object (navigator,
1129 "notify::words-counted",
1130 G_CALLBACK (gbp_spell_widget__words_counted_cb),
1131 self,
1132 G_CONNECT_SWAPPED);
1133 }
1134
1135 dzl_signal_group_set_target (self->editor_page_addin_signals, addin);
1136
1137 g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDITOR]);
1138 }
1139 }
1140
1141 GtkWidget *
gbp_spell_widget_new(IdeEditorPage * editor)1142 gbp_spell_widget_new (IdeEditorPage *editor)
1143 {
1144 g_return_val_if_fail (!editor || IDE_IS_EDITOR_PAGE (editor), NULL);
1145
1146 return g_object_new (GBP_TYPE_SPELL_WIDGET,
1147 "editor", editor,
1148 NULL);
1149 }
1150
1151 void
_gbp_spell_widget_change(GbpSpellWidget * self,gboolean change_all)1152 _gbp_spell_widget_change (GbpSpellWidget *self,
1153 gboolean change_all)
1154 {
1155 g_autofree gchar *change_to = NULL;
1156 GspellNavigator *navigator;
1157 GspellChecker *checker;
1158 const gchar *word;
1159
1160 g_assert (GBP_IS_SPELL_WIDGET (self));
1161 g_assert (IDE_IS_EDITOR_PAGE (self->editor));
1162 g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self->editor_page_addin));
1163
1164 checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
1165 g_assert (GSPELL_IS_CHECKER (checker));
1166
1167 word = gtk_label_get_text (self->word_label);
1168 g_assert (!dzl_str_empty0 (word));
1169
1170 change_to = g_strdup (gtk_entry_get_text (self->word_entry));
1171 g_assert (!dzl_str_empty0 (change_to));
1172
1173 navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
1174 g_assert (navigator != NULL);
1175
1176 gspell_checker_set_correction (checker, word, -1, change_to, -1);
1177
1178 if (change_all)
1179 gspell_navigator_change_all (navigator, word, change_to);
1180 else
1181 gspell_navigator_change (navigator, word, change_to);
1182
1183 _gbp_spell_widget_move_next_word (self);
1184 }
1185
1186