Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
spellcheck.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
6/* Authors:
7 * bulia byak <bulia@users.sf.net>
8 * Jon A. Cruz <jon@joncruz.org>
9 * Abhishek Sharma
10 *
11 * Copyright (C) 2009 Authors
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 */
15
16#include "spellcheck.h"
17
18#include <glibmm/i18n.h>
19#include <gtkmm/dropdown.h>
20#include <gtkmm/columnview.h>
21#include <gtkmm/singleselection.h>
22#include <gtkmm/stringlist.h>
23
24#include "desktop.h"
25#include "document-undo.h"
26#include "document.h"
27#include "inkscape.h"
28#include "layer-manager.h"
29#include "selection.h"
30#include "selection-chemistry.h"
31#include "text-editing.h"
33#include "object/sp-defs.h"
34#include "object/sp-flowtext.h"
35#include "object/sp-object.h"
36#include "object/sp-root.h"
37#include "object/sp-string.h"
38#include "object/sp-text.h"
39#include "ui/builder-utils.h"
41#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK
42#include "ui/icon-names.h"
43#include "ui/tools/text-tool.h"
45
46namespace Inkscape::UI::Dialog {
47namespace {
48
49void show_spellcheck_preferences_dialog()
50{
51 Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK);
52 SP_ACTIVE_DESKTOP->getContainer()->new_dialog("Preferences");
53}
54
55// Returns a < b
56bool compare_bboxes(SPItem const *a, SPItem const *b)
57{
58 auto bbox1 = a->documentVisualBounds();
59 auto bbox2 = b->documentVisualBounds();
60 if (!bbox1 || !bbox2) {
61 return true;
62 }
63
64 // vector between top left corners
65 auto diff = bbox1->min() - bbox2->min();
66
67 return diff.y() == 0 ? diff.x() < 0 : diff.y() < 0;
68}
69
70} // namespace
71
73 : SpellCheck(UI::create_builder("dialog-spellcheck.ui"))
74{}
75
76// Note: Macro can be replaced using cpp2 metaclasses.
77#define BUILD(name) name{UI::get_widget<std::remove_reference_t<decltype(name)>>(builder, #name)}
78
79SpellCheck::SpellCheck(Glib::RefPtr<Gtk::Builder> const &builder)
80 : DialogBase("/dialogs/spellcheck/", "Spellcheck")
81 , _prefs{*Preferences::get()}
82 , BUILD(banner_label)
83 , BUILD(column_view)
84 , BUILD(accept_button)
85 , BUILD(ignoreonce_button)
86 , BUILD(ignore_button)
87 , BUILD(add_button)
88 , BUILD(pref_button)
89 , BUILD(dictionary_combo)
90 , BUILD(stop_button)
91 , BUILD(start_button)
92{
93 append(UI::get_widget<Gtk::Box>(builder, "main_box"));
94
95 _provider = spelling_provider_get_default();
97 [&](auto name, auto code) { _langs.push_back({.name = name, .code = code}); });
98
99 if (_langs.empty()) {
100 banner_label.set_markup(Glib::ustring::compose("<i>%1</i>", _("No dictionaries installed")));
101 }
102
103 corrections = Gtk::StringList::create();
104 selection_model = Gtk::SingleSelection::create(corrections);
105 column_view.set_model(selection_model);
106
107 if (!_langs.empty()) {
108 auto list = Gtk::StringList::create();
109 for (auto const &pair : _langs) {
110 list->append(pair.name);
111 }
112 dictionary_combo.set_model(list);
113
114 auto lookup_lang_code = [this] (Glib::ustring const &code) -> std::optional<int> {
115 auto it = std::find_if(begin(_langs), end(_langs), [&] (auto &pair) {
116 return pair.code == code.raw();
117 });
118 if (it == end(_langs)) {
119 return {};
120 }
121 return std::distance(begin(_langs), it);
122 };
123
124 // Set previously set language (or the first item)
125 dictionary_combo.set_selected(lookup_lang_code(_prefs.getString("/dialogs/spellcheck/lang")).value_or(0));
126 }
127
128 /*
129 * Signal handlers
130 */
131 accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept));
132 ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce));
133 ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore));
134 add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd));
135 start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart));
136 stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop));
137 selection_model->property_selected().signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange));
138 dictionary_combo.property_selected().signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged));
139 pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog));
140
141 column_view.set_sensitive(false);
142 accept_button.set_sensitive(false);
143 ignore_button.set_sensitive(false);
144 ignoreonce_button.set_sensitive(false);
145 add_button.set_sensitive(false);
146 stop_button.set_sensitive(false);
147}
148
149SpellCheck::~SpellCheck() = default;
150
152{
153 if (_working) {
154 // Stop and start on the new desktop
155 finished();
156 onStart();
157 }
158}
159
161{
162 _rects.clear();
163}
164
166{
167 _release_connection.disconnect();
168 _modified_connection.disconnect();
169}
170
171void SpellCheck::allTextItems(SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked)
172{
173 if (is<SPDefs>(r)) {
174 return; // we're not interested in items in defs
175 }
176
177 if (!std::strcmp(r->getRepr()->name(), "svg:metadata")) {
178 return; // we're not interested in metadata
179 }
180
181 if (auto desktop = getDesktop()) {
182 for (auto &child: r->children) {
183 if (auto item = cast<SPItem>(&child)) {
184 if (!child.cloned && !desktop->layerManager().isLayer(item)) {
185 if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
186 if (is<SPText>(item) || is<SPFlowtext>(item)) {
187 l.push_back(item);
188 }
189 }
190 }
191 }
192 allTextItems(&child, l, hidden, locked);
193 }
194 }
195}
196
198{
199 std::vector<SPItem *> l;
200 allTextItems(root, l, false, true);
201 return std::find(l.begin(), l.end(), text) != l.end();
202}
203
204// We regenerate and resort the list every time, because user could have changed it while the
205// dialog was waiting
207{
208 std::vector<SPItem *> l;
209 allTextItems(root, l, false, true);
210 std::sort(l.begin(), l.end(), compare_bboxes);
211
212 for (auto item : l) {
213 if (_seen_objects.insert(item).second) {
214 return item;
215 }
216 }
217
218 return nullptr;
219}
220
222{
223 disconnect();
224
226 if (_text) {
227 _modified_connection = _text->connectModified([this] (auto, auto) { onObjModified(); });
228 _release_connection = _text->connectRelease([this] (auto) { onObjReleased(); });
229
232 }
233
235 _word.clear();
236}
237
239{
240 _checker.reset();
241
242 auto i = dictionary_combo.get_selected();
243 if (i != GTK_INVALID_LIST_POSITION) {
244 _checker = GObjectPtr(spelling_checker_new(_provider, _langs.at(i).code.c_str()));
245 }
246
247 return !!_checker;
248}
249
251{
252 if (!getDocument())
253 return;
254
255 start_button.set_sensitive(false);
256
257 _stops = 0;
258 _adds = 0;
259 clearRects();
260
261 if (!updateSpeller())
262 return;
263
265
266 // empty the list of objects we've checked
267 _seen_objects.clear();
268
269 // grab first text
270 nextText();
271
272 _working = true;
273
274 doSpellcheck();
275}
276
278{
279 clearRects();
280 disconnect();
281
282 corrections->splice(0, corrections->get_n_items(), {});
283 column_view.set_sensitive(false);
284 accept_button.set_sensitive(false);
285 ignore_button.set_sensitive(false);
286 ignoreonce_button.set_sensitive(false);
287 add_button.set_sensitive(false);
288 stop_button.set_sensitive(false);
289 start_button.set_sensitive(true);
290
291 banner_label.set_markup(
292 _stops
293 ? Glib::ustring::compose(_("<b>Finished</b>, <b>%1</b> words added to dictionary"), _adds)
294 : _("<b>Finished</b>, nothing suspicious found")
295 );
296
297 _seen_objects.clear();
298
299 _root = nullptr;
300
301 _working = false;
302}
303
305{
306 auto desktop = getDesktop();
307 if (!_working || !desktop)
308 return false;
309
310 if (!_text) {
311 finished();
312 return false;
313 }
314 _word.clear();
315
316 while (_word.size() == 0) {
318
319 if (!_layout || _begin_w == _layout->end()) {
320 nextText();
321 return false;
322 }
323
326 }
327
331 }
332
333 // try to link this word with the next if separated by '
334 SPObject *char_item = nullptr;
335 Glib::ustring::iterator text_iter;
336 _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter);
337 if (is<SPString>(char_item)) {
338 int this_char = *text_iter;
339 if (this_char == '\'' || this_char == 0x2019) {
340 auto end_t = _end_w;
341 end_t.nextCharacter();
342 _layout->getSourceOfCharacter(end_t, &char_item, &text_iter);
343 if (is<SPString>(char_item)) {
344 int this_char = *text_iter;
345 if (g_ascii_isalpha(this_char)) { // 's
348 }
349 }
350 }
351 }
352
353 // skip words containing digits
354 if (_prefs.getInt(_prefs_path + "ignorenumbers") != 0) {
355 bool digits = false;
356 for (unsigned int i : _word) {
357 if (g_unichar_isdigit(i)) {
358 digits = true;
359 break;
360 }
361 }
362 if (digits) {
363 return false;
364 }
365 }
366
367 // skip ALL-CAPS words
368 if (_prefs.getInt(_prefs_path + "ignoreallcaps") != 0) {
369 bool allcaps = true;
370 for (unsigned int i : _word) {
371 if (!g_unichar_isupper(i)) {
372 allcaps = false;
373 break;
374 }
375 }
376 if (allcaps) {
377 return false;
378 }
379 }
380
381 bool found = false;
382
383 if (_checker) {
384 found = spelling_checker_check_word(_checker.get(), _word.c_str(), _word.length());
385 }
386
387 if (!found) {
388 _stops++;
389
390 // display it in window
391 banner_label.set_markup(Glib::ustring::compose(_("Not in dictionary: <b>%1</b>"), _word));
392
393 column_view.set_sensitive(true);
394 ignore_button.set_sensitive(true);
395 ignoreonce_button.set_sensitive(true);
396 add_button.set_sensitive(true);
397 stop_button.set_sensitive(true);
398
399 // draw rect
401 // We may not have a single quad if this is a clipped part of text on path;
402 // in that case skip drawing the rect
403 if (points.size() >= 4) {
404 // expand slightly
405 auto area = Geom::Rect::from_range(points.begin(), points.end());
406 double mindim = std::min(area.width(), area.height());
407 area.expandBy(std::max(0.05 * mindim, 1.0));
408
409 // Create canvas item rect with red stroke. (TODO: a quad could allow non-axis aligned rects.)
410 auto rect = new Inkscape::CanvasItemRect(desktop->getCanvasSketch(), area);
411 rect->set_stroke(0xff0000ff);
412 rect->set_visible(true);
413 _rects.emplace_back(rect);
414
415 // scroll to make it all visible
416 Geom::Point const center = desktop->current_center();
417 area.expandBy(0.5 * mindim);
418 Geom::Point scrollto;
419 double dist = 0;
420 for (unsigned corner = 0; corner < 4; corner ++) {
421 if (Geom::L2(area.corner(corner) - center) > dist) {
422 dist = Geom::L2(area.corner(corner) - center);
423 scrollto = area.corner(corner);
424 }
425 }
426 desktop->scroll_to_point(scrollto);
427 }
428
429 // select text; if in Text tool, position cursor to the beginning of word
430 // unless it is already in the word
431 if (desktop->getSelection()->singleItem() != _text) {
433 }
434
435 if (auto const text_tool = dynamic_cast<Tools::TextTool *>(desktop->getTool())) {
436 auto cursor = get_cursor_position(*text_tool, _text);
437 if (!cursor) { // some other text is selected there
439 } else if (*cursor <= _begin_w || *cursor >= _end_w) {
440 text_tool->placeCursor(_text, _begin_w);
441 }
442 }
443
444 // get corrections
445 auto new_corrections = list_corrections(_checker.get(), _word.c_str());
446 corrections->splice(0, corrections->get_n_items(), new_corrections);
447
448 // select first correction
449 if (!new_corrections.empty()) {
450 selection_model->property_selected().set_value(0);
451 }
452
453 accept_button.set_sensitive(!new_corrections.empty());
454
455 return true;
456 }
457
458 return false;
459}
460
462{
463 if (!_rects.empty()) {
464 _rects.pop_back();
465 }
466}
467
469{
470 if (_langs.empty()) {
471 return;
472 }
473
474 banner_label.set_markup(_("<i>Checking...</i>"));
475
476 while (_working)
477 if (nextWord())
478 break;
479}
480
482{
483 accept_button.set_sensitive(true);
484}
485
487{
488 if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it
489 _local_change = false;
490 return;
491 }
492
493 if (_working && _root) {
494 // user may have edited the text we're checking; try to do the most sensible thing in this
495 // situation
496
497 // just in case, re-get text's layout
499
500 // re-get the word
504 Glib::ustring word_new = sp_te_get_string_multiline(_text, _begin_w, _end_w);
505 if (word_new != _word) {
508 doSpellcheck(); // recheck this word and go ahead if it's ok
509 }
510 }
511}
512
514{
515 if (_working && _root) {
516 // the text object was deleted
518 nextText();
519 doSpellcheck(); // get next text and continue
520 }
521}
522
524{
525 // insert chosen correction
526
527 auto index = selection_model->get_selected();
528 if (index != GTK_INVALID_LIST_POSITION) {
529 auto corr = corrections->get_string(index);
530 if (!corr.empty()) {
531 _local_change = true;
532 sp_te_replace(_text, _begin_w, _end_w, corr.c_str());
533 // find the end of the word anew
536 DocumentUndo::done(getDocument(), _("Fix spelling"), INKSCAPE_ICON("draw-text"));
537 }
538 }
539
541 doSpellcheck();
542}
543
545{
546 if (_checker) {
547 spelling_checker_ignore_word(_checker.get(), _word.c_str());
548 }
549
551 doSpellcheck();
552}
553
559
561{
562 _adds++;
563
564 if (_checker) {
565 spelling_checker_add_word(_checker.get(), _word.c_str());
566 }
567
569 doSpellcheck();
570}
571
573{
574 finished();
575}
576
578{
579 // First, save language for next load
580 auto index = dictionary_combo.get_selected();
581 if (index == GTK_INVALID_LIST_POSITION) {
582 return;
583 }
584 _prefs.setString("/dialogs/spellcheck/lang", _langs.at(index).code);
585
586 if (!_working) {
587 onStart();
588 return;
589 }
590
591 if (!updateSpeller()) {
592 return;
593 }
594
595 // recheck current word
598 doSpellcheck();
599}
600
601} // namespace Inkscape::UI::Dialog
602
603/*
604 Local Variables:
605 mode:c++
606 c-file-style:"stroustrup"
607 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
608 indent-tabs-mode:nil
609 fill-column:99
610 End:
611*/
612// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Gtk builder utilities.
static CRect from_range(InputIterator start, InputIterator end)
Create a rectangle from a range of points.
Two-dimensional point that doubles as a vector.
Definition point.h:66
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
bool isLayer(SPObject *object) const
True if object is a layer.
SPItem * singleItem()
Returns a single selected item.
Preference storage class.
Definition preferences.h:66
Glib::ustring getString(Glib::ustring const &pref_path, Glib::ustring const &def="")
Retrieve an UTF-8 string.
static Preferences * get()
Access the singleton Preferences object.
int getInt(Glib::ustring const &pref_path, int def=0)
Retrieve an integer.
void setString(Glib::ustring const &pref_path, Glib::ustring const &value)
Set an UTF-8 string value.
void setInt(Glib::ustring const &pref_path, int value)
Set an integer value.
void set(XML::Node *repr)
Set the selection to an XML node's SPObject.
Definition selection.h:118
std::vector< Geom::Point > createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const
Basically uses characterBoundingBox() on all the characters from start to end and returns the union o...
void validateIterator(iterator *it) const
Checks the validity of the given iterator over the current layout.
iterator end() const
Returns an iterator pointing just past the end of the last glyph, which is also just past the end of ...
void getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator=nullptr) const
Discovers where the character pointed to by it came from, by retrieving the object that was passed to...
bool isStartOfWord(iterator const &it) const
Returns true if it points to a character which is a the start of a word, as defined by Pango.
iterator begin() const
Returns an iterator pointing at the first glyph of the flowed output.
DialogBase is the base class for the dialog system.
Definition dialog-base.h:42
SPDocument * getDocument() const
Definition dialog-base.h:83
Glib::ustring const _prefs_path
Definition dialog-base.h:88
SPDesktop * getDesktop() const
Definition dialog-base.h:79
A dialog widget to checking spelling of text elements in the document Uses gspell and one of the lang...
Definition spellcheck.h:55
void onIgnore()
Ignore button clicked.
sigc::scoped_connection _release_connection
Definition spellcheck.h:212
void onAccept()
Accept button clicked.
SPItem * getText(SPObject *root)
Compare the visual bounds of 2 SPItems referred to by a and b.
void onStop()
Stop button clicked.
Glib::ustring _word
the word we're checking
Definition spellcheck.h:195
Glib::RefPtr< Gtk::StringList > corrections
Definition spellcheck.h:239
std::vector< LanguagePair > _langs
Definition spellcheck.h:225
void onStart()
Start button clicked.
void disconnect()
Release handlers to the selected item.
int _stops
counters for the number of stops and dictionary adds
Definition spellcheck.h:200
void onLanguageChanged()
Language selection changed.
Text::Layout::iterator _begin_w
iterators for the start and end of the current word
Definition spellcheck.h:189
Util::GObjectPtr< SpellingChecker > _checker
Definition spellcheck.h:164
void onIgnoreOnce()
Ignore once button clicked.
void finished()
Cleanup after spellcheck is finished.
bool textIsValid(SPObject *root, SPItem *text)
Is text inside the SPOject's tree.
bool _working
true if we are in the middle of a check
Definition spellcheck.h:206
void onTreeSelectionChange()
Selection in suggestions text view changed.
void onAdd()
Add button clicked.
bool nextWord()
Find the next word to spell check.
void clearRects()
Remove the highlight rectangle form the canvas.
std::vector< CanvasItemPtr< CanvasItemRect > > _rects
list of canvasitems (currently just rects) that mark misspelled things on canvas
Definition spellcheck.h:169
SPItem * _text
the object currently being checked
Definition spellcheck.h:179
sigc::scoped_connection _modified_connection
connect to the object being checked in case it is modified or deleted by user
Definition spellcheck.h:211
bool _local_change
true if the spell checker dialog has changed text, to suppress modified callback
Definition spellcheck.h:217
void onObjModified()
Selected object modified on canvas.
Text::Layout const * _layout
current objects layout
Definition spellcheck.h:184
void onObjReleased()
Selected object removed from canvas.
void allTextItems(SPObject *r, std::vector< SPItem * > &l, bool hidden, bool locked)
Returns a list of all the text items in the SPObject.
Glib::RefPtr< Gtk::SingleSelection > selection_model
Definition spellcheck.h:240
Text::Layout::iterator _end_w
Definition spellcheck.h:190
std::set< SPItem * > _seen_objects
list of text objects we have already checked in this session
Definition spellcheck.h:174
bool updateSpeller()
Update speller from language combobox.
virtual char const * name() const =0
Get the name of the element node.
Geom::Point current_center() const
Definition desktop.cpp:660
bool itemIsHidden(SPItem const *item) const
Definition desktop.cpp:264
Inkscape::CanvasItemGroup * getCanvasSketch() const
Definition desktop.h:201
Inkscape::Selection * getSelection() const
Definition desktop.h:188
Inkscape::UI::Tools::ToolBase * getTool() const
Definition desktop.h:187
bool scroll_to_point(Geom::Point const &s_dt, double autoscrollspeed=0)
Scroll screen so as to keep point 'p' visible in window.
Definition desktop.cpp:894
Inkscape::LayerManager & layerManager()
Definition desktop.h:287
SPRoot * getRoot()
Returns our SPRoot.
Definition document.h:200
Base class for visual SVG elements.
Definition sp-item.h:109
Geom::Affine i2dt_affine() const
Returns the transformation from item to desktop coords.
Definition sp-item.cpp:1837
Geom::OptRect documentVisualBounds() const
Get item's visual bbox in document coordinate system.
Definition sp-item.cpp:1034
bool isLocked() const
Definition sp-item.cpp:218
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
sigc::connection connectRelease(sigc::slot< void(SPObject *)> slot)
Connects to the release request signal.
Definition sp-object.h:237
Inkscape::XML::Node * getRepr()
Returns the XML representation of tree.
sigc::connection connectModified(sigc::slot< void(SPObject *, unsigned int)> slot)
Connects to the modification notification signal.
Definition sp-object.h:705
ChildrenList children
Definition sp-object.h:907
RootCluster root
Editable view implementation.
A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned.
TODO: insert short description here.
Macro for icon names used in Inkscape.
SPItem * item
Inkscape Preferences dialog.
@ PREFS_PAGE_SPELLCHECK
C++ wrapping for libspelling C API.
SBasis L2(D2< SBasis > const &a, unsigned k)
Definition d2-sbasis.cpp:42
Dialog code.
Definition desktop.h:117
auto list_corrections(SpellingChecker *checker, char const *word)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
void list_language_names_and_codes(SpellingProvider *provider, F &&cb)
static void append(std::vector< T > &target, std::vector< T > &&source)
Ocnode * child[8]
Definition quantize.cpp:33
TODO: insert short description here.
SPRoot: SVG <svg> implementation.
TODO: insert short description here.
Spellcheck dialog.
int index
Glib::ustring sp_te_get_string_multiline(SPItem const *text)
Gets a text-only representation of the given text or flowroot object, replacing line break elements w...
Inkscape::Text::Layout const * te_get_layout(SPItem const *item)
Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8)
TextTool.
Glib::ustring name
Definition toolbars.cpp:55
Glib::RefPtr< Gtk::Builder > builder