dictionaries_manager.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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 "boxes/dictionaries_manager.h"
  8. #ifndef TDESKTOP_DISABLE_SPELLCHECK
  9. #include "base/event_filter.h"
  10. #include "chat_helpers/spellchecker_common.h"
  11. #include "core/application.h"
  12. #include "core/core_settings.h"
  13. #include "lang/lang_keys.h"
  14. #include "main/main_account.h"
  15. #include "main/main_session.h"
  16. #include "mainwidget.h"
  17. #include "mtproto/dedicated_file_loader.h"
  18. #include "spellcheck/spellcheck_utils.h"
  19. #include "ui/wrap/vertical_layout.h"
  20. #include "ui/widgets/buttons.h"
  21. #include "ui/widgets/labels.h"
  22. #include "ui/widgets/multi_select.h"
  23. #include "ui/widgets/popup_menu.h"
  24. #include "ui/wrap/slide_wrap.h"
  25. #include "ui/effects/animations.h"
  26. #include "styles/style_layers.h"
  27. #include "styles/style_settings.h"
  28. #include "styles/style_boxes.h"
  29. #include "styles/style_menu_icons.h"
  30. namespace Ui {
  31. namespace {
  32. using Dictionaries = std::vector<int>;
  33. using namespace Storage::CloudBlob;
  34. using Loading = MTP::DedicatedLoader::Progress;
  35. using DictState = BlobState;
  36. using QueryCallback = Fn<void(const QString &)>;
  37. constexpr auto kMaxQueryLength = 15;
  38. class Inner : public Ui::RpWidget {
  39. public:
  40. Inner(
  41. QWidget *parent,
  42. not_null<Main::Session*> session,
  43. Dictionaries enabledDictionaries);
  44. Dictionaries enabledRows() const;
  45. QueryCallback queryCallback() const;
  46. private:
  47. void setupContent(
  48. not_null<Main::Session*> session,
  49. Dictionaries enabledDictionaries);
  50. Dictionaries _enabledRows;
  51. QueryCallback _queryCallback;
  52. };
  53. inline auto DictExists(int langId) {
  54. return Spellchecker::DictionaryExists(langId);
  55. }
  56. inline auto FilterEnabledDict(Dictionaries dicts) {
  57. return dicts | ranges::views::filter(
  58. DictExists
  59. ) | ranges::to_vector;
  60. }
  61. DictState ComputeState(int id, bool enabled) {
  62. const auto result = enabled ? DictState(Active()) : DictState(Ready());
  63. if (DictExists(id)) {
  64. return result;
  65. }
  66. return Available{ Spellchecker::GetDownloadSize(id) };
  67. }
  68. QString StateDescription(const DictState &state) {
  69. return StateDescription(
  70. state,
  71. tr::lng_settings_manage_enabled_dictionary);
  72. }
  73. auto CreateMultiSelect(QWidget *parent) {
  74. const auto result = Ui::CreateChild<Ui::MultiSelect>(
  75. parent,
  76. st::defaultMultiSelect,
  77. tr::lng_participant_filter());
  78. result->resizeToWidth(st::boxWidth);
  79. result->moveToLeft(0, 0);
  80. return result;
  81. }
  82. Inner::Inner(
  83. QWidget *parent,
  84. not_null<Main::Session*> session,
  85. Dictionaries enabledDictionaries)
  86. : RpWidget(parent) {
  87. setupContent(session, std::move(enabledDictionaries));
  88. }
  89. QueryCallback Inner::queryCallback() const {
  90. return _queryCallback;
  91. }
  92. Dictionaries Inner::enabledRows() const {
  93. return _enabledRows;
  94. }
  95. auto AddButtonWithLoader(
  96. not_null<Ui::VerticalLayout*> content,
  97. not_null<Main::Session*> session,
  98. const Spellchecker::Dict &dict,
  99. bool buttonEnabled,
  100. rpl::producer<QStringView> query) {
  101. const auto id = dict.id;
  102. buttonEnabled &= DictExists(id);
  103. const auto locale = Spellchecker::LocaleFromLangId(id);
  104. const std::vector<QString> indexList = {
  105. dict.name,
  106. QLocale::languageToString(locale.language()),
  107. QLocale::countryToString(locale.country())
  108. };
  109. const auto wrap = content->add(
  110. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  111. content,
  112. object_ptr<Ui::SettingsButton>(
  113. content,
  114. rpl::single(dict.name),
  115. st::dictionariesSectionButton
  116. )
  117. )
  118. );
  119. const auto button = wrap->entity();
  120. std::move(
  121. query
  122. ) | rpl::start_with_next([=](auto string) {
  123. wrap->toggle(
  124. ranges::any_of(indexList, [&](const QString &s) {
  125. return s.startsWith(string, Qt::CaseInsensitive);
  126. }),
  127. anim::type::instant);
  128. }, button->lifetime());
  129. using Loader = Spellchecker::DictLoader;
  130. using GlobalLoaderPtr = std::shared_ptr<base::unique_qptr<Loader>>;
  131. const auto localLoader = button->lifetime()
  132. .make_state<base::unique_qptr<Loader>>();
  133. const auto localLoaderValues = button->lifetime()
  134. .make_state<rpl::event_stream<Loader*>>();
  135. const auto setLocalLoader = [=](base::unique_qptr<Loader> loader) {
  136. *localLoader = std::move(loader);
  137. localLoaderValues->fire(localLoader->get());
  138. };
  139. const auto destroyLocalLoader = [=] {
  140. setLocalLoader(nullptr);
  141. };
  142. const auto buttonState = button->lifetime()
  143. .make_state<rpl::variable<DictState>>();
  144. const auto dictionaryRemoved = button->lifetime()
  145. .make_state<rpl::event_stream<>>();
  146. const auto dictionaryFromGlobalLoader = button->lifetime()
  147. .make_state<rpl::event_stream<>>();
  148. const auto globalLoader = button->lifetime()
  149. .make_state<GlobalLoaderPtr>();
  150. const auto rawGlobalLoaderPtr = [=]() -> Loader* {
  151. if (!globalLoader || !*globalLoader || !*globalLoader->get()) {
  152. return nullptr;
  153. }
  154. return globalLoader->get()->get();
  155. };
  156. const auto setGlobalLoaderPtr = [=](GlobalLoaderPtr loader) {
  157. if (localLoader->get()) {
  158. if (loader && loader->get()) {
  159. loader->get()->destroy();
  160. }
  161. return;
  162. }
  163. *globalLoader = std::move(loader);
  164. localLoaderValues->fire(rawGlobalLoaderPtr());
  165. if (rawGlobalLoaderPtr()) {
  166. dictionaryFromGlobalLoader->fire({});
  167. }
  168. };
  169. Spellchecker::GlobalLoaderChanged(
  170. ) | rpl::start_with_next([=](int langId) {
  171. if (!langId && rawGlobalLoaderPtr()) {
  172. setGlobalLoaderPtr(nullptr);
  173. } else if (langId == id) {
  174. setGlobalLoaderPtr(Spellchecker::GlobalLoader());
  175. }
  176. }, button->lifetime());
  177. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  178. button,
  179. buttonState->value() | rpl::map(StateDescription),
  180. st::settingsUpdateState);
  181. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  182. rpl::combine(
  183. button->widthValue(),
  184. label->widthValue()
  185. ) | rpl::start_with_next([=] {
  186. label->moveToLeft(
  187. st::settingsUpdateStatePosition.x(),
  188. st::settingsUpdateStatePosition.y());
  189. }, label->lifetime());
  190. buttonState->value(
  191. ) | rpl::start_with_next([=](const DictState &state) {
  192. const auto isToggledSet = v::is<Active>(state);
  193. const auto toggled = isToggledSet ? 1. : 0.;
  194. const auto over = !button->isDisabled()
  195. && (button->isDown() || button->isOver());
  196. if (toggled == 0. && !over) {
  197. label->setTextColorOverride(std::nullopt);
  198. } else {
  199. label->setTextColorOverride(anim::color(
  200. over ? st::contactsStatusFgOver : st::contactsStatusFg,
  201. st::contactsStatusFgOnline,
  202. toggled));
  203. }
  204. }, label->lifetime());
  205. button->toggleOn(
  206. rpl::single(
  207. buttonEnabled
  208. ) | rpl::then(
  209. rpl::merge(
  210. // Events to toggle on.
  211. dictionaryFromGlobalLoader->events() | rpl::map_to(true),
  212. // Events to toggle off.
  213. rpl::merge(
  214. dictionaryRemoved->events(),
  215. buttonState->value(
  216. ) | rpl::filter([](const DictState &state) {
  217. return v::is<Failed>(state);
  218. }) | rpl::to_empty
  219. ) | rpl::map_to(false)
  220. )
  221. )
  222. );
  223. *buttonState = localLoaderValues->events_starting_with(
  224. rawGlobalLoaderPtr() ? rawGlobalLoaderPtr() : localLoader->get()
  225. ) | rpl::map([=](Loader *loader) {
  226. return (loader && loader->id() == id)
  227. ? loader->state()
  228. : rpl::single(
  229. buttonEnabled
  230. ) | rpl::then(
  231. rpl::merge(
  232. dictionaryRemoved->events() | rpl::map_to(false),
  233. button->toggledValue()
  234. )
  235. ) | rpl::map([=](auto enabled) {
  236. return ComputeState(id, enabled);
  237. });
  238. }) | rpl::flatten_latest(
  239. ) | rpl::filter([=](const DictState &state) {
  240. return !v::is<Failed>(buttonState->current())
  241. || !v::is<Available>(state);
  242. });
  243. button->toggledValue(
  244. ) | rpl::start_with_next([=](bool toggled) {
  245. const auto &state = buttonState->current();
  246. if (toggled && (v::is<Available>(state) || v::is<Failed>(state))) {
  247. const auto weak = Ui::MakeWeak(button);
  248. setLocalLoader(base::make_unique_q<Loader>(
  249. QCoreApplication::instance(),
  250. session,
  251. id,
  252. Spellchecker::GetDownloadLocation(id),
  253. Spellchecker::DictPathByLangId(id),
  254. Spellchecker::GetDownloadSize(id),
  255. crl::guard(weak, destroyLocalLoader)));
  256. } else if (!toggled && v::is<Loading>(state)) {
  257. if (const auto g = rawGlobalLoaderPtr()) {
  258. g->destroy();
  259. return;
  260. }
  261. if (localLoader && localLoader->get()->id() == id) {
  262. destroyLocalLoader();
  263. }
  264. }
  265. }, button->lifetime());
  266. const auto contextMenu = button->lifetime()
  267. .make_state<base::unique_qptr<Ui::PopupMenu>>();
  268. const auto showMenu = [=] {
  269. if (!DictExists(id)) {
  270. return false;
  271. }
  272. *contextMenu = base::make_unique_q<Ui::PopupMenu>(
  273. button,
  274. st::popupMenuWithIcons);
  275. contextMenu->get()->addAction(
  276. tr::lng_settings_manage_remove_dictionary(tr::now), [=] {
  277. Spellchecker::RemoveDictionary(id);
  278. dictionaryRemoved->fire({});
  279. }, &st::menuIconDelete);
  280. contextMenu->get()->popup(QCursor::pos());
  281. return true;
  282. };
  283. base::install_event_filter(button, [=](not_null<QEvent*> e) {
  284. if (e->type() == QEvent::ContextMenu && showMenu()) {
  285. return base::EventFilterResult::Cancel;
  286. }
  287. return base::EventFilterResult::Continue;
  288. });
  289. if (const auto g = Spellchecker::GlobalLoader()) {
  290. if (g.get() && g->get()->id() == id) {
  291. setGlobalLoaderPtr(g);
  292. }
  293. }
  294. return button;
  295. }
  296. void Inner::setupContent(
  297. not_null<Main::Session*> session,
  298. Dictionaries enabledDictionaries) {
  299. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  300. const auto queryStream = content->lifetime()
  301. .make_state<rpl::event_stream<QStringView>>();
  302. for (const auto &dict : Spellchecker::Dictionaries()) {
  303. const auto id = dict.id;
  304. const auto row = AddButtonWithLoader(
  305. content,
  306. session,
  307. dict,
  308. ranges::contains(enabledDictionaries, id),
  309. queryStream->events());
  310. row->toggledValue(
  311. ) | rpl::start_with_next([=](auto enabled) {
  312. if (enabled) {
  313. _enabledRows.push_back(id);
  314. } else {
  315. auto &rows = _enabledRows;
  316. rows.erase(ranges::remove(rows, id), end(rows));
  317. }
  318. }, row->lifetime());
  319. }
  320. _queryCallback = [=](const QString &query) {
  321. if (query.size() >= kMaxQueryLength) {
  322. return;
  323. }
  324. queryStream->fire_copy(query);
  325. };
  326. content->resizeToWidth(st::boxWidth);
  327. Ui::ResizeFitChild(this, content);
  328. }
  329. } // namespace
  330. ManageDictionariesBox::ManageDictionariesBox(
  331. QWidget*,
  332. not_null<Main::Session*> session)
  333. : _session(session) {
  334. }
  335. void ManageDictionariesBox::setInnerFocus() {
  336. _setInnerFocus();
  337. }
  338. void ManageDictionariesBox::prepare() {
  339. const auto multiSelect = CreateMultiSelect(this);
  340. const auto inner = setInnerWidget(
  341. object_ptr<Inner>(
  342. this,
  343. _session,
  344. Core::App().settings().dictionariesEnabled()),
  345. st::boxScroll,
  346. multiSelect->height()
  347. );
  348. multiSelect->setQueryChangedCallback(inner->queryCallback());
  349. _setInnerFocus = [=] {
  350. multiSelect->setInnerFocus();
  351. };
  352. // The initial list of enabled rows may differ from the list of languages
  353. // in settings, so we should store it when box opens
  354. // and save it when box closes (don't do it when "Save" was pressed).
  355. const auto initialEnabledRows = inner->enabledRows();
  356. setTitle(tr::lng_settings_manage_dictionaries());
  357. addButton(tr::lng_settings_save(), [=] {
  358. Core::App().settings().setDictionariesEnabled(
  359. FilterEnabledDict(inner->enabledRows()));
  360. Core::App().saveSettingsDelayed();
  361. // Ignore boxClosing() when the Save button was pressed.
  362. lifetime().destroy();
  363. closeBox();
  364. });
  365. addButton(tr::lng_close(), [=] { closeBox(); });
  366. boxClosing() | rpl::start_with_next([=] {
  367. Core::App().settings().setDictionariesEnabled(
  368. FilterEnabledDict(initialEnabledRows));
  369. Core::App().saveSettingsDelayed();
  370. }, lifetime());
  371. setDimensionsToContent(st::boxWidth, inner);
  372. using namespace rpl::mappers;
  373. const auto max = lifetime().make_state<int>(0);
  374. rpl::combine(
  375. inner->heightValue(),
  376. multiSelect->heightValue(),
  377. _1 + _2
  378. ) | rpl::start_with_next([=](int height) {
  379. using std::min;
  380. accumulate_max(*max, height);
  381. setDimensions(st::boxWidth, min(*max, st::boxMaxListHeight), true);
  382. }, inner->lifetime());
  383. }
  384. } // namespace Ui
  385. #endif // !TDESKTOP_DISABLE_SPELLCHECK