Skip to content

Commit 8353d49

Browse files
committed
enchant: allow toggling individual dictionaries
Allow disabling and enabling individual dictionaries with the Enchant/Hunspell backend. This is mostly relevant in shared systems (where different users want to enable different sets of languages) or on distributions which install an exorbitant amount of dictionaries by default. Depends-On: desktop-app/lib_spellcheck#32
1 parent 147439a commit 8353d49

File tree

4 files changed

+170
-26
lines changed

4 files changed

+170
-26
lines changed

Telegram/SourceFiles/boxes/dictionaries_manager.cpp

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ For license and copyright information please follow this link:
1919
#include "mainwidget.h"
2020
#include "mtproto/dedicated_file_loader.h"
2121
#include "spellcheck/spellcheck_utils.h"
22+
#include "spellcheck/platform/platform_spellcheck.h"
2223
#include "ui/wrap/vertical_layout.h"
2324
#include "ui/widgets/buttons.h"
2425
#include "ui/widgets/labels.h"
@@ -42,6 +43,32 @@ using DictState = BlobState;
4243
using QueryCallback = Fn<void(const QString &)>;
4344
constexpr auto kMaxQueryLength = 15;
4445

46+
// Get system dictionaries for enchant backend.
47+
std::vector<Spellchecker::Dict> SystemDictionaries() {
48+
const auto available = Platform::Spellchecker::AvailableLanguages();
49+
std::vector<Spellchecker::Dict> result;
50+
std::set<int> seenIds;
51+
result.reserve(available.size());
52+
53+
for (const auto &locale : available) {
54+
const auto langId = Spellchecker::LangIdFromLocale(locale);
55+
56+
// Deduplicate langIds. E.g.: en_GB and en_GB-large.
57+
if (!seenIds.insert(langId).second) {
58+
continue;
59+
}
60+
61+
const auto qlocale = QLocale(locale);
62+
auto name = QLocale::languageToString(qlocale.language());
63+
if (qlocale.country() != QLocale::AnyCountry) {
64+
name += " (" + QLocale::countryToString(qlocale.country()) + ")";
65+
}
66+
result.push_back(Spellchecker::Dict{ langId, 0, 0, name });
67+
}
68+
69+
return result;
70+
}
71+
4572
class Inner : public Ui::RpWidget {
4673
public:
4774
Inner(
@@ -63,6 +90,16 @@ class Inner : public Ui::RpWidget {
6390
};
6491

6592
inline auto DictExists(int langId) {
93+
if (Platform::Spellchecker::IsSystemSpellchecker()) {
94+
// Check if the locale exists in available languages.
95+
const auto available = Platform::Spellchecker::AvailableLanguages();
96+
for (const auto &availableLocale : available) {
97+
if (Spellchecker::LangIdFromLocale(availableLocale) == langId) {
98+
return true;
99+
}
100+
}
101+
return false;
102+
}
66103
return Spellchecker::DictionaryExists(langId);
67104
}
68105

@@ -77,6 +114,10 @@ DictState ComputeState(int id, bool enabled) {
77114
if (DictExists(id)) {
78115
return result;
79116
}
117+
// For system spellchecker, dictionaries that don't exist are not available.
118+
if (Platform::Spellchecker::IsSystemSpellchecker()) {
119+
return Ready();
120+
}
80121
return Available{ Spellchecker::GetDownloadSize(id) };
81122
}
82123

@@ -113,6 +154,86 @@ Dictionaries Inner::enabledRows() const {
113154
return _enabledRows;
114155
}
115156

157+
// Simplified button for system dictionaries (no download/remove).
158+
auto AddSystemDictButton(
159+
not_null<Ui::VerticalLayout*> content,
160+
const Spellchecker::Dict &dict,
161+
bool buttonEnabled,
162+
rpl::producer<QStringView> query) {
163+
const auto id = dict.id;
164+
buttonEnabled &= DictExists(id);
165+
166+
const auto locale = Spellchecker::LocaleFromLangId(id);
167+
const std::vector<QString> indexList = {
168+
dict.name,
169+
QLocale::languageToString(locale.language()),
170+
QLocale::countryToString(locale.country())
171+
};
172+
173+
const auto wrap = content->add(
174+
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
175+
content,
176+
object_ptr<Ui::SettingsButton>(
177+
content,
178+
rpl::single(dict.name),
179+
st::dictionariesSectionButton
180+
)
181+
)
182+
);
183+
const auto button = wrap->entity();
184+
185+
std::move(
186+
query
187+
) | rpl::start_with_next([=](auto string) {
188+
wrap->toggle(
189+
ranges::any_of(indexList, [&](const QString &s) {
190+
return s.startsWith(string, Qt::CaseInsensitive);
191+
}),
192+
anim::type::instant);
193+
}, button->lifetime());
194+
195+
const auto label = Ui::CreateChild<Ui::FlatLabel>(
196+
button,
197+
(buttonEnabled
198+
? tr::lng_settings_manage_enabled_dictionary(tr::now)
199+
: QString()),
200+
st::settingsUpdateState);
201+
label->setAttribute(Qt::WA_TransparentForMouseEvents);
202+
203+
rpl::combine(
204+
button->widthValue(),
205+
label->widthValue()
206+
) | rpl::start_with_next([=] {
207+
label->moveToLeft(
208+
st::settingsUpdateStatePosition.x(),
209+
st::settingsUpdateStatePosition.y());
210+
}, label->lifetime());
211+
212+
button->toggleOn(rpl::single(buttonEnabled));
213+
214+
button->toggledValue(
215+
) | rpl::start_with_next([=](bool toggled) {
216+
const auto over = !button->isDisabled()
217+
&& (button->isDown() || button->isOver());
218+
const auto toggledFloat = toggled ? 1. : 0.;
219+
220+
if (toggledFloat == 0. && !over) {
221+
label->setTextColorOverride(std::nullopt);
222+
} else {
223+
label->setTextColorOverride(anim::color(
224+
over ? st::contactsStatusFgOver : st::contactsStatusFg,
225+
st::contactsStatusFgOnline,
226+
toggledFloat));
227+
}
228+
229+
label->setText(toggled
230+
? tr::lng_settings_manage_enabled_dictionary(tr::now)
231+
: QString());
232+
}, label->lifetime());
233+
234+
return button;
235+
}
236+
116237
auto AddButtonWithLoader(
117238
not_null<Ui::VerticalLayout*> content,
118239
not_null<Main::Session*> session,
@@ -344,14 +465,25 @@ void Inner::setupContent(
344465
const auto queryStream = content->lifetime()
345466
.make_state<rpl::event_stream<QStringView>>();
346467

347-
for (const auto &dict : Spellchecker::Dictionaries()) {
468+
const auto isSystemSpellchecker = Platform::Spellchecker::IsSystemSpellchecker();
469+
const auto dictionaries = isSystemSpellchecker
470+
? SystemDictionaries()
471+
: Spellchecker::Dictionaries();
472+
473+
for (const auto &dict : dictionaries) {
348474
const auto id = dict.id;
349-
const auto row = AddButtonWithLoader(
350-
content,
351-
session,
352-
dict,
353-
ranges::contains(enabledDictionaries, id),
354-
queryStream->events());
475+
const auto row = isSystemSpellchecker
476+
? AddSystemDictButton(
477+
content,
478+
dict,
479+
ranges::contains(enabledDictionaries, id),
480+
queryStream->events())
481+
: AddButtonWithLoader(
482+
content,
483+
session,
484+
dict,
485+
ranges::contains(enabledDictionaries, id),
486+
queryStream->events());
355487
row->toggledValue(
356488
) | rpl::start_with_next([=](auto enabled) {
357489
if (enabled) {

Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,14 @@ void Start(not_null<Main::Session*> session) {
391391
| rpl::take(1)
392392
| rpl::start_with_next(AddExceptions, lifetime);
393393

394+
settings->dictionariesEnabledChanges(
395+
) | rpl::start_with_next([](auto dictionaries) {
396+
Platform::Spellchecker::UpdateLanguages(dictionaries);
397+
}, lifetime);
398+
399+
settings->spellcheckerEnabledChanges(
400+
) | rpl::start_with_next(onEnabled, lifetime);
401+
394402
return;
395403
}
396404

Telegram/SourceFiles/settings/settings_advanced.cpp

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -306,33 +306,37 @@ void SetupSpellchecker(
306306
Core::App().saveSettingsDelayed();
307307
}, container->lifetime());
308308

309-
if (isSystem) {
310-
return;
311-
}
312-
313309
const auto sliding = container->add(
314310
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
315311
container,
316312
object_ptr<Ui::VerticalLayout>(container)));
317313

318-
sliding->entity()->add(object_ptr<Button>(
319-
sliding->entity(),
320-
tr::lng_settings_auto_download_dictionaries(),
321-
st::settingsButtonNoIcon
322-
))->toggleOn(
323-
rpl::single(settings->autoDownloadDictionaries())
324-
)->toggledValue(
325-
) | rpl::filter([=](bool enabled) {
326-
return (enabled != settings->autoDownloadDictionaries());
327-
}) | rpl::start_with_next([=](bool enabled) {
328-
settings->setAutoDownloadDictionaries(enabled);
329-
Core::App().saveSettingsDelayed();
330-
}, sliding->entity()->lifetime());
314+
if (!isSystem) {
315+
sliding->entity()->add(object_ptr<Button>(
316+
sliding->entity(),
317+
tr::lng_settings_auto_download_dictionaries(),
318+
st::settingsButtonNoIcon
319+
))->toggleOn(
320+
rpl::single(settings->autoDownloadDictionaries())
321+
)->toggledValue(
322+
) | rpl::filter([=](bool enabled) {
323+
return (enabled != settings->autoDownloadDictionaries());
324+
}) | rpl::start_with_next([=](bool enabled) {
325+
settings->setAutoDownloadDictionaries(enabled);
326+
Core::App().saveSettingsDelayed();
327+
}, sliding->entity()->lifetime());
328+
}
329+
330+
if (isSystem && !Platform::Spellchecker::SupportsToggleDictionaries()) {
331+
return;
332+
}
331333

332334
AddButtonWithLabel(
333335
sliding->entity(),
334336
tr::lng_settings_manage_dictionaries(),
335-
Spellchecker::ButtonManageDictsState(session),
337+
isSystem
338+
? rpl::single(QString())
339+
: Spellchecker::ButtonManageDictsState(session),
336340
st::settingsButtonNoIcon
337341
)->addClickHandler([=] {
338342
controller->show(

0 commit comments

Comments
 (0)