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