tdesktop/Telegram/SourceFiles/boxes/translate_box.cpp

586 lines
15 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/translate_box.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/ui_integration.h"
#include "data/data_peer.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mtproto/sender.h"
#include "settings/settings_common.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "spellcheck/platform/platform_language.h"
#endif
#include "ui/effects/loading_element.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h" // inviteLinkListItem.
#include "styles/style_layers.h"
#include "styles/style_settings.h" // settingsSubsectionTitlePadding.
#include <QLocale>
namespace Ui {
namespace {
[[nodiscard]] std::vector<QLocale::Language> Languages() {
return std::vector<QLocale::Language>{
QLocale::English,
QLocale::Afrikaans,
QLocale::Albanian,
QLocale::Amharic,
QLocale::Arabic,
QLocale::Armenian,
QLocale::Azerbaijani,
QLocale::Basque,
QLocale::Belarusian,
QLocale::Bosnian,
QLocale::Bulgarian,
QLocale::Burmese,
QLocale::Catalan,
QLocale::Chinese,
QLocale::Croatian,
QLocale::Czech,
QLocale::Danish,
QLocale::Dutch,
QLocale::Esperanto,
QLocale::Estonian,
QLocale::Finnish,
QLocale::French,
QLocale::Gaelic,
QLocale::Galician,
QLocale::Georgian,
QLocale::German,
QLocale::Greek,
QLocale::Gusii,
QLocale::Hausa,
QLocale::Hebrew,
QLocale::Hungarian,
QLocale::Icelandic,
QLocale::Igbo,
QLocale::Indonesian,
QLocale::Irish,
QLocale::Italian,
QLocale::Japanese,
QLocale::Kazakh,
QLocale::Kinyarwanda,
QLocale::Korean,
QLocale::Kurdish,
QLocale::Lao,
QLocale::Latvian,
QLocale::Lithuanian,
QLocale::Luxembourgish,
QLocale::Macedonian,
QLocale::Malagasy,
QLocale::Malay,
QLocale::Maltese,
QLocale::Maori,
QLocale::Mongolian,
QLocale::Nepali,
QLocale::Pashto,
QLocale::Persian,
QLocale::Polish,
QLocale::Portuguese,
QLocale::Romanian,
QLocale::Russian,
QLocale::Serbian,
QLocale::Shona,
QLocale::Sindhi,
QLocale::Sinhala,
QLocale::Slovak,
QLocale::Slovenian,
QLocale::Somali,
QLocale::Spanish,
QLocale::Sundanese,
QLocale::Swahili,
QLocale::Swedish,
QLocale::Tajik,
QLocale::Tatar,
QLocale::Teso,
QLocale::Thai,
QLocale::Turkish,
QLocale::Turkmen,
QLocale::Ukrainian,
QLocale::Urdu,
QLocale::Uzbek,
QLocale::Vietnamese,
QLocale::Welsh,
QLocale::WesternFrisian,
QLocale::Xhosa,
QLocale::Yiddish,
};
}
class Row final : public Ui::SettingsButton {
public:
Row(not_null<Ui::RpWidget*> parent, const QLocale &locale);
[[nodiscard]] bool filtered(const QString &query) const;
[[nodiscard]] QLocale locale() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::PeerListItem &_st;
const QLocale _locale;
const QString _status;
const QString _titleText;
Ui::Text::String _title;
};
Row::Row(not_null<Ui::RpWidget*> parent, const QLocale &locale)
: SettingsButton(parent, rpl::never<QString>())
, _st(st::inviteLinkListItem)
, _locale(locale)
, _status(QLocale::languageToString(locale.language()))
, _titleText(LanguageName(locale))
, _title(_st.nameStyle, _titleText) {
}
QLocale Row::locale() const {
return _locale;
}
bool Row::filtered(const QString &query) const {
return _status.startsWith(query, Qt::CaseInsensitive)
|| _titleText.startsWith(query, Qt::CaseInsensitive);
}
int Row::resizeGetHeight(int newWidth) {
return _st.height;
}
void Row::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
Ui::SettingsButton::paintBg(p, e->rect(), paintOver);
Ui::SettingsButton::paintRipple(p, 0, 0);
Ui::SettingsButton::paintToggle(p, width());
const auto &color = st::windowSubTextFg;
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto left = st::settingsSubsectionTitlePadding.left();
const auto toggleRect = Ui::SettingsButton::maybeToggleRect();
const auto right = left
+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));
p.setPen(_st.nameFg);
_title.drawLeft(
p,
left,
_st.namePosition.y(),
width() - left - right,
width() - left - right);
p.setPen(paintOver ? _st.statusFgOver : _st.statusFg);
p.setFont(st::contactsStatusFont);
p.drawTextLeft(
left,
_st.statusPosition.y(),
width() - left - right,
_status);
}
class ShowButton final : public RpWidget {
public:
ShowButton(not_null<Ui::RpWidget*> parent);
[[nodiscard]] rpl::producer<Qt::MouseButton> clicks() const;
protected:
void paintEvent(QPaintEvent *e) override;
private:
LinkButton _button;
};
ShowButton::ShowButton(not_null<Ui::RpWidget*> parent)
: RpWidget(parent)
, _button(this, tr::lng_usernames_activate_confirm(tr::now)) {
_button.sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
resize(
s.width() + st::emojiSuggestionsFadeRight.width(),
s.height());
_button.moveToRight(0, 0);
}, lifetime());
_button.show();
}
void ShowButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto clip = e->rect();
const auto &icon = st::emojiSuggestionsFadeRight;
const auto fade = QRect(0, 0, icon.width(), height());
if (fade.intersects(clip)) {
icon.fill(p, fade);
}
const auto fill = clip.intersected(
{ icon.width(), 0, width() - icon.width(), height() });
if (!fill.isEmpty()) {
p.fillRect(fill, st::boxBg);
}
}
rpl::producer<Qt::MouseButton> ShowButton::clicks() const {
return _button.clicks();
}
} // namespace
namespace Translate {
std::vector<QLocale> LocalesFromSettings() {
const auto langs = Core::App().settings().skipTranslationForLanguages();
if (langs.empty()) {
return { QLocale(QLocale::English) };
}
return ranges::views::all(
langs
) | ranges::view::transform([](int langId) {
const auto lang = QLocale::Language(langId);
return (lang == QLocale::English)
? QLocale(Lang::LanguageIdOrDefault(Lang::Id()))
: (lang == QLocale::C)
? QLocale(QLocale::English)
: QLocale(lang);
}) | ranges::to_vector;
}
} // namespace Translate
using namespace Translate;
QString LanguageName(const QLocale &locale) {
if (locale.language() == QLocale::English
&& (locale.country() == QLocale::UnitedStates
|| locale.country() == QLocale::AnyCountry)) {
return u"English"_q;
} else if (locale.language() == QLocale::Spanish) {
return QString::fromUtf8("\x45\x73\x70\x61\xc3\xb1\x6f\x6c");
} else {
const auto name = locale.nativeLanguageName();
return name.left(1).toUpper() + name.mid(1);
}
}
void TranslateBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
MsgId msgId,
TextWithEntities text,
bool hasCopyRestriction) {
box->setWidth(st::boxWideWidth);
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
const auto container = box->verticalLayout();
const auto defaultId = LocalesFromSettings().front().name().mid(0, 2);
const auto api = box->lifetime().make_state<MTP::Sender>(
&peer->session().mtp());
struct State {
rpl::event_stream<QLocale> locale;
};
const auto state = box->lifetime().make_state<State>();
text.entities = ranges::views::all(
text.entities
) | ranges::views::filter([](const EntityInText &e) {
return e.type() != EntityType::Spoiler;
}) | ranges::to<EntitiesInText>();
if (!IsServerMsgId(msgId)) {
msgId = 0;
}
using Flag = MTPmessages_translateText::Flag;
const auto flags = msgId
? (Flag::f_peer | Flag::f_msg_id)
: !text.text.isEmpty()
? Flag::f_text
: Flag(0);
const auto &stLabel = st::aboutLabel;
const auto lineHeight = stLabel.style.lineHeight;
Settings::AddSkip(container);
// Settings::AddSubsectionTitle(
// container,
// tr::lng_translate_box_original());
const auto original = box->addRow(object_ptr<SlideWrap<FlatLabel>>(
box,
object_ptr<FlatLabel>(box, stLabel)));
{
if (hasCopyRestriction) {
original->entity()->setContextMenuHook([](auto&&) {
});
}
original->entity()->setMarkedText(
text,
Core::MarkedTextContext{
.session = &peer->session(),
.customEmojiRepaint = [=] { original->entity()->update(); },
});
original->setMinimalHeight(lineHeight);
original->hide(anim::type::instant);
const auto show = Ui::CreateChild<FadeWrap<ShowButton>>(
container.get(),
object_ptr<ShowButton>(container));
show->hide(anim::type::instant);
rpl::combine(
container->widthValue(),
original->geometryValue()
) | rpl::start_with_next([=](int width, const QRect &rect) {
show->moveToLeft(
width - show->width() - st::boxRowPadding.right(),
rect.y() + std::abs(lineHeight - show->height()) / 2);
}, show->lifetime());
original->entity()->heightValue(
) | rpl::filter([](int height) {
return height > 0;
}) | rpl::take(1) | rpl::start_with_next([=](int height) {
if (height > lineHeight) {
show->show(anim::type::instant);
}
}, show->lifetime());
show->toggleOn(show->entity()->clicks() | rpl::map_to(false));
original->toggleOn(show->entity()->clicks() | rpl::map_to(true));
}
Settings::AddSkip(container);
Settings::AddSkip(container);
Settings::AddDivider(container);
Settings::AddSkip(container);
{
const auto padding = st::settingsSubsectionTitlePadding;
const auto subtitle = Settings::AddSubsectionTitle(
container,
state->locale.events() | rpl::map(LanguageName));
// Workaround.
state->locale.events(
) | rpl::start_with_next([=] {
subtitle->resizeToWidth(container->width()
- padding.left()
- padding.right());
}, subtitle->lifetime());
}
const auto translated = box->addRow(object_ptr<SlideWrap<FlatLabel>>(
box,
object_ptr<FlatLabel>(box, stLabel)));
translated->entity()->setSelectable(!hasCopyRestriction);
translated->hide(anim::type::instant);
constexpr auto kMaxLines = 3;
container->resizeToWidth(box->width());
const auto loading = box->addRow(object_ptr<SlideWrap<RpWidget>>(
box,
CreateLoadingTextWidget(
box,
st::aboutLabel,
std::min(original->entity()->height() / lineHeight, kMaxLines),
state->locale.events() | rpl::map([=](const QLocale &locale) {
return locale.textDirection() == Qt::RightToLeft;
}))));
loading->show(anim::type::instant);
const auto showText = [=](const QString &text) {
translated->entity()->setText(text);
translated->show(anim::type::instant);
loading->hide(anim::type::instant);
};
const auto send = [=](const QString &toLang) {
api->request(MTPmessages_TranslateText(
MTP_flags(flags),
msgId ? peer->input : MTP_inputPeerEmpty(),
MTP_int(msgId),
MTP_string(text.text),
MTPstring(),
MTP_string(toLang)
)).done([=](const MTPmessages_TranslatedText &result) {
const auto text = result.match([](
const MTPDmessages_translateNoResult &data) {
return tr::lng_translate_box_error(tr::now);
}, [](const MTPDmessages_translateResultText &data) {
return qs(data.vtext());
});
showText(text);
}).fail([=](const MTP::Error &error) {
showText(tr::lng_translate_box_error(tr::now));
}).send();
};
send(defaultId);
state->locale.fire(QLocale(defaultId));
box->addLeftButton(tr::lng_settings_language(), [=] {
if (loading->toggled()) {
return;
}
Ui::BoxShow(box).showBox(Box(ChooseLanguageBox, [=](
std::vector<QLocale> locales) {
const auto &locale = locales.front();
state->locale.fire_copy(locale);
loading->show(anim::type::instant);
translated->hide(anim::type::instant);
send(locale.name().mid(0, 2));
}, std::vector<QLocale>()));
});
}
void ChooseLanguageBox(
not_null<Ui::GenericBox*> box,
Fn<void(std::vector<QLocale>)> callback,
std::vector<QLocale> toggled) {
box->setMinHeight(st::boxWidth);
box->setMaxHeight(st::boxWidth);
box->setTitle(tr::lng_languages());
const auto hasToggled = !toggled.empty();
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<Ui::MultiSelect>(
box,
st::defaultMultiSelect,
tr::lng_participant_filter()));
box->setFocusCallback([=] { multiSelect->setInnerFocus(); });
const auto container = box->verticalLayout();
const auto langs = [&] {
auto langs = Languages();
const auto current = QLocale(
Lang::LanguageIdOrDefault(Lang::Id())).language();
if (const auto it = ranges::find(langs, current); it != end(langs)) {
base::reorder(langs, std::distance(begin(langs), it), 0);
}
return langs;
}();
auto rows = std::vector<not_null<Ui::SlideWrap<Row>*>>();
rows.reserve(langs.size());
for (const auto &lang : langs) {
const auto locale = QLocale(lang);
const auto button = container->add(
object_ptr<Ui::SlideWrap<Row>>(
container,
object_ptr<Row>(container, locale)));
if (hasToggled) {
button->entity()->toggleOn(
rpl::single(ranges::contains(toggled, locale)),
false);
} else {
button->entity()->setClickedCallback([=] {
callback({ locale });
box->closeBox();
});
}
rows.push_back(button);
}
multiSelect->setQueryChangedCallback([=](const QString &query) {
for (const auto &row : rows) {
const auto toggled = row->entity()->filtered(query);
if (toggled != row->toggled()) {
row->toggle(toggled, anim::type::instant);
}
}
});
{
const auto label = Ui::CreateChild<Ui::FlatLabel>(
box.get(),
tr::lng_languages_none(),
st::membersAbout);
box->verticalLayout()->geometryValue(
) | rpl::start_with_next([=](const QRect &geometry) {
const auto shown = (geometry.height() <= 0);
label->setVisible(shown);
if (shown) {
label->moveToLeft(
(geometry.width() - label->width()) / 2,
geometry.y() + st::membersAbout.style.font->height * 4);
label->stackUnder(box->verticalLayout());
}
}, label->lifetime());
}
if (hasToggled) {
box->addButton(tr::lng_settings_save(), [=] {
auto result = ranges::views::all(
rows
) | ranges::views::filter([](const auto &row) {
return row->entity()->toggled();
}) | ranges::views::transform([](const auto &row) {
return row->entity()->locale();
}) | ranges::to_vector;
if (!result.empty()) {
callback(std::move(result));
}
box->closeBox();
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
bool SkipTranslate(TextWithEntities textWithEntities) {
const auto &text = textWithEntities.text;
if (text.isEmpty()) {
return true;
}
if (!Core::App().settings().translateButtonEnabled()) {
return true;
}
constexpr auto kFirstChunk = size_t(100);
auto hasLetters = (text.size() >= kFirstChunk);
for (auto i = 0; i < kFirstChunk; i++) {
if (i >= text.size()) {
break;
}
if (text.at(i).isLetter()) {
hasLetters = true;
break;
}
}
if (!hasLetters) {
return true;
}
#ifndef TDESKTOP_DISABLE_SPELLCHECK
const auto result = Platform::Language::Recognize(text);
if (result.unknown) {
return false;
}
return ranges::any_of(LocalesFromSettings(), [&](const QLocale &l) {
return result.locale.language() == l.language();
});
#else
return false;
#endif
}
} // namespace Ui