Redesign languages box with a search filter.

This commit is contained in:
John Preston 2018-10-30 15:23:54 +04:00
parent 162da089ec
commit 6d65cf2382
10 changed files with 852 additions and 136 deletions

View File

@ -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";

View File

@ -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*> 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<int> selections() const;
void activateSelected();
rpl::producer<Language> 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<Ui::RippleAnimation> ripple;
int titleHeight = 0;
int descriptionHeight = 0;
QStringList keywords;
};
not_null<Languages*> _languages;
std::shared_ptr<Ui::RadiobuttonGroup> _group;
std::vector<object_ptr<Ui::Radiobutton>> _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*> row);
void activateByIndex(int index);
std::vector<Row> _rows;
std::vector<not_null<Row*>> _filtered;
int _selected = -1;
int _pressed = -1;
QString _chosen;
QStringList _query;
bool _mouseSelection = false;
QPoint _globalMousePosition;
rpl::event_stream<int> _selections;
rpl::event_stream<Language> _activations;
};
LanguageBox::Inner::Inner(QWidget *parent, not_null<Languages*> languages) : TWidget(parent)
, _languages(languages) {
_group = std::make_shared<Ui::RadiobuttonGroup>(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<Language> activations() const;
void activateBySubmit();
void LanguageBox::Inner::refresh() {
for (auto &button : _buttons) {
button.destroy();
private:
void setupContent(
const Languages &official,
const Languages &unofficial);
Fn<Ui::ScrollToRequest(int rows)> _jump;
Fn<void(const QString &query)> _filter;
Fn<rpl::producer<Language>()> _activations;
Fn<void()> _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<Inner>(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<Ui::RippleAnimation>(
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<Language> 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<int> 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*> 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<Ui::VerticalLayout>(this);
const auto primary = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
const auto container = primary->entity();
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxVerticalMargin));
const auto main = container->add(object_ptr<Rows>(
container,
official,
current));
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxVerticalMargin));
const auto additional = !unofficial.isEmpty()
? content->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)))
: nullptr;
const auto inner = additional ? additional->entity() : nullptr;
const auto divider = inner
? inner->add(object_ptr<Ui::SlideWrap<BoxContentDivider>>(
inner,
object_ptr<BoxContentDivider>(inner)))
: nullptr;
const auto label = inner
? inner->add(
object_ptr<Ui::FlatLabel>(
inner,
Lang::Viewer(lng_languages_unofficial),
st::passportFormHeader),
st::passportFormHeaderPadding)
: nullptr;
const auto other = inner
? inner->add(object_ptr<Rows>(inner, unofficial, current))
: nullptr;
if (inner) {
inner->add(object_ptr<Ui::FixedHeightWidget>(
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<Language> 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<Content>(this, official, unofficial),
st::boxLayerScroll,
select->height());
inner->resizeToWidth(st::langsWidth);
const auto max = lifetime().make_state<int>(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<Ui::MultiSelect*> LanguageBox::createMultiSelect() {
const auto result = Ui::CreateChild<Ui::MultiSelect>(
this,
st::contactsMultiSelect,
langFactory(lng_participant_filter));
result->resizeToWidth(st::langsWidth);
result->moveToLeft(0, 0);
return result;
}
base::binary_guard LanguageBox::Show() {

View File

@ -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<Ui::MultiSelect*> createMultiSelect();
int rowsInPage() const;
Languages _languages;
class Inner;
QPointer<Inner> _inner;
Fn<void()> _setInnerFocus;
Fn<Ui::ScrollToRequest(int rows)> _jump;
};

View File

@ -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<MTP::Instance*> mtproto)
@ -246,10 +260,8 @@ void CloudManager::requestLanguageList() {
MTP_string(CloudLangPackName())
)).done([=](const MTPVector<MTPLangPackLanguage> &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<ConfirmSwitchBox>(

View File

@ -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<MTP::Instance*> mtproto);
struct Language {
QString id;
QString name;
QString nativeName;
};
using Languages = QVector<Language>;
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);
}

View File

@ -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<Lang::Language> 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<Lang::Language>();
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

View File

@ -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<Lang::Language> readRecentLanguages();
void writeRecentHashtagsAndBots();
void readRecentHashtagsAndBots();

View File

@ -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);

View File

@ -293,6 +293,10 @@ void MultiSelect::setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback)
_inner->setSubmittedCallback(std::move(callback));
}
void MultiSelect::setCancelledCallback(Fn<void()> callback) {
_inner->setCancelledCallback(std::move(callback));
}
void MultiSelect::setResizedCallback(Fn<void()> callback) {
_resizedCallback = std::move(callback);
}
@ -362,6 +366,7 @@ MultiSelect::Inner::Inner(QWidget *parent, const style::MultiSelect &st, Fn<QStr
connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); });
connect(_field, &Ui::InputField::changed, [=] { queryChanged(); });
connect(_field, &Ui::InputField::submitted, this, &Inner::submitted);
connect(_field, &Ui::InputField::cancelled, this, &Inner::cancelled);
_cancel->setClickedCallback([=] {
clearQuery();
_field->setFocus();
@ -405,6 +410,10 @@ void MultiSelect::Inner::setSubmittedCallback(
_submittedCallback = std::move(callback);
}
void MultiSelect::Inner::setCancelledCallback(Fn<void()> 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);
}

View File

@ -26,6 +26,7 @@ public:
void setQueryChangedCallback(Fn<void(const QString &query)> callback);
void setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);
void setCancelledCallback(Fn<void()> callback);
void setResizedCallback(Fn<void()> callback);
enum class AddItemWay {
@ -78,6 +79,7 @@ public:
void setQueryChangedCallback(Fn<void(const QString &query)> callback);
void setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);
void setCancelledCallback(Fn<void()> callback);
void addItemInBunch(std::unique_ptr<Item> 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<void(const QString &query)> _queryChangedCallback;
Fn<void(Qt::KeyboardModifiers)> _submittedCallback;
Fn<void()> _cancelledCallback;
Fn<void(uint64 itemId)> _itemRemovedCallback;
Fn<void(int heightDelta)> _resizedCallback;