diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e587594d9..cf8d7a8f5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -292,6 +292,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_section_general" = "General"; "lng_settings_change_lang" = "Change language"; "lng_languages" = "Languages"; +"lng_languages_unofficial" = "Unofficial languages"; "lng_sure_save_language" = "Telegram will restart in order to change language"; "lng_settings_update_automatically" = "Update automatically"; "lng_settings_install_beta" = "Install beta versions"; diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 710fe3efc..df3444e54 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -10,6 +10,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/multi_select.h" +#include "ui/widgets/scroll_area.h" +#include "ui/text/text_entity.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/effects/ripple_animation.h" +#include "ui/text_options.h" #include "storage/localstorage.h" #include "boxes/confirm_box.h" #include "mainwidget.h" @@ -18,138 +26,721 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_instance.h" #include "lang/lang_cloud_manager.h" #include "styles/style_boxes.h" +#include "styles/style_passport.h" -class LanguageBox::Inner : public TWidget, private base::Subscriber { +namespace { + +using Language = Lang::Language; +using Languages = Lang::CloudManager::Languages; + +class Rows : public Ui::RpWidget { public: - Inner(QWidget *parent, not_null languages); + Rows(QWidget *parent, const Languages &data, const QString &chosen); - void setSelected(int index); - void refresh(); + void filter(const QString &query); + + int count() const; + int selected() const; + void setSelected(int selected); + rpl::producer selections() const; + + void activateSelected(); + rpl::producer activations() const; + + Ui::ScrollToRequest rowScrollRequest(int index) const; + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void leaveEventHook(QEvent *e) override; private: - void activateCurrent(); - void languageChanged(int languageIndex); + struct Row { + Language data; + Text title = { st::boxWideWidth / 2 }; + Text description = { st::boxWideWidth / 2 }; + int top = 0; + int height = 0; + mutable std::unique_ptr ripple; + int titleHeight = 0; + int descriptionHeight = 0; + QStringList keywords; + }; - not_null _languages; - std::shared_ptr _group; - std::vector> _buttons; + void updateSelected(int selected); + void updatePressed(int pressed); + Rows::Row &rowByIndex(int index); + const Rows::Row &rowByIndex(int index) const; + int countAvailableWidth() const; + int countAvailableWidth(int newWidth) const; + void repaint(int index); + void repaint(const Row &row); + void repaintChecked(not_null row); + void activateByIndex(int index); + + std::vector _rows; + std::vector> _filtered; + int _selected = -1; + int _pressed = -1; + QString _chosen; + QStringList _query; + + bool _mouseSelection = false; + QPoint _globalMousePosition; + + rpl::event_stream _selections; + rpl::event_stream _activations; }; -LanguageBox::Inner::Inner(QWidget *parent, not_null languages) : TWidget(parent) -, _languages(languages) { - _group = std::make_shared(0); - _group->setChangedCallback([this](int value) { languageChanged(value); }); - subscribe(Lang::Current().updated(), [this] { - activateCurrent(); - refresh(); - }); -} +class Content : public Ui::RpWidget { +public: + Content( + QWidget *parent, + const Languages &official, + const Languages &unofficial); -void LanguageBox::Inner::setSelected(int index) { - _group->setValue(index); -} + Ui::ScrollToRequest jump(int rows); + void filter(const QString &query); + rpl::producer activations() const; + void activateBySubmit(); -void LanguageBox::Inner::refresh() { - for (auto &button : _buttons) { - button.destroy(); +private: + void setupContent( + const Languages &official, + const Languages &unofficial); + + Fn _jump; + Fn _filter; + Fn()> _activations; + Fn _activateBySubmit; + +}; + +Rows::Rows(QWidget *parent, const Languages &data, const QString &chosen) +: RpWidget(parent) +, _chosen(chosen) { + const auto descriptionOptions = TextParseOptions{ + TextParseMultiline, + 0, + 0, + Qt::LayoutDirectionAuto + }; + _rows.reserve(data.size()); + for (const auto &item : data) { + _rows.push_back(Row{ item }); + auto &row = _rows.back(); + row.title.setText( + st::semiboldTextStyle, + item.nativeName, + Ui::NameTextOptions()); + row.description.setText( + st::defaultTextStyle, + item.name, + descriptionOptions); + row.keywords = TextUtilities::PrepareSearchWords( + item.name + ' ' + item.nativeName); } - _buttons.clear(); - - auto y = st::boxOptionListPadding.top() + st::langsButton.margin.top(); - _buttons.reserve(_languages->size()); - auto index = 0; - for_const (auto &language, *_languages) { - _buttons.emplace_back(this, _group, index++, language.nativeName, st::langsButton); - auto button = _buttons.back().data(); - button->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), y); - button->show(); - y += button->heightNoMargins() + st::boxOptionListSkip; - } - auto newHeight = y - st::boxOptionListSkip + st::boxOptionListPadding.bottom() + st::langsButton.margin.bottom(); - resize(st::langsWidth, newHeight); -} - -void LanguageBox::Inner::languageChanged(int languageIndex) { - Expects(languageIndex >= 0 && languageIndex < _languages->size()); - - activateCurrent(); - auto languageId = (*_languages)[languageIndex].id; - if (Lang::Current().id() != languageId) { - // "#custom" is applied each time it's passed to switchToLanguage(). - // So we check that the language really has changed. - Lang::CurrentCloudManager().switchToLanguage(languageId); - } -} - -void LanguageBox::Inner::activateCurrent() { - const auto currentId = Lang::LanguageIdOrDefault(Lang::Current().id()); - for (auto i = 0, count = _languages->size(); i != count; ++i) { - const auto languageId = (*_languages)[i].id; - const auto isCurrent = (languageId == currentId); - if (isCurrent) { - _group->setValue(i); - return; - } - } -} - -void LanguageBox::prepare() { - refreshLang(); - subscribe(Lang::Current().updated(), [this] { - refreshLang(); - }); - - _inner = setInnerWidget(object_ptr(this, &_languages), st::boxLayerScroll); - - refresh(); - subscribe(Lang::CurrentCloudManager().languageListChanged(), [this] { - refresh(); - }); -} - -void LanguageBox::refreshLang() { - clearButtons(); - addButton(langFactory(lng_box_ok), [this] { closeBox(); }); - - setTitle(langFactory(lng_languages)); - + resizeToWidth(width()); + setAttribute(Qt::WA_MouseTracking); update(); } -void LanguageBox::refresh() { - refreshLanguages(); - - _inner->refresh(); - setDimensions(st::langsWidth, qMin(_inner->height(), st::boxMaxListHeight)); +void Rows::mouseMoveEvent(QMouseEvent *e) { + const auto position = e->globalPos(); + if (!_mouseSelection && position == _globalMousePosition) { + return; + } + _mouseSelection = true; + _globalMousePosition = position; + const auto index = [&] { + const auto y = e->pos().y(); + if (y < 0) { + return -1; + } + for (auto i = 0, till = count(); i != till; ++i) { + const auto &row = rowByIndex(i); + if (row.top + row.height > y) { + return i; + } + } + return -1; + }(); + updateSelected(index); } -void LanguageBox::refreshLanguages() { - _languages = Languages(); - auto list = Lang::CurrentCloudManager().languageList(); - _languages.reserve(list.size() + 1); - const auto currentId = Lang::LanguageIdOrDefault(Lang::Current().id()); - auto currentIndex = -1; - _languages.push_back({ qsl("en"), qsl("English"), qsl("English") }); - for (auto &language : list) { - const auto isCurrent = (language.id == currentId); - if (language.id != qstr("en")) { - if (isCurrent) { - currentIndex = _languages.size(); +void Rows::mousePressEvent(QMouseEvent *e) { + updatePressed(_selected); + if (_pressed >= 0) { + auto &row = rowByIndex(_pressed); + if (!row.ripple) { + auto mask = Ui::RippleAnimation::rectMask({ + width(), + row.height + }); + row.ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=, row = &row] { repaintChecked(row); }); + } + row.ripple->add(e->pos() - QPoint(0, row.top)); + } +} + +void Rows::mouseReleaseEvent(QMouseEvent *e) { + const auto pressed = _pressed; + updatePressed(-1); + if (pressed == _selected && pressed >= 0) { + activateByIndex(_selected); + } +} + +void Rows::activateByIndex(int index) { + _activations.fire_copy(rowByIndex(index).data); +} + +void Rows::leaveEventHook(QEvent *e) { + updateSelected(-1); +} + +void Rows::filter(const QString &query) { + updateSelected(-1); + updatePressed(-1); + + _query = TextUtilities::PrepareSearchWords(query); + + const auto skip = []( + const QStringList &haystack, + const QStringList &needles) { + const auto find = []( + const QStringList &haystack, + const QString &needle) { + for (const auto &item : haystack) { + if (item.startsWith(needle)) { + return true; + } + } + return false; + }; + for (const auto &needle : needles) { + if (!find(haystack, needle)) { + return true; + } + } + return false; + }; + + if (!_query.isEmpty()) { + _filtered.clear(); + _filtered.reserve(_rows.size()); + for (auto &row : _rows) { + if (!skip(row.keywords, _query)) { + _filtered.push_back(&row); + } else { + row.ripple = nullptr; } - _languages.push_back(language); - } else if (isCurrent) { - currentIndex = 0; } } - if (currentId == qstr("#custom")) { - _languages.insert(_languages.begin(), { currentId, qsl("Custom LangPack"), qsl("Custom LangPack") }); - currentIndex = 0; - } else if (currentIndex < 0) { - currentIndex = _languages.size(); - _languages.push_back({ currentId, lang(lng_language_name), lang(lng_language_name) }); + + resizeToWidth(width()); + Ui::SendPendingMoveResizeEvents(this); +} + +int Rows::count() const { + return _query.isEmpty() ? _rows.size() : _filtered.size(); +} + +int Rows::selected() const { + const auto limit = count(); + return (_selected >= 0 && _selected < limit) ? _selected : -1; +} + +void Rows::activateSelected() { + const auto index = selected(); + if (index >= 0) { + activateByIndex(index); } - _inner->setSelected(currentIndex); +} + +rpl::producer Rows::activations() const { + return _activations.events(); +} + +void Rows::setSelected(int selected) { + _mouseSelection = false; + const auto limit = count(); + updateSelected((selected >= 0 && selected < limit) ? selected : -1); +} + +rpl::producer Rows::selections() const { + return _selections.events(); +} + +void Rows::repaint(int index) { + if (index >= 0) { + repaint(rowByIndex(index)); + } +} + +void Rows::repaint(const Row &row) { + update(0, row.top, width(), row.height); +} + +void Rows::repaintChecked(not_null row) { + const auto found = (ranges::find(_filtered, row) != end(_filtered)); + if (_query.isEmpty() || found) { + repaint(*row); + } +} + +void Rows::updateSelected(int selected) { + repaint(_selected); + _selected = selected; + repaint(_selected); + _selections.fire_copy(_selected); +} + +void Rows::updatePressed(int pressed) { + if (_pressed >= 0) { + if (const auto ripple = rowByIndex(_pressed).ripple.get()) { + ripple->lastStop(); + } + } + _pressed = pressed; +} + +Rows::Row &Rows::rowByIndex(int index) { + Expects(index >= 0 && index < count()); + + return _query.isEmpty() ? _rows[index] : *_filtered[index]; +} + +const Rows::Row &Rows::rowByIndex(int index) const { + Expects(index >= 0 && index < count()); + + return _query.isEmpty() ? _rows[index] : *_filtered[index]; +} + +Ui::ScrollToRequest Rows::rowScrollRequest(int index) const { + const auto &row = rowByIndex(index); + return Ui::ScrollToRequest(row.top, row.top + row.height); +} + +int Rows::resizeGetHeight(int newWidth) { + const auto availableWidth = countAvailableWidth(newWidth); + auto result = 0; + for (auto i = 0, till = count(); i != till; ++i) { + auto &row = rowByIndex(i); + row.top = result; + row.titleHeight = row.title.countHeight(availableWidth); + row.descriptionHeight = row.description.countHeight(availableWidth); + row.height = st::passportRowPadding.top() + + row.titleHeight + + st::passportRowSkip + + row.descriptionHeight + + st::passportRowPadding.bottom(); + result += row.height; + } + return result; +} + +int Rows::countAvailableWidth(int newWidth) const { + return newWidth + - st::passportRowPadding.left() + - st::passportRowPadding.right() + - st::passportRowReadyIcon.width() + - st::passportRowIconSkip; +} + +int Rows::countAvailableWidth() const { + return countAvailableWidth(width()); +} + +void Rows::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto ms = getms(); + const auto clip = e->rect(); + + const auto left = st::passportRowPadding.left(); + const auto availableWidth = countAvailableWidth(); + for (auto i = 0, till = count(); i != till; ++i) { + const auto &row = rowByIndex(i); + if (row.top + row.height <= clip.y()) { + continue; + } else if (row.top >= clip.y() + clip.height()) { + break; + } + p.translate(0, row.top); + const auto guard = gsl::finally([&] { p.translate(0, -row.top); }); + + const auto selected = (_selected == i); + if (selected) { + p.fillRect(0, 0, width(), row.height, st::windowBgOver); + } + + if (row.ripple) { + row.ripple->paint(p, 0, 0, width(), ms); + if (row.ripple->empty()) { + row.ripple.reset(); + } + } + + auto top = st::passportRowPadding.top(); + + p.setPen(st::passportRowTitleFg); + row.title.drawLeft(p, left, top, availableWidth, width()); + top += row.titleHeight + st::passportRowSkip; + + p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg); + row.description.drawLeft(p, left, top, availableWidth, width()); + top += row.descriptionHeight + st::passportRowPadding.bottom(); + + if (row.data.id == _chosen) { + const auto &icon = st::passportRowReadyIcon; + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (row.height - icon.height()) / 2, + width()); + } + } +} + +Content::Content( + QWidget *parent, + const Languages &official, + const Languages &unofficial) +: RpWidget(parent) { + setupContent(official, unofficial); +} + +void Content::setupContent( + const Languages &official, + const Languages &unofficial) { + const auto current = Lang::LanguageIdOrDefault(Lang::Current().id()); + const auto content = Ui::CreateChild(this); + const auto primary = content->add( + object_ptr>( + content, + object_ptr(content))); + const auto container = primary->entity(); + container->add(object_ptr( + container, + st::boxVerticalMargin)); + const auto main = container->add(object_ptr( + container, + official, + current)); + container->add(object_ptr( + container, + st::boxVerticalMargin)); + const auto additional = !unofficial.isEmpty() + ? content->add(object_ptr>( + content, + object_ptr(content))) + : nullptr; + const auto inner = additional ? additional->entity() : nullptr; + const auto divider = inner + ? inner->add(object_ptr>( + inner, + object_ptr(inner))) + : nullptr; + const auto label = inner + ? inner->add( + object_ptr( + inner, + Lang::Viewer(lng_languages_unofficial), + st::passportFormHeader), + st::passportFormHeaderPadding) + : nullptr; + const auto other = inner + ? inner->add(object_ptr(inner, unofficial, current)) + : nullptr; + if (inner) { + inner->add(object_ptr( + inner, + st::boxVerticalMargin)); + } + Ui::ResizeFitChild(this, content); + + using namespace rpl::mappers; + auto nonempty = [](Rows *rows) { + return rows->heightValue( + ) | rpl::map( + _1 > 0 + ) | rpl::distinct_until_changed( + ); + }; + nonempty(main) | rpl::start_with_next([=](bool nonempty) { + primary->toggle(nonempty, anim::type::instant); + }, main->lifetime()); + if (other) { + nonempty(other) | rpl::start_with_next([=](bool nonempty) { + additional->toggle(nonempty, anim::type::instant); + }, other->lifetime()); + + rpl::combine( + nonempty(main), + nonempty(other), + _1 && _2 + ) | rpl::start_with_next([=](bool nonempty) { + divider->toggle(nonempty, anim::type::instant); + }, divider->lifetime()); + + const auto excludeSelections = [](Rows *a, Rows *b) { + a->selections( + ) | rpl::filter( + _1 >= 0 + ) | rpl::start_with_next([=] { + b->setSelected(-1); + }, a->lifetime()); + }; + excludeSelections(main, other); + excludeSelections(other, main); + } + + const auto rowsCount = [=] { + return main->count() + (other ? other->count() : 0); + }; + const auto selectedIndex = [=] { + if (const auto index = main->selected(); index >= 0) { + return index; + } + const auto index = other ? other->selected() : -1; + return (index >= 0) ? (main->count() + index) : -1; + }; + const auto setSelectedIndex = [=](int index) { + const auto count = main->count(); + if (index >= count) { + main->setSelected(-1); + if (other) { + other->setSelected(index - count); + } + } else { + main->setSelected(index); + if (other) { + other->setSelected(-1); + } + } + }; + const auto selectedCoords = [=] { + const auto coords = [=](Rows *rows, int index) { + const auto result = rows->rowScrollRequest(index); + const auto shift = rows->mapToGlobal({ 0, 0 }).y() + - mapToGlobal({ 0, 0 }).y(); + return Ui::ScrollToRequest( + result.ymin + shift, + result.ymax + shift); + }; + if (const auto index = main->selected(); index >= 0) { + return coords(main, index); + } + const auto index = other ? other->selected() : -1; + if (index >= 0) { + return coords(other, index); + } + return Ui::ScrollToRequest(-1, -1); + }; + _jump = [=](int rows) { + const auto count = rowsCount(); + const auto now = selectedIndex(); + if (now >= 0) { + const auto changed = now + rows; + if (changed < 0) { + setSelectedIndex((now > 0) ? 0 : -1); + } else if (changed >= count) { + setSelectedIndex(count - 1); + } else { + setSelectedIndex(changed); + } + } else if (rows > 0) { + setSelectedIndex(0); + } + return selectedCoords(); + }; + _filter = [=](const QString &query) { + main->filter(query); + if (other) { + other->filter(query); + } + }; + _activations = [=] { + if (!other) { + return main->activations(); + } + return rpl::merge( + main->activations(), + other->activations() + ) | rpl::type_erased(); + }; + _activateBySubmit = [=] { + if (selectedIndex() < 0) { + _jump(1); + } + main->activateSelected(); + if (other) { + other->activateSelected(); + } + }; +} + +void Content::filter(const QString &query) { + _filter(query); +} + +rpl::producer Content::activations() const { + return _activations(); +} + +void Content::activateBySubmit() { + _activateBySubmit(); +} + +Ui::ScrollToRequest Content::jump(int rows) { + return _jump(rows); +} + +} // namespace + +void LanguageBox::prepare() { + addButton(langFactory(lng_box_ok), [=] { closeBox(); }); + + setTitle(langFactory(lng_languages)); + + const auto select = createMultiSelect(); + + const auto current = Lang::LanguageIdOrDefault(Lang::Current().id()); + auto official = Lang::CurrentCloudManager().languageList(); + if (official.isEmpty()) { + official.push_back({ "en", "English", "English" }); + } + ranges::stable_partition(official, [&](const Language &language) { + return (language.id == current); + }); + ranges::stable_partition(official, [&](const Language &language) { + return (language.id == current) || (language.id == "en"); + }); + const auto foundInOfficial = [&](const Language &language) { + return ranges::find(official, language.id, [](const Language &v) { + return v.id; + }) != official.end(); + }; + auto unofficial = Local::readRecentLanguages(); + unofficial.erase( + ranges::remove_if( + unofficial, + foundInOfficial), + unofficial.end()); + ranges::stable_partition(unofficial, [&](const Language &language) { + return (language.id == current); + }); + if (official.front().id != current + && (unofficial.isEmpty() || unofficial.front().id != current)) { + const auto name = (current == "#custom") + ? "Custom lang pack" + : lang(lng_language_name); + unofficial.push_back({ + current, + QString(), + QString(), + name, + lang(lng_language_name) + }); + } + + using namespace rpl::mappers; + + const auto inner = setInnerWidget( + object_ptr(this, official, unofficial), + st::boxLayerScroll, + select->height()); + inner->resizeToWidth(st::langsWidth); + + const auto max = lifetime().make_state(0); + rpl::combine( + inner->heightValue(), + select->heightValue(), + _1 + _2 + ) | rpl::start_with_next([=](int height) { + accumulate_max(*max, height); + setDimensions(st::langsWidth, qMin(*max, st::boxMaxListHeight)); + }, inner->lifetime()); + + select->setSubmittedCallback([=](Qt::KeyboardModifiers) { + inner->activateBySubmit(); + }); + select->setQueryChangedCallback([=](const QString &query) { + inner->filter(query); + }); + select->setCancelledCallback([=] { + select->clearQuery(); + }); + + inner->activations( + ) | rpl::start_with_next([=](const Language &language) { + // "#custom" is applied each time it's passed to switchToLanguage(). + // So we check that the language really has changed. + if (language.id != Lang::Current().id()) { + Lang::CurrentCloudManager().switchToLanguage( + language.id, + language.pluralId, + language.baseId); + } + }, inner->lifetime()); + + _setInnerFocus = [=] { + select->setInnerFocus(); + }; + _jump = [=](int rows) { + return inner->jump(rows); + }; +} + +void LanguageBox::keyPressEvent(QKeyEvent *e) { + const auto key = e->key(); + const auto selected = [&] { + if (key == Qt::Key_Up) { + return _jump(-1); + } else if (key == Qt::Key_Down) { + return _jump(1); + } else if (key == Qt::Key_PageUp) { + return _jump(-rowsInPage()); + } else if (key == Qt::Key_PageDown) { + return _jump(rowsInPage()); + } + return Ui::ScrollToRequest(-1, -1); + }(); + if (selected.ymin >= 0 && selected.ymax >= 0) { + onScrollToY(selected.ymin, selected.ymax); + } +} + +int LanguageBox::rowsInPage() const { + const auto rowHeight = st::passportRowPadding.top() + + st::semiboldFont->height + + st::passportRowSkip + + st::normalFont->height + + st::passportRowPadding.bottom(); + return std::max(height() / rowHeight, 1); +} + +void LanguageBox::setInnerFocus() { + _setInnerFocus(); +} + +not_null LanguageBox::createMultiSelect() { + const auto result = Ui::CreateChild( + this, + st::contactsMultiSelect, + langFactory(lng_participant_filter)); + result->resizeToWidth(st::langsWidth); + result->moveToLeft(0, 0); + return result; } base::binary_guard LanguageBox::Show() { diff --git a/Telegram/SourceFiles/boxes/language_box.h b/Telegram/SourceFiles/boxes/language_box.h index 23b10cfb1..4cc3dd56b 100644 --- a/Telegram/SourceFiles/boxes/language_box.h +++ b/Telegram/SourceFiles/boxes/language_box.h @@ -9,34 +9,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_cloud_manager.h" #include "boxes/abstract_box.h" -#include "mtproto/sender.h" #include "base/binary_guard.h" namespace Ui { -class RadiobuttonGroup; -class Radiobutton; +class MultiSelect; +struct ScrollToRequest; } // namespace Ui -class LanguageBox : public BoxContent, private MTP::Sender { +class LanguageBox : public BoxContent { public: LanguageBox(QWidget*) { } + void setInnerFocus() override; + static base::binary_guard Show(); protected: void prepare() override; + void keyPressEvent(QKeyEvent *e) override; + private: using Languages = Lang::CloudManager::Languages; - void refresh(); - void refreshLanguages(); - void refreshLang(); + not_null createMultiSelect(); + int rowsInPage() const; - Languages _languages; - - class Inner; - QPointer _inner; + Fn _setInnerFocus; + Fn _jump; }; diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp index 34bd9d436..6a16d7a42 100644 --- a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp +++ b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp @@ -107,6 +107,20 @@ void ConfirmSwitchBox::prepare() { } // namespace +Language ParseLanguage(const MTPLangPackLanguage &data) { + return data.match([](const MTPDlangPackLanguage &data) { + return Language{ + qs(data.vlang_code), + qs(data.vplural_code), + (data.has_base_lang_code() + ? qs(data.vbase_lang_code) + : QString()), + qs(data.vname), + qs(data.vnative_name) + }; + }); +} + CloudManager::CloudManager( Instance &langpack, not_null mtproto) @@ -246,10 +260,8 @@ void CloudManager::requestLanguageList() { MTP_string(CloudLangPackName()) )).done([=](const MTPVector &result) { auto languages = Languages(); - for_const (auto &langData, result.v) { - Assert(langData.type() == mtpc_langPackLanguage); - auto &language = langData.c_langPackLanguage(); - languages.push_back({ qs(language.vlang_code), qs(language.vname), qs(language.vnative_name) }); + for (const auto &language : result.v) { + languages.push_back(ParseLanguage(language)); } if (_languages != languages) { _languages = languages; @@ -348,6 +360,7 @@ void CloudManager::switchWithWarning(const QString &id) { const auto pluralId = qs(data.vplural_code); const auto baseId = qs(data.vbase_lang_code); const auto perform = [=] { + Local::pushRecentLanguage(ParseLanguage(result)); performSwitchAndRestart(id, pluralId, baseId); }; Ui::show(Box( diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.h b/Telegram/SourceFiles/lang/lang_cloud_manager.h index 05aa74acb..6bfb85fb2 100644 --- a/Telegram/SourceFiles/lang/lang_cloud_manager.h +++ b/Telegram/SourceFiles/lang/lang_cloud_manager.h @@ -19,15 +19,19 @@ namespace Lang { class Instance; enum class Pack; +struct Language { + QString id; + QString pluralId; + QString baseId; + QString name; + QString nativeName; +}; +Language ParseLanguage(const MTPLangPackLanguage &data); + class CloudManager : public base::has_weak_ptr, private MTP::Sender, private base::Subscriber { public: CloudManager(Instance &langpack, not_null mtproto); - struct Language { - QString id; - QString name; - QString nativeName; - }; using Languages = QVector; void requestLanguageList(); @@ -103,11 +107,11 @@ private: }; -inline bool operator==(const CloudManager::Language &a, const CloudManager::Language &b) { +inline bool operator==(const Language &a, const Language &b) { return (a.id == b.id) && (a.name == b.name); } -inline bool operator!=(const CloudManager::Language &a, const CloudManager::Language &b) { +inline bool operator!=(const Language &a, const Language &b) { return !(a == b); } diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index 4fe1bd8ef..9753746ca 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "mainwindow.h" #include "lang/lang_keys.h" +#include "lang/lang_cloud_manager.h" #include "media/media_audio.h" #include "mtproto/dc_options.h" #include "messenger.h" @@ -599,6 +600,7 @@ enum { dbiAnimationsDisabled = 0x57, dbiScalePercent = 0x58, dbiPlaybackSpeed = 0x59, + dbiLanguagesKey = 0x5a, dbiEncryptedWithSalt = 333, dbiEncrypted = 444, @@ -664,6 +666,7 @@ FileKey _exportSettingsKey = 0; FileKey _savedPeersKey = 0; FileKey _langPackKey = 0; +FileKey _languagesKey = 0; bool _mapChanged = false; int32 _oldMapVersion = 0, _oldSettingsVersion = 0; @@ -1354,6 +1357,14 @@ bool _readSetting(quint32 blockId, QDataStream &stream, int version, ReadSetting _langPackKey = langPackKey; } break; + case dbiLanguagesKey: { + quint64 languagesKey = 0; + stream >> languagesKey; + if (!_checkStreamStatus(stream)) return false; + + _languagesKey = languagesKey; + } break; + case dbiTryIPv6: { qint32 v; stream >> v; @@ -2629,6 +2640,9 @@ void writeSettings() { if (_langPackKey) { data.stream << quint32(dbiLangPackKey) << quint64(_langPackKey); } + if (_languagesKey) { + data.stream << quint32(dbiLanguagesKey) << quint64(_languagesKey); + } auto position = cWindowPos(); data.stream << quint32(dbiWindowPosition) << qint32(position.x) << qint32(position.y) << qint32(position.w) << qint32(position.h); @@ -4117,6 +4131,74 @@ void writeLangPack() { file.writeEncrypted(data, SettingsKey); } +void pushRecentLanguage(const Lang::Language &language) { + if (language.id.startsWith('#')) { + return; + } + auto list = readRecentLanguages(); + list.erase( + ranges::remove_if( + list, + [&](const Lang::Language &v) { return (v.id == language.id); }), + list.end()); + list.insert(list.begin(), language); + + auto size = sizeof(qint32); + for (const auto &language : list) { + size += Serialize::stringSize(language.id) + + Serialize::stringSize(language.pluralId) + + Serialize::stringSize(language.baseId) + + Serialize::stringSize(language.name) + + Serialize::stringSize(language.nativeName); + } + if (!_languagesKey) { + _languagesKey = genKey(FileOption::Safe); + writeSettings(); + } + + EncryptedDescriptor data(size); + data.stream << qint32(list.size()); + for (const auto &language : list) { + data.stream + << language.id + << language.pluralId + << language.baseId + << language.name + << language.nativeName; + } + + FileWriteDescriptor file(_languagesKey, FileOption::Safe); + file.writeEncrypted(data, SettingsKey); +} + +QVector readRecentLanguages() { + FileReadDescriptor languages; + if (!_languagesKey || !readEncryptedFile(languages, _languagesKey, FileOption::Safe, SettingsKey)) { + return {}; + } + qint32 count = 0; + languages.stream >> count; + if (count <= 0) { + return {}; + } + auto result = QVector(); + result.reserve(count); + for (auto i = 0; i != count; ++i) { + auto language = Lang::Language(); + languages.stream + >> language.id + >> language.pluralId + >> language.baseId + >> language.name + >> language.nativeName; + result.push_back(language); + } + if (languages.stream.status() != QDataStream::Ok) { + return {}; + } + return result; +} + bool copyThemeColorsToPalette(const QString &path) { auto &themeKey = Window::Theme::IsNightMode() ? _themeKeyNight diff --git a/Telegram/SourceFiles/storage/localstorage.h b/Telegram/SourceFiles/storage/localstorage.h index dc48f5358..77a2a5039 100644 --- a/Telegram/SourceFiles/storage/localstorage.h +++ b/Telegram/SourceFiles/storage/localstorage.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localimageloader.h" #include "auth_session.h" +namespace Lang { +struct Language; +} // namespace Lang + namespace Storage { class EncryptionKey; } // namespace Storage @@ -141,6 +145,8 @@ bool copyThemeColorsToPalette(const QString &file); Window::Theme::Saved readThemeAfterSwitch(); void writeLangPack(); +void pushRecentLanguage(const Lang::Language &language); +QVector readRecentLanguages(); void writeRecentHashtagsAndBots(); void readRecentHashtagsAndBots(); diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 90997a1f3..2dd172773 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -1057,7 +1057,7 @@ void FlatInput::phPrepare(Painter &p, float64 placeholderFocused) { void FlatInput::keyPressEvent(QKeyEvent *e) { QString wasText(_oldtext); - if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { + if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) { e->ignore(); } else { QLineEdit::keyPressEvent(e); @@ -2530,7 +2530,7 @@ void InputField::keyPressEventInner(QKeyEvent *e) { e->ignore(); } else if (handleMarkdownKey(e)) { e->accept(); - } else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { + } else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) { e->ignore(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { @@ -3756,7 +3756,7 @@ void MaskedInputField::keyPressEvent(QKeyEvent *e) { QString wasText(_oldtext); int32 wasCursor(_oldcursor); - if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { + if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) { e->ignore(); } else { QLineEdit::keyPressEvent(e); diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.cpp b/Telegram/SourceFiles/ui/widgets/multi_select.cpp index bcf7722bd..ee5c46d99 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.cpp +++ b/Telegram/SourceFiles/ui/widgets/multi_select.cpp @@ -293,6 +293,10 @@ void MultiSelect::setSubmittedCallback(Fn callback) _inner->setSubmittedCallback(std::move(callback)); } +void MultiSelect::setCancelledCallback(Fn callback) { + _inner->setCancelledCallback(std::move(callback)); +} + void MultiSelect::setResizedCallback(Fn callback) { _resizedCallback = std::move(callback); } @@ -362,6 +366,7 @@ MultiSelect::Inner::Inner(QWidget *parent, const style::MultiSelect &st, FnsetClickedCallback([=] { clearQuery(); _field->setFocus(); @@ -405,6 +410,10 @@ void MultiSelect::Inner::setSubmittedCallback( _submittedCallback = std::move(callback); } +void MultiSelect::Inner::setCancelledCallback(Fn callback) { + _cancelledCallback = std::move(callback); +} + void MultiSelect::Inner::updateFieldGeometry() { auto fieldFinalWidth = _fieldWidth; if (_cancel->toggled()) { @@ -570,6 +579,12 @@ void MultiSelect::Inner::submitted(Qt::KeyboardModifiers modifiers) { } } +void MultiSelect::Inner::cancelled() { + if (_cancelledCallback) { + _cancelledCallback(); + } +} + void MultiSelect::Inner::fieldFocused() { setActiveItem(-1, ChangeActiveWay::SkipSetFocus); } diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.h b/Telegram/SourceFiles/ui/widgets/multi_select.h index 9c84522d7..cf07cc0b5 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.h +++ b/Telegram/SourceFiles/ui/widgets/multi_select.h @@ -26,6 +26,7 @@ public: void setQueryChangedCallback(Fn callback); void setSubmittedCallback(Fn callback); + void setCancelledCallback(Fn callback); void setResizedCallback(Fn callback); enum class AddItemWay { @@ -78,6 +79,7 @@ public: void setQueryChangedCallback(Fn callback); void setSubmittedCallback(Fn callback); + void setCancelledCallback(Fn callback); void addItemInBunch(std::unique_ptr item); void finishItemsBunch(AddItemWay way); @@ -105,6 +107,7 @@ protected: private: void submitted(Qt::KeyboardModifiers modifiers); + void cancelled(); void queryChanged(); void fieldFocused(); void computeItemsGeometry(int newWidth); @@ -152,6 +155,7 @@ private: Fn _queryChangedCallback; Fn _submittedCallback; + Fn _cancelledCallback; Fn _itemRemovedCallback; Fn _resizedCallback;