spellchecker_common.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "chat_helpers/spellchecker_common.h"
  8. #ifndef TDESKTOP_DISABLE_SPELLCHECK
  9. #include "base/platform/base_platform_info.h"
  10. #include "base/zlib_help.h"
  11. #include "data/data_session.h"
  12. #include "lang/lang_instance.h"
  13. #include "lang/lang_keys.h"
  14. #include "main/main_account.h"
  15. #include "main/main_domain.h"
  16. #include "main/main_session.h"
  17. #include "mainwidget.h"
  18. #include "spellcheck/platform/platform_spellcheck.h"
  19. #include "spellcheck/spellcheck_utils.h"
  20. #include "spellcheck/spellcheck_value.h"
  21. #include "core/application.h"
  22. #include "core/core_settings.h"
  23. #include <QtGui/QGuiApplication>
  24. #include <QtGui/QInputMethod>
  25. namespace Spellchecker {
  26. namespace {
  27. using namespace Storage::CloudBlob;
  28. constexpr auto kDictExtensions = { "dic", "aff" };
  29. constexpr auto kExceptions = {
  30. AppFile,
  31. "\xd0\xa2\xd0\xb5\xd0\xbb\xd0\xb5\xd0\xb3\xd1\x80\xd0\xb0\xd0\xbc"_cs,
  32. };
  33. constexpr auto kLangsForLWC = { QLocale::English, QLocale::Portuguese };
  34. constexpr auto kDefaultCountries = { QLocale::UnitedStates, QLocale::Brazil };
  35. // Language With Country.
  36. inline auto LWC(QLocale::Language language, QLocale::Country country) {
  37. if (ranges::contains(kDefaultCountries, country)) {
  38. return int(language);
  39. }
  40. return (language * 1000) + country;
  41. }
  42. inline auto LanguageFromLocale(QLocale loc) {
  43. const auto locLang = loc.language();
  44. return (ranges::contains(kLangsForLWC, locLang)
  45. && (loc.country() != QLocale::AnyCountry))
  46. ? LWC(locLang, loc.country())
  47. : int(locLang);
  48. }
  49. const auto kDictionaries = {
  50. Dict{{ QLocale::English, 649, 174'516, "English" }}, // en_US
  51. Dict{{ QLocale::Bulgarian, 594, 229'658, "\xd0\x91\xd1\x8a\xd0\xbb\xd0\xb3\xd0\xb0\xd1\x80\xd1\x81\xd0\xba\xd0\xb8" }}, // bg_BG
  52. Dict{{ QLocale::Catalan, 595, 417'611, "\x43\x61\x74\x61\x6c\xc3\xa0" }}, // ca_ES
  53. Dict{{ QLocale::Czech, 596, 860'286, "\xc4\x8c\x65\xc5\xa1\x74\x69\x6e\x61" }}, // cs_CZ
  54. Dict{{ QLocale::Welsh, 597, 177'305, "\x43\x79\x6d\x72\x61\x65\x67" }}, // cy_GB
  55. Dict{{ QLocale::Danish, 598, 345'874, "\x44\x61\x6e\x73\x6b" }}, // da_DK
  56. Dict{{ QLocale::German, 599, 2'412'780, "\x44\x65\x75\x74\x73\x63\x68" }}, // de_DE
  57. Dict{{ QLocale::Greek, 600, 1'389'160, "\xce\x95\xce\xbb\xce\xbb\xce\xb7\xce\xbd\xce\xb9\xce\xba\xce\xac" }}, // el_GR
  58. Dict{{ LWC(QLocale::English, QLocale::Australia), 601, 175'266, "English (Australia)" }}, // en_AU
  59. Dict{{ LWC(QLocale::English, QLocale::Canada), 602, 174'295, "English (Canada)" }}, // en_CA
  60. Dict{{ LWC(QLocale::English, QLocale::UnitedKingdom), 603, 174'433, "English (United Kingdom)" }}, // en_GB
  61. Dict{{ QLocale::Spanish, 604, 264'717, "\x45\x73\x70\x61\xc3\xb1\x6f\x6c" }}, // es_ES
  62. Dict{{ QLocale::Estonian, 605, 757'394, "\x45\x65\x73\x74\x69" }}, // et_EE
  63. Dict{{ QLocale::Persian, 606, 333'911, "\xd9\x81\xd8\xa7\xd8\xb1\xd8\xb3\xdb\x8c" }}, // fa_IR
  64. Dict{{ QLocale::French, 607, 321'391, "\x46\x72\x61\x6e\xc3\xa7\x61\x69\x73" }}, // fr_FR
  65. Dict{{ QLocale::Hebrew, 608, 622'550, "\xd7\xa2\xd7\x91\xd7\xa8\xd7\x99\xd7\xaa" }}, // he_IL
  66. Dict{{ QLocale::Hindi, 609, 56'105, "\xe0\xa4\xb9\xe0\xa4\xbf\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x80" }}, // hi_IN
  67. Dict{{ QLocale::Croatian, 610, 668'876, "\x48\x72\x76\x61\x74\x73\x6b\x69" }}, // hr_HR
  68. Dict{{ QLocale::Hungarian, 611, 660'402, "\x4d\x61\x67\x79\x61\x72" }}, // hu_HU
  69. Dict{{ QLocale::Armenian, 612, 928'746, "\xd5\x80\xd5\xa1\xd5\xb5\xd5\xa5\xd6\x80\xd5\xa5\xd5\xb6" }}, // hy_AM
  70. Dict{{ QLocale::Indonesian, 613, 100'134, "\x49\x6e\x64\x6f\x6e\x65\x73\x69\x61" }}, // id_ID
  71. Dict{{ QLocale::Italian, 614, 324'613, "\x49\x74\x61\x6c\x69\x61\x6e\x6f" }}, // it_IT
  72. Dict{{ QLocale::Korean, 615, 1'256'987, "\xed\x95\x9c\xea\xb5\xad\xec\x96\xb4" }}, // ko_KR
  73. Dict{{ QLocale::Lithuanian, 616, 267'427, "\x4c\x69\x65\x74\x75\x76\x69\xc5\xb3" }}, // lt_LT
  74. Dict{{ QLocale::Latvian, 617, 641'602, "\x4c\x61\x74\x76\x69\x65\xc5\xa1\x75" }}, // lv_LV
  75. Dict{{ QLocale::NorwegianBokmal, 618, 588'650, "\x4e\x6f\x72\x73\x6b" }}, // nb_NO
  76. Dict{{ QLocale::Dutch, 619, 743'406, "\x4e\x65\x64\x65\x72\x6c\x61\x6e\x64\x73" }}, // nl_NL
  77. Dict{{ QLocale::Polish, 620, 1'015'747, "\x50\x6f\x6c\x73\x6b\x69" }}, // pl_PL
  78. Dict{{ QLocale::Portuguese, 621, 1'231'999, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73 (Brazil)" }}, // pt_BR
  79. Dict{{ LWC(QLocale::Portuguese, QLocale::Portugal), 622, 138'571, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73" }}, // pt_PT
  80. Dict{{ QLocale::Romanian, 623, 455'643, "\x52\x6f\x6d\xc3\xa2\x6e\xc4\x83" }}, // ro_RO
  81. Dict{{ QLocale::Russian, 624, 463'194, "\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xb8\xd0\xb9" }}, // ru_RU
  82. Dict{{ QLocale::Slovak, 625, 525'328, "\x53\x6c\x6f\x76\x65\x6e\xc4\x8d\x69\x6e\x61" }}, // sk_SK
  83. Dict{{ QLocale::Slovenian, 626, 1'143'710, "\x53\x6c\x6f\x76\x65\x6e\xc5\xa1\xc4\x8d\x69\x6e\x61" }}, // sl_SI
  84. Dict{{ QLocale::Albanian, 627, 583'412, "\x53\x68\x71\x69\x70" }}, // sq_AL
  85. Dict{{ QLocale::Swedish, 628, 593'877, "\x53\x76\x65\x6e\x73\x6b\x61" }}, // sv_SE
  86. Dict{{ QLocale::Tamil, 629, 323'193, "\xe0\xae\xa4\xe0\xae\xae\xe0\xae\xbf\xe0\xae\xb4\xe0\xaf\x8d" }}, // ta_IN
  87. Dict{{ QLocale::Tajik, 630, 369'931, "\xd0\xa2\xd0\xbe\xd2\xb7\xd0\xb8\xd0\xba\xd3\xa3" }}, // tg_TG
  88. Dict{{ QLocale::Turkish, 631, 4'301'099, "\x54\xc3\xbc\x72\x6b\xc3\xa7\x65" }}, // tr_TR
  89. Dict{{ QLocale::Ukrainian, 632, 445'711, "\xd0\xa3\xd0\xba\xd1\x80\xd0\xb0\xd1\x97\xd0\xbd\xd1\x81\xd1\x8c\xd0\xba\xd0\xb0" }}, // uk_UA
  90. Dict{{ QLocale::Vietnamese, 633, 12'949, "\x54\x69\xe1\xba\xbf\x6e\x67\x20\x56\x69\xe1\xbb\x87\x74" }}, // vi_VN
  91. // The Tajik code is 'tg_TG' in Chromium, but QT has only 'tg_TJ'.
  92. };
  93. inline auto IsSupportedLang(int lang) {
  94. return ranges::contains(kDictionaries, lang, &Dict::id);
  95. }
  96. void EnsurePath() {
  97. if (!QDir::current().mkpath(Spellchecker::DictionariesPath())) {
  98. LOG(("App Error: Could not create dictionaries path."));
  99. }
  100. }
  101. bool IsGoodPartName(const QString &name) {
  102. return ranges::any_of(kDictExtensions, [&](const auto &ext) {
  103. return name.endsWith(ext);
  104. });
  105. }
  106. using DictLoaderPtr = std::shared_ptr<base::unique_qptr<DictLoader>>;
  107. DictLoaderPtr BackgroundLoader;
  108. rpl::event_stream<int> BackgroundLoaderChanged;
  109. void SetBackgroundLoader(DictLoaderPtr loader) {
  110. BackgroundLoader = std::move(loader);
  111. }
  112. void DownloadDictionaryInBackground(
  113. not_null<Main::Session*> session,
  114. int counter,
  115. std::vector<int> langs) {
  116. if (counter >= langs.size()) {
  117. return;
  118. }
  119. const auto id = langs[counter];
  120. counter++;
  121. const auto destroyer = [=] {
  122. BackgroundLoader = nullptr;
  123. BackgroundLoaderChanged.fire(0);
  124. if (DictionaryExists(id)) {
  125. auto dicts = Core::App().settings().dictionariesEnabled();
  126. if (!ranges::contains(dicts, id)) {
  127. dicts.push_back(id);
  128. Core::App().settings().setDictionariesEnabled(std::move(dicts));
  129. Core::App().saveSettingsDelayed();
  130. }
  131. }
  132. DownloadDictionaryInBackground(session, counter, langs);
  133. };
  134. if (DictionaryExists(id)) {
  135. destroyer();
  136. return;
  137. }
  138. auto sharedLoader = std::make_shared<base::unique_qptr<DictLoader>>();
  139. *sharedLoader = base::make_unique_q<DictLoader>(
  140. QCoreApplication::instance(),
  141. session,
  142. id,
  143. GetDownloadLocation(id),
  144. DictPathByLangId(id),
  145. GetDownloadSize(id),
  146. crl::guard(session, destroyer));
  147. SetBackgroundLoader(std::move(sharedLoader));
  148. BackgroundLoaderChanged.fire_copy(id);
  149. }
  150. void AddExceptions() {
  151. const auto exceptions = ranges::views::all(
  152. kExceptions
  153. ) | ranges::views::transform([](const auto &word) {
  154. return word.utf16();
  155. }) | ranges::views::filter([](const auto &word) {
  156. return !(Platform::Spellchecker::IsWordInDictionary(word)
  157. || Spellchecker::IsWordSkippable(word));
  158. }) | ranges::to_vector;
  159. ranges::for_each(exceptions, Platform::Spellchecker::AddWord);
  160. }
  161. } // namespace
  162. DictLoaderPtr GlobalLoader() {
  163. return BackgroundLoader;
  164. }
  165. rpl::producer<int> GlobalLoaderChanged() {
  166. return BackgroundLoaderChanged.events();
  167. }
  168. DictLoader::DictLoader(
  169. QObject *parent,
  170. not_null<Main::Session*> session,
  171. int id,
  172. MTP::DedicatedLoader::Location location,
  173. const QString &folder,
  174. int64 size,
  175. Fn<void()> destroyCallback)
  176. : BlobLoader(parent, session, id, location, folder, size)
  177. , _destroyCallback(std::move(destroyCallback)) {
  178. }
  179. void DictLoader::unpack(const QString &path) {
  180. crl::async([=] {
  181. const auto success = Spellchecker::UnpackDictionary(path, id());
  182. if (success) {
  183. QFile(path).remove();
  184. destroy();
  185. return;
  186. }
  187. crl::on_main([=] { fail(); });
  188. });
  189. }
  190. void DictLoader::destroy() {
  191. Expects(_destroyCallback);
  192. crl::on_main(_destroyCallback);
  193. }
  194. void DictLoader::fail() {
  195. BlobLoader::fail();
  196. destroy();
  197. }
  198. std::vector<Dict> Dictionaries() {
  199. return kDictionaries | ranges::to_vector;
  200. }
  201. int64 GetDownloadSize(int id) {
  202. return ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size;
  203. }
  204. MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
  205. const auto username = kCloudLocationUsername.utf16();
  206. const auto i = ranges::find(kDictionaries, id, &Spellchecker::Dict::id);
  207. return MTP::DedicatedLoader::Location{ username, i->postId };
  208. }
  209. QString DictPathByLangId(int langId) {
  210. EnsurePath();
  211. return u"%1/%2"_q.arg(
  212. DictionariesPath(),
  213. Spellchecker::LocaleFromLangId(langId).name());
  214. }
  215. QString DictionariesPath() {
  216. return cWorkingDir() + u"tdata/dictionaries"_q;
  217. }
  218. bool UnpackDictionary(const QString &path, int langId) {
  219. const auto folder = DictPathByLangId(langId);
  220. return UnpackBlob(path, folder, IsGoodPartName);
  221. }
  222. bool DictionaryExists(int langId) {
  223. if (!langId) {
  224. return true;
  225. }
  226. const auto folder = DictPathByLangId(langId) + '/';
  227. return ranges::none_of(kDictExtensions, [&](const auto &ext) {
  228. const auto name = Spellchecker::LocaleFromLangId(langId).name();
  229. return !QFile(folder + name + '.' + ext).exists();
  230. });
  231. }
  232. bool RemoveDictionary(int langId) {
  233. if (!langId) {
  234. return true;
  235. }
  236. const auto fileName = Spellchecker::LocaleFromLangId(langId).name();
  237. const auto folder = u"%1/%2/"_q.arg(
  238. DictionariesPath(),
  239. fileName);
  240. return QDir(folder).removeRecursively();
  241. }
  242. bool WriteDefaultDictionary() {
  243. // This is an unused function.
  244. const auto en = QLocale::English;
  245. if (DictionaryExists(en)) {
  246. return false;
  247. }
  248. const auto fileName = QLocale(en).name();
  249. const auto folder = u"%1/%2/"_q.arg(
  250. DictionariesPath(),
  251. fileName);
  252. QDir(folder).removeRecursively();
  253. const auto path = folder + fileName;
  254. QDir().mkpath(folder);
  255. auto input = QFile(u":/misc/en_US_dictionary"_q);
  256. auto output = QFile(path);
  257. if (input.open(QIODevice::ReadOnly)
  258. && output.open(QIODevice::WriteOnly)) {
  259. output.write(input.readAll());
  260. const auto result = Spellchecker::UnpackDictionary(path, en);
  261. output.remove();
  262. return result;
  263. }
  264. return false;
  265. }
  266. rpl::producer<QString> ButtonManageDictsState(
  267. not_null<Main::Session*> session) {
  268. if (Platform::Spellchecker::IsSystemSpellchecker()) {
  269. return rpl::single(QString());
  270. }
  271. const auto computeString = [=] {
  272. if (!Core::App().settings().spellcheckerEnabled()) {
  273. return QString();
  274. }
  275. if (!Core::App().settings().dictionariesEnabled().size()) {
  276. return QString();
  277. }
  278. const auto dicts = Core::App().settings().dictionariesEnabled();
  279. const auto filtered = ranges::views::all(
  280. dicts
  281. ) | ranges::views::filter(
  282. DictionaryExists
  283. ) | ranges::to_vector;
  284. const auto active = Platform::Spellchecker::ActiveLanguages();
  285. return (active.size() == filtered.size())
  286. ? QString::number(filtered.size())
  287. : tr::lng_contacts_loading(tr::now);
  288. };
  289. return rpl::single(
  290. computeString()
  291. ) | rpl::then(
  292. rpl::merge(
  293. Spellchecker::SupportedScriptsChanged(),
  294. Core::App().settings().dictionariesEnabledChanges(
  295. ) | rpl::to_empty,
  296. Core::App().settings().spellcheckerEnabledChanges(
  297. ) | rpl::to_empty
  298. ) | rpl::map(computeString)
  299. );
  300. }
  301. std::vector<int> DefaultLanguages() {
  302. std::vector<int> langs;
  303. const auto append = [&](const auto loc) {
  304. const auto l = LanguageFromLocale(loc);
  305. if (!ranges::contains(langs, l) && IsSupportedLang(l)) {
  306. langs.push_back(l);
  307. }
  308. };
  309. const auto method = QGuiApplication::inputMethod();
  310. langs.reserve(method ? 3 : 2);
  311. if (method) {
  312. append(method->locale());
  313. }
  314. append(QLocale(Platform::SystemLanguage()));
  315. append(QLocale(Lang::LanguageIdOrDefault(Lang::Id())));
  316. return langs;
  317. }
  318. void Start(not_null<Main::Session*> session) {
  319. Spellchecker::SetPhrases({ {
  320. { &ph::lng_spellchecker_submenu, tr::lng_spellchecker_submenu() },
  321. { &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },
  322. { &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },
  323. { &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },
  324. } });
  325. const auto settings = &Core::App().settings();
  326. auto &lifetime = session->lifetime();
  327. const auto onEnabled = [=](auto enabled) {
  328. Platform::Spellchecker::UpdateLanguages(
  329. enabled
  330. ? settings->dictionariesEnabled()
  331. : std::vector<int>());
  332. };
  333. const auto guard = gsl::finally([=] {
  334. onEnabled(settings->spellcheckerEnabled());
  335. });
  336. if (Platform::Spellchecker::IsSystemSpellchecker()) {
  337. Spellchecker::SupportedScriptsChanged()
  338. | rpl::take(1)
  339. | rpl::start_with_next(AddExceptions, lifetime);
  340. return;
  341. }
  342. Spellchecker::SupportedScriptsChanged(
  343. ) | rpl::start_with_next(AddExceptions, lifetime);
  344. Spellchecker::SetWorkingDirPath(DictionariesPath());
  345. settings->dictionariesEnabledChanges(
  346. ) | rpl::start_with_next([](auto dictionaries) {
  347. Platform::Spellchecker::UpdateLanguages(dictionaries);
  348. }, lifetime);
  349. settings->spellcheckerEnabledChanges(
  350. ) | rpl::start_with_next(onEnabled, lifetime);
  351. const auto method = QGuiApplication::inputMethod();
  352. const auto connectInput = [=] {
  353. if (!method || !settings->spellcheckerEnabled()) {
  354. return;
  355. }
  356. auto callback = [=] {
  357. if (BackgroundLoader) {
  358. return;
  359. }
  360. const auto l = LanguageFromLocale(method->locale());
  361. if (!IsSupportedLang(l) || DictionaryExists(l)) {
  362. return;
  363. }
  364. crl::on_main(session, [=] {
  365. DownloadDictionaryInBackground(session, 0, { l });
  366. });
  367. };
  368. QObject::connect(
  369. method,
  370. &QInputMethod::localeChanged,
  371. std::move(callback));
  372. };
  373. if (settings->autoDownloadDictionaries()) {
  374. session->data().contactsLoaded().changes(
  375. ) | rpl::start_with_next([=](bool loaded) {
  376. if (!loaded) {
  377. return;
  378. }
  379. DownloadDictionaryInBackground(session, 0, DefaultLanguages());
  380. }, lifetime);
  381. connectInput();
  382. }
  383. const auto disconnect = [=] {
  384. QObject::disconnect(
  385. method,
  386. &QInputMethod::localeChanged,
  387. nullptr,
  388. nullptr);
  389. };
  390. lifetime.add([=] {
  391. disconnect();
  392. for (auto &[index, account] : session->domain().accounts()) {
  393. if (const auto anotherSession = account->maybeSession()) {
  394. if (anotherSession->uniqueId() != session->uniqueId()) {
  395. Spellchecker::Start(anotherSession);
  396. return;
  397. }
  398. }
  399. }
  400. });
  401. rpl::combine(
  402. settings->spellcheckerEnabledValue(),
  403. settings->autoDownloadDictionariesValue()
  404. ) | rpl::start_with_next([=](bool spell, bool download) {
  405. if (spell && download) {
  406. connectInput();
  407. return;
  408. }
  409. disconnect();
  410. }, lifetime);
  411. }
  412. } // namespace Spellchecker
  413. #endif // !TDESKTOP_DISABLE_SPELLCHECK