spelling_highlighter.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // For license and copyright information please follow this link:
  5. // https://github.com/desktop-app/legal/blob/master/LEGAL
  6. //
  7. #include "spellcheck/spelling_highlighter.h"
  8. #include "spellcheck/spellcheck_value.h"
  9. #include "spellcheck/spellcheck_utils.h"
  10. #include "spellcheck/spelling_highlighter_helper.h"
  11. #include "ui/qt_weak_factory.h"
  12. #include "ui/widgets/menu/menu.h"
  13. #include "ui/text/text_entity.h"
  14. #include "ui/text/text_utilities.h"
  15. #include "ui/widgets/popup_menu.h"
  16. #include "ui/ui_utility.h"
  17. #include "base/qt/qt_common_adapters.h"
  18. #include "base/platform/base_platform_info.h"
  19. #include "base/event_filter.h"
  20. #include "styles/palette.h"
  21. namespace Spellchecker {
  22. namespace {
  23. constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
  24. const auto kUnspellcheckableTags = {
  25. &Ui::InputField::kTagCode,
  26. &Ui::InputField::kTagPre,
  27. &Ui::InputField::kTagUnderline
  28. };
  29. constexpr auto kColdSpellcheckingTimeout = crl::time(1000);
  30. constexpr auto kMaxDeadKeys = 1;
  31. constexpr auto kSkippableFlags = 0
  32. | TextParseLinks
  33. | TextParseMentions
  34. | TextParseHashtags
  35. | TextParseBotCommands;
  36. const auto kKeysToCheck = {
  37. Qt::Key_Up,
  38. Qt::Key_Down,
  39. Qt::Key_Left,
  40. Qt::Key_Right,
  41. Qt::Key_PageUp,
  42. Qt::Key_PageDown,
  43. Qt::Key_Home,
  44. Qt::Key_End,
  45. };
  46. inline int EndOfWord(const MisspelledWord &range) {
  47. return range.first + range.second;
  48. }
  49. inline bool IntersectsWordRanges(
  50. const MisspelledWord &range,
  51. int pos2,
  52. int len2) {
  53. const auto l1 = range.first;
  54. const auto r1 = EndOfWord(range) - 1;
  55. const auto l2 = pos2;
  56. const auto r2 = pos2 + len2 - 1;
  57. return !(l1 > r2 || l2 > r1);
  58. }
  59. inline bool IntersectsWordRanges(
  60. const MisspelledWord &range,
  61. const MisspelledWord &range2) {
  62. const auto l1 = range.first;
  63. const auto r1 = EndOfWord(range) - 1;
  64. const auto l2 = range2.first;
  65. const auto r2 = EndOfWord(range2) - 1;
  66. return !(l1 > r2 || l2 > r1);
  67. }
  68. inline bool IntersectsWordRanges(const EntityInText &e, int pos2, int len2) {
  69. return IntersectsWordRanges({ e.offset(), e.length() }, pos2, len2);
  70. }
  71. inline bool IsTagUnspellcheckable(const QString &tag) {
  72. if (tag.isEmpty()) {
  73. return false;
  74. }
  75. for (const auto &single : TextUtilities::SplitTags(tag)) {
  76. const auto isCommonFormatting = ranges::any_of(
  77. kUnspellcheckableTags,
  78. [&](const auto *t) { return (*t) == single; });
  79. if (isCommonFormatting) {
  80. return true;
  81. }
  82. if (Ui::InputField::IsValidMarkdownLink(single)) {
  83. return true;
  84. }
  85. if (TextUtilities::IsMentionLink(single)) {
  86. return true;
  87. }
  88. }
  89. return false;
  90. }
  91. inline auto FindEntities(const QString &text) {
  92. return TextUtilities::ParseEntities(text, kSkippableFlags).entities;
  93. }
  94. inline auto IntersectsAnyOfEntities(
  95. int pos,
  96. int len,
  97. EntitiesInText entities) {
  98. return !entities.empty() && ranges::any_of(entities, [&](const auto &e) {
  99. return IntersectsWordRanges(e, pos, len);
  100. });
  101. }
  102. inline QChar AddedSymbol(QStringView text, int position, int added) {
  103. if (added != 1 || position >= text.size()) {
  104. return QChar();
  105. }
  106. return text.at(position);
  107. }
  108. inline MisspelledWord CorrectAccentValues(
  109. const QString &oldText,
  110. const QString &newText) {
  111. auto diff = std::vector<int>();
  112. const auto sizeOfDiff = newText.size() - oldText.size();
  113. if (sizeOfDiff <= 0 || sizeOfDiff > kMaxDeadKeys) {
  114. return MisspelledWord();
  115. }
  116. for (auto i = 0; i < oldText.size(); i++) {
  117. if (oldText.at(i) != newText.at(i + diff.size())) {
  118. diff.push_back(i);
  119. if (diff.size() > kMaxDeadKeys) {
  120. return MisspelledWord();
  121. }
  122. }
  123. }
  124. if (diff.size() == 0) {
  125. return MisspelledWord(oldText.size(), sizeOfDiff);
  126. }
  127. return MisspelledWord(diff.front(), diff.size() > 1 ? diff.back() : 1);
  128. }
  129. inline MisspelledWord RangeFromCursorSelection(const QTextCursor &cursor) {
  130. const auto start = cursor.selectionStart();
  131. return MisspelledWord(start, cursor.selectionEnd() - start);
  132. }
  133. [[nodiscard]] bool SpellcheckSuggestEvent(not_null<QKeyEvent*> e) {
  134. const auto modifier = Platform::IsMac()
  135. ? Qt::MetaModifier
  136. : Qt::ControlModifier;
  137. return (e->key() == Qt::Key_Space) && e->modifiers().testFlag(modifier);
  138. }
  139. } // namespace
  140. SpellingHighlighter::SpellingHighlighter(
  141. not_null<Ui::InputField*> field,
  142. rpl::producer<bool> enabled,
  143. std::optional<CustomContextMenuItem> customContextMenuItem)
  144. : QSyntaxHighlighter(field->rawTextEdit()->document())
  145. , _cursor(QTextCursor(document()))
  146. , _coldSpellcheckingTimer([=] { checkChangedText(); })
  147. , _field(field)
  148. , _textEdit(field->rawTextEdit())
  149. , _customContextMenuItem(customContextMenuItem) {
  150. #ifdef Q_OS_WIN
  151. Platform::Spellchecker::Init();
  152. #endif // !Q_OS_WIN
  153. _cachedRanges = MisspelledWords();
  154. // Use the patched SpellCheckUnderline style.
  155. _misspelledFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
  156. style::PaletteChanged(
  157. ) | rpl::start_with_next([=] {
  158. updatePalette();
  159. rehighlight();
  160. }, _lifetime);
  161. updatePalette();
  162. _field->documentContentsChanges(
  163. ) | rpl::start_with_next([=](const auto &value) {
  164. const auto &[pos, removed, added] = value;
  165. contentsChange(pos, removed, added);
  166. }, _lifetime);
  167. _field->markdownTagApplies(
  168. ) | rpl::start_with_next([=](auto markdownTag) {
  169. if (!IsTagUnspellcheckable(markdownTag.tag)) {
  170. return;
  171. }
  172. _cachedRanges = ranges::views::all(
  173. _cachedRanges
  174. ) | ranges::views::filter([&](const auto &range) {
  175. return !IntersectsWordRanges(
  176. range,
  177. markdownTag.internalStart,
  178. markdownTag.internalLength);
  179. }) | ranges::to_vector;
  180. rehighlight();
  181. }, _lifetime);
  182. updateDocumentText();
  183. std::move(
  184. enabled
  185. ) | rpl::start_with_next([=](bool value) {
  186. setEnabled(value);
  187. if (_enabled) {
  188. _field->installEventFilter(this);
  189. _textEdit->installEventFilter(this);
  190. _textEdit->viewport()->installEventFilter(this);
  191. } else {
  192. _field->installEventFilter(this);
  193. _textEdit->removeEventFilter(this);
  194. _textEdit->viewport()->removeEventFilter(this);
  195. }
  196. }, _lifetime);
  197. Spellchecker::SupportedScriptsChanged(
  198. ) | rpl::start_with_next([=] {
  199. checkCurrentText();
  200. }, _lifetime);
  201. }
  202. void SpellingHighlighter::updatePalette() {
  203. _misspelledFormat.setUnderlineColor(st::spellUnderline->c);
  204. }
  205. void SpellingHighlighter::contentsChange(int pos, int removed, int added) {
  206. if (!_enabled) {
  207. return;
  208. }
  209. if (document()->isEmpty()) {
  210. updateDocumentText();
  211. _cachedRanges.clear();
  212. return;
  213. }
  214. {
  215. const auto oldText = documentText().mid(
  216. pos,
  217. documentText().indexOf(QChar::ParagraphSeparator, pos));
  218. updateDocumentText();
  219. const auto b = findBlock(pos);
  220. const auto bLen = (document()->blockCount() > 1)
  221. ? b.length()
  222. : b.text().size();
  223. // This is a workaround for typing accents.
  224. // For example, when the user press the dead key (e.g. ` or ´),
  225. // Qt sends wrong values. E.g. if a text length is 100,
  226. // then values will be 0, 100, 100.
  227. // This invokes to re-check the entire text.
  228. // The Mac's accent menu has a pretty similar behavior.
  229. if ((b.position() == pos) && (bLen == added)) {
  230. const auto newText = b.text();
  231. const auto diff = added - removed;
  232. // The plain text of the document cannot contain dead keys.
  233. if (!diff) {
  234. if (!oldText.compare(newText, Qt::CaseSensitive)) {
  235. const auto c = RangeFromCursorSelection(
  236. _textEdit->textCursor());
  237. // If the cursor has a selection for the entire text,
  238. // we probably just changed its formatting.
  239. // So if we find the unspellcheckable tag,
  240. // we can clear cached ranges of misspelled words.
  241. if (!c.first && c.second == bLen) {
  242. if (hasUnspellcheckableTag(pos, added)) {
  243. _cachedRanges.clear();
  244. rehighlight();
  245. } else {
  246. checkCurrentText();
  247. }
  248. }
  249. return;
  250. }
  251. } else if (diff > 0 && diff <= kMaxDeadKeys) {
  252. const auto [p, l] = CorrectAccentValues(oldText, newText);
  253. if (l) {
  254. pos = p + b.position();
  255. added = l;
  256. removed = 0;
  257. }
  258. }
  259. }
  260. }
  261. const auto shift = [&](auto chars) {
  262. ranges::for_each(_cachedRanges, [&](auto &range) {
  263. if (range.first >= pos + removed) {
  264. range.first += chars;
  265. }
  266. });
  267. };
  268. // Shift to the right all words after the cursor, when adding text.
  269. if (added > 0) {
  270. shift(added);
  271. }
  272. // Remove all words that are in the selection.
  273. // Remove the word that is under the cursor.
  274. const auto wordUnderPos = getWordUnderPosition(pos);
  275. // If the cursor is between spaces,
  276. // QTextCursor::WordUnderCursor highlights the word on the left
  277. // even if the word is not under the cursor.
  278. // Example: "super | test", where | is the cursor position.
  279. // In this example QTextCursor::WordUnderCursor will select "super".
  280. const auto isPosNotInWord = pos > EndOfWord(wordUnderPos);
  281. _cachedRanges = (
  282. _cachedRanges
  283. ) | ranges::views::filter([&](const auto &range) {
  284. const auto isIntersected = IntersectsWordRanges(range, wordUnderPos);
  285. if (isIntersected) {
  286. return isPosNotInWord;
  287. }
  288. return !(isIntersected
  289. || (removed > 0 && IntersectsWordRanges(range, pos, removed)));
  290. }) | ranges::to_vector;
  291. // Shift to the left all words after the cursor, when deleting text.
  292. if (removed > 0) {
  293. shift(-removed);
  294. }
  295. // Normally we should to invoke rehighlighting to immediately apply
  296. // shifting of ranges. But we don't have to do this because the call of
  297. // contentsChange() is performed before the application's call of
  298. // highlightBlock().
  299. _addedSymbols += added;
  300. _removedSymbols += removed;
  301. // The typing of text character by character should produce
  302. // the same _lastPosition, _addedSymbols and _removedSymbols values
  303. // as removing and pasting several characters at a time.
  304. if (!_lastPosition || (removed == 1)) {
  305. _lastPosition = pos;
  306. }
  307. const auto addedSymbol = AddedSymbol(documentText(), pos, added);
  308. if ((removed == 1) || addedSymbol.isLetterOrNumber()) {
  309. if (_coldSpellcheckingTimer.isActive()) {
  310. _coldSpellcheckingTimer.cancel();
  311. }
  312. _coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
  313. } else {
  314. // We forcefully increase the range of check
  315. // when inserting a non-char. This can help when the user inserts
  316. // a non-char in the middle of a word.
  317. if (!(addedSymbol.isNull()
  318. || addedSymbol.isSpace()
  319. || addedSymbol.isLetterOrNumber())) {
  320. _lastPosition--;
  321. _addedSymbols++;
  322. }
  323. if (_isLastKeyRepeat) {
  324. return;
  325. }
  326. checkChangedText();
  327. }
  328. }
  329. void SpellingHighlighter::checkChangedText() {
  330. const auto pos = _lastPosition;
  331. const auto added = _addedSymbols;
  332. const auto removed = _removedSymbols;
  333. _lastPosition = 0;
  334. _removedSymbols = 0;
  335. _addedSymbols = 0;
  336. if (_coldSpellcheckingTimer.isActive()) {
  337. _coldSpellcheckingTimer.cancel();
  338. }
  339. const auto wordUnderCursor = getWordUnderPosition(pos);
  340. // If the length of the word is 0, there is no sense in checking it.
  341. if (!wordUnderCursor.second) {
  342. return;
  343. }
  344. const auto wordInCacheIt = [=] {
  345. return ranges::find_if(_cachedRanges, [&](auto &&w) {
  346. return w.first >= wordUnderCursor.first;
  347. });
  348. };
  349. if (added > 0) {
  350. const auto lastWordNewSelection = getWordUnderPosition(pos + added);
  351. // This is the same word.
  352. if (wordUnderCursor == lastWordNewSelection) {
  353. checkSingleWord(wordUnderCursor);
  354. return;
  355. }
  356. const auto beginNewSelection = wordUnderCursor.first;
  357. const auto endNewSelection = EndOfWord(lastWordNewSelection);
  358. auto callback = [=](MisspelledWords &&r) {
  359. ranges::insert(_cachedRanges, wordInCacheIt(), std::move(r));
  360. };
  361. invokeCheckText(
  362. beginNewSelection,
  363. endNewSelection - beginNewSelection,
  364. std::move(callback));
  365. return;
  366. }
  367. if (removed > 0) {
  368. checkSingleWord(wordUnderCursor);
  369. }
  370. }
  371. MisspelledWords SpellingHighlighter::filterSkippableWords(
  372. MisspelledWords &ranges) {
  373. const auto text = documentText();
  374. if (text.isEmpty()) {
  375. return MisspelledWords();
  376. }
  377. return ranges | ranges::views::filter([&](const auto &range) {
  378. return !isSkippableWord(range);
  379. }) | ranges::to_vector;
  380. }
  381. bool SpellingHighlighter::isSkippableWord(const MisspelledWord &range) {
  382. return isSkippableWord(range.first, range.second);
  383. }
  384. bool SpellingHighlighter::isSkippableWord(int position, int length) {
  385. if (hasUnspellcheckableTag(position, length)) {
  386. return true;
  387. }
  388. const auto text = documentText();
  389. const auto ref = base::StringViewMid(text, position, length);
  390. if (ref.isNull()) {
  391. return true;
  392. }
  393. return IsWordSkippable(ref);
  394. }
  395. void SpellingHighlighter::checkCurrentText() {
  396. if (document()->isEmpty()) {
  397. _cachedRanges.clear();
  398. return;
  399. }
  400. invokeCheckText(0, size(), [&](MisspelledWords &&ranges) {
  401. _cachedRanges = std::move(ranges);
  402. });
  403. }
  404. void SpellingHighlighter::invokeCheckText(
  405. int textPosition,
  406. int textLength,
  407. Fn<void(MisspelledWords &&ranges)> callback) {
  408. if (!_enabled) {
  409. return;
  410. }
  411. const auto rangesOffset = textPosition;
  412. const auto text = partDocumentText(textPosition, textLength);
  413. const auto weak = Ui::MakeWeak(this);
  414. _countOfCheckingTextAsync++;
  415. crl::async([=,
  416. text = std::move(text),
  417. callback = std::move(callback)]() mutable {
  418. MisspelledWords misspelledWordRanges;
  419. Platform::Spellchecker::CheckSpellingText(
  420. text,
  421. &misspelledWordRanges);
  422. if (rangesOffset) {
  423. ranges::for_each(misspelledWordRanges, [&](auto &&range) {
  424. range.first += rangesOffset;
  425. });
  426. }
  427. crl::on_main(weak, [=,
  428. text = std::move(text),
  429. ranges = std::move(misspelledWordRanges),
  430. callback = std::move(callback)]() mutable {
  431. _countOfCheckingTextAsync--;
  432. // Checking a large part of text can take an unknown amount of
  433. // time. So we have to compare the text before and after async
  434. // work.
  435. // If the text has changed during async and we have more async,
  436. // we don't perform further refreshing of cache and underlines.
  437. // But if it was the last async, we should invoke a new one.
  438. if (compareDocumentText(text, textPosition, textLength)) {
  439. if (!_countOfCheckingTextAsync) {
  440. checkCurrentText();
  441. }
  442. return;
  443. }
  444. auto filtered = filterSkippableWords(ranges);
  445. // When we finish checking the text, the user can
  446. // supplement the last word and there may be a situation where
  447. // a part of the last word may not be underlined correctly.
  448. // Example:
  449. // 1. We insert a text with an incomplete last word.
  450. // "Time in a bottl".
  451. // 2. We don't wait for the check to be finished
  452. // and end the last word with the letter "e".
  453. // 3. invokeCheckText() will mark the last word "bottl" as
  454. // misspelled.
  455. // 4. checkSingleWord() will mark the "bottle" as correct and
  456. // leave it as it is.
  457. // 5. The first five letters of the "bottle" will be underlined
  458. // and the sixth will not be underlined.
  459. // We can fix it with a check of completeness of the last word.
  460. if (filtered.size()) {
  461. const auto lastWord = filtered.back();
  462. if (const auto endOfText = textPosition + textLength;
  463. EndOfWord(lastWord) == endOfText) {
  464. const auto word = getWordUnderPosition(endOfText);
  465. if (EndOfWord(word) != endOfText) {
  466. filtered.pop_back();
  467. checkSingleWord(word);
  468. }
  469. }
  470. }
  471. callback(std::move(filtered));
  472. for (const auto &b : blocksFromRange(textPosition, textLength)) {
  473. rehighlightBlock(b);
  474. }
  475. });
  476. });
  477. }
  478. void SpellingHighlighter::checkSingleWord(const MisspelledWord &singleWord) {
  479. const auto weak = Ui::MakeWeak(this);
  480. auto w = partDocumentText(singleWord.first, singleWord.second);
  481. if (isSkippableWord(singleWord)) {
  482. return;
  483. }
  484. crl::async([=,
  485. w = std::move(w),
  486. singleWord = std::move(singleWord)]() mutable {
  487. if (Platform::Spellchecker::CheckSpelling(std::move(w))) {
  488. return;
  489. }
  490. crl::on_main(weak, [=,
  491. singleWord = std::move(singleWord)]() mutable {
  492. const auto posOfWord = singleWord.first;
  493. ranges::insert(
  494. _cachedRanges,
  495. ranges::find_if(_cachedRanges, [&](auto &&w) {
  496. return w.first >= posOfWord;
  497. }),
  498. singleWord);
  499. rehighlightBlock(findBlock(posOfWord));
  500. });
  501. });
  502. }
  503. bool SpellingHighlighter::hasUnspellcheckableTag(int begin, int length) {
  504. // This method is called only in the context of separate words,
  505. // so it is not supposed that the word can be in more than one block.
  506. const auto block = findBlock(begin);
  507. length = std::min(block.position() + block.length() - begin, length);
  508. for (auto it = block.begin(); !(it.atEnd()); ++it) {
  509. const auto fragment = it.fragment();
  510. if (!fragment.isValid()) {
  511. continue;
  512. }
  513. const auto frPos = fragment.position();
  514. const auto frLen = fragment.length();
  515. if (!IntersectsWordRanges({ frPos, frLen }, begin, length)) {
  516. continue;
  517. }
  518. const auto format = fragment.charFormat();
  519. if (!format.hasProperty(kTagProperty)) {
  520. continue;
  521. }
  522. const auto tag = format.property(kTagProperty).toString();
  523. if (IsTagUnspellcheckable(tag)) {
  524. return true;
  525. }
  526. }
  527. return false;
  528. }
  529. MisspelledWord SpellingHighlighter::getWordUnderPosition(int position) {
  530. if (position < 0) {
  531. position = 0;
  532. }
  533. _cursor.setPosition(std::min(position, size()));
  534. _cursor.select(QTextCursor::WordUnderCursor);
  535. return RangeFromCursorSelection(_cursor);
  536. }
  537. void SpellingHighlighter::highlightBlock(const QString &text) {
  538. if (_cachedRanges.empty() || !_enabled || text.isEmpty()) {
  539. return;
  540. }
  541. const auto entities = FindEntities(text);
  542. const auto bPos = currentBlock().position();
  543. const auto bLen = currentBlock().length();
  544. ranges::for_each((
  545. _cachedRanges
  546. // Skip the all words outside the current block.
  547. ) | ranges::views::filter([&](const auto &range) {
  548. return IntersectsWordRanges(range, bPos, bLen);
  549. }), [&](const auto &range) {
  550. const auto posInBlock = range.first - bPos;
  551. if (IntersectsAnyOfEntities(posInBlock, range.second, entities)) {
  552. return;
  553. }
  554. setFormat(posInBlock, range.second, _misspelledFormat);
  555. });
  556. setCurrentBlockState(0);
  557. }
  558. bool SpellingHighlighter::eventFilter(QObject *o, QEvent *e) {
  559. if (!_enabled) {
  560. return false;
  561. } else if (o == _field) {
  562. if (e->type() == QEvent::KeyPress) {
  563. const auto k = static_cast<QKeyEvent*>(e);
  564. if (SpellcheckSuggestEvent(k)) {
  565. showSpellcheckerMenu();
  566. return true;
  567. }
  568. }
  569. return false;
  570. }
  571. if (e->type() == QEvent::ContextMenu) {
  572. const auto c = static_cast<QContextMenuEvent*>(e);
  573. const auto menu = _textEdit->createStandardContextMenu();
  574. if (!menu || !c) {
  575. return false;
  576. }
  577. // Copy of QContextMenuEvent.
  578. auto copyEvent = std::make_shared<QContextMenuEvent>(
  579. c->reason(),
  580. c->pos(),
  581. c->globalPos());
  582. auto showMenu = [=, copyEvent = std::move(copyEvent)] {
  583. _contextMenuCreated.fire({ menu, copyEvent });
  584. };
  585. addSpellcheckerActions(
  586. std::move(menu),
  587. _textEdit->cursorForPosition(c->pos()),
  588. std::move(showMenu),
  589. c->globalPos());
  590. return true;
  591. } else if (e->type() == QEvent::KeyPress) {
  592. const auto k = static_cast<QKeyEvent*>(e);
  593. if (ranges::contains(kKeysToCheck, k->key())) {
  594. if (_addedSymbols + _removedSymbols + _lastPosition) {
  595. checkCurrentText();
  596. }
  597. } else if ((o == _textEdit) && k->isAutoRepeat()) {
  598. _isLastKeyRepeat = true;
  599. }
  600. } else if (_isLastKeyRepeat && (o == _textEdit)) {
  601. if (e->type() == QEvent::FocusOut) {
  602. _isLastKeyRepeat = false;
  603. if (_addedSymbols + _removedSymbols + _lastPosition) {
  604. checkCurrentText();
  605. }
  606. } else if (e->type() == QEvent::KeyRelease) {
  607. const auto k = static_cast<QKeyEvent*>(e);
  608. if (!k->isAutoRepeat()) {
  609. _isLastKeyRepeat = false;
  610. _coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
  611. }
  612. }
  613. } else if ((o == _textEdit->viewport())
  614. && (e->type() == QEvent::MouseButtonPress)) {
  615. if (_addedSymbols + _removedSymbols + _lastPosition) {
  616. checkCurrentText();
  617. }
  618. }
  619. return false;
  620. }
  621. bool SpellingHighlighter::enabled() {
  622. return _enabled;
  623. }
  624. void SpellingHighlighter::setEnabled(bool enabled) {
  625. _enabled = enabled;
  626. if (_enabled) {
  627. updateDocumentText();
  628. checkCurrentText();
  629. } else {
  630. _cachedRanges.clear();
  631. rehighlight();
  632. }
  633. }
  634. QString SpellingHighlighter::documentText() {
  635. return _lastPlainText;
  636. }
  637. void SpellingHighlighter::updateDocumentText() {
  638. _lastPlainText = document()->toRawText();
  639. }
  640. QString SpellingHighlighter::partDocumentText(int pos, int length) {
  641. return _lastPlainText.mid(pos, length);
  642. }
  643. int SpellingHighlighter::size() {
  644. return document()->characterCount() - 1;
  645. }
  646. QTextBlock SpellingHighlighter::findBlock(int pos) {
  647. return document()->findBlock(pos);
  648. }
  649. std::vector<QTextBlock> SpellingHighlighter::blocksFromRange(
  650. int pos,
  651. int length) {
  652. auto b = findBlock(pos);
  653. auto blocks = std::vector<QTextBlock>{b};
  654. const auto end = pos + length;
  655. while (!b.contains(end) && (b != document()->end())) {
  656. if ((b = b.next()).isValid()) {
  657. blocks.push_back(b);
  658. }
  659. }
  660. return blocks;
  661. }
  662. int SpellingHighlighter::compareDocumentText(
  663. const QString &text,
  664. int textPos,
  665. int textLen) {
  666. if (_lastPlainText.size() < textPos + textLen) {
  667. return -1;
  668. }
  669. const auto p = base::StringViewMid(_lastPlainText, textPos, textLen);
  670. if (p.isNull()) {
  671. return -1;
  672. }
  673. return text.compare(p, Qt::CaseSensitive);
  674. }
  675. void SpellingHighlighter::addSpellcheckerActions(
  676. not_null<QMenu*> parentMenu,
  677. QTextCursor cursorForPosition,
  678. Fn<void()> showMenuCallback,
  679. QPoint mousePosition) {
  680. const auto menu = new QMenu(
  681. ph::lng_spellchecker_submenu(ph::now),
  682. parentMenu);
  683. auto addToParentAndShow = [=](int) {
  684. if (!menu->isEmpty()) {
  685. using namespace Spelling::Helper;
  686. if (IsContextMenuTop(parentMenu, mousePosition)) {
  687. parentMenu->addSeparator();
  688. parentMenu->addMenu(menu);
  689. } else {
  690. const auto first = parentMenu->actions().first();
  691. parentMenu->insertMenu(first, menu);
  692. parentMenu->insertSeparator(first);
  693. }
  694. }
  695. showMenuCallback();
  696. };
  697. fillSpellcheckerMenu(menu, cursorForPosition, addToParentAndShow);
  698. }
  699. void SpellingHighlighter::showSpellcheckerMenu() {
  700. auto menu = std::make_unique<QMenu>();
  701. const auto raw = menu.get();
  702. const auto cursor = _textEdit->textCursor();
  703. auto rect = _textEdit->cursorRect(cursor);
  704. rect.setTopLeft(_textEdit->viewport()->mapToGlobal(rect.topLeft()));
  705. auto show = [=, menu = std::move(menu)](int firstSuggestion) mutable {
  706. if (!menu->isEmpty()) {
  707. _menu = base::make_unique_q<Ui::PopupMenu>(
  708. _textEdit,
  709. menu.release(),
  710. _field->st().menu);
  711. _menu->setForcedVerticalOrigin(
  712. Ui::PopupMenu::VerticalOrigin::Top);
  713. base::install_event_filter(_menu.get(), [=](
  714. not_null<QEvent*> e) {
  715. if (e->type() == QEvent::KeyPress) {
  716. const auto k = static_cast<QKeyEvent*>(e.get());
  717. if (SpellcheckSuggestEvent(k)) {
  718. auto event = QKeyEvent(
  719. QEvent::KeyPress,
  720. Qt::Key_Down,
  721. Qt::KeyboardModifiers());
  722. _menu->menu()->handleKeyPress(&event);
  723. }
  724. }
  725. return base::EventFilterResult::Continue;
  726. });
  727. _menu->popup(rect.topLeft());
  728. if (_menu && firstSuggestion >= 0) {
  729. _menu->menu()->setSelected(firstSuggestion, false);
  730. }
  731. }
  732. };
  733. fillSpellcheckerMenu(raw, cursor, std::move(show));
  734. }
  735. void SpellingHighlighter::fillSpellcheckerMenu(
  736. not_null<QMenu*> menu,
  737. QTextCursor cursorForPosition,
  738. FnMut<void(int firstSuggestionIndex)> show) {
  739. const auto customItem = !Platform::Spellchecker::IsSystemSpellchecker()
  740. && _customContextMenuItem.has_value();
  741. cursorForPosition.select(QTextCursor::WordUnderCursor);
  742. // There is no reason to call async work if the word is skippable.
  743. const auto skippable = [&] {
  744. const auto &[p, l] = RangeFromCursorSelection(cursorForPosition);
  745. const auto e = FindEntities(findBlock(p).text());
  746. return (!l
  747. || isSkippableWord(p, l)
  748. || IntersectsAnyOfEntities(p, l, e));
  749. }();
  750. if (customItem) {
  751. menu->addAction(
  752. _customContextMenuItem->title,
  753. _customContextMenuItem->callback);
  754. }
  755. if (skippable) {
  756. show(-1);
  757. return;
  758. }
  759. const auto word = cursorForPosition.selectedText();
  760. auto fillMenu = [
  761. =,
  762. show = std::move(show),
  763. menu = std::move(menu)
  764. ](
  765. bool isCorrect,
  766. const auto &suggestions,
  767. const auto &newTextCursor) mutable {
  768. auto firstSuggestionIndex = -1;
  769. const auto guard = gsl::finally([&] {
  770. show(firstSuggestionIndex);
  771. });
  772. const auto addSeparator = [&] {
  773. if (!menu->isEmpty()) {
  774. menu->addSeparator();
  775. }
  776. };
  777. if (isCorrect) {
  778. if (Platform::Spellchecker::IsWordInDictionary(word)) {
  779. addSeparator();
  780. auto remove = [=] {
  781. Platform::Spellchecker::RemoveWord(word);
  782. checkCurrentText();
  783. };
  784. menu->addAction(
  785. ph::lng_spellchecker_remove(ph::now),
  786. std::move(remove));
  787. }
  788. return;
  789. }
  790. addSeparator();
  791. auto add = [=] {
  792. Platform::Spellchecker::AddWord(word);
  793. checkCurrentText();
  794. };
  795. menu->addAction(ph::lng_spellchecker_add(ph::now), std::move(add));
  796. auto ignore = [=] {
  797. Platform::Spellchecker::IgnoreWord(word);
  798. checkCurrentText();
  799. };
  800. menu->addAction(
  801. ph::lng_spellchecker_ignore(ph::now),
  802. std::move(ignore));
  803. if (suggestions.empty()) {
  804. return;
  805. }
  806. addSeparator();
  807. for (const auto &suggestion : suggestions) {
  808. if (firstSuggestionIndex < 0) {
  809. firstSuggestionIndex = menu->actions().size();
  810. }
  811. auto replaceWord = [=] {
  812. const auto oldTextCursor = _textEdit->textCursor();
  813. _textEdit->setTextCursor(newTextCursor);
  814. _textEdit->textCursor().insertText(suggestion);
  815. _textEdit->setTextCursor(oldTextCursor);
  816. };
  817. menu->addAction(suggestion, std::move(replaceWord));
  818. }
  819. };
  820. const auto weak = Ui::MakeWeak(this);
  821. crl::async([=,
  822. newTextCursor = std::move(cursorForPosition),
  823. fillMenu = std::move(fillMenu),
  824. word = std::move(word)
  825. ]() mutable {
  826. const auto isCorrect = Platform::Spellchecker::CheckSpelling(word);
  827. auto suggestions = std::vector<QString>();
  828. if (!isCorrect) {
  829. Platform::Spellchecker::FillSuggestionList(word, &suggestions);
  830. }
  831. crl::on_main(weak, [=,
  832. newTextCursor = std::move(newTextCursor),
  833. suggestions = std::move(suggestions),
  834. fillMenu = std::move(fillMenu)
  835. ]() mutable {
  836. fillMenu(
  837. isCorrect,
  838. std::move(suggestions),
  839. std::move(newTextCursor));
  840. });
  841. });
  842. }
  843. } // namespace Spellchecker