/* 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 namespace Ui { namespace { [[nodiscard]] std::vector Languages() { return std::vector{ 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 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 parent, const QLocale &locale) : SettingsButton(parent, rpl::never()) , _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 parent); [[nodiscard]] rpl::producer clicks() const; protected: void paintEvent(QPaintEvent *e) override; private: LinkButton _button; }; ShowButton::ShowButton(not_null 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 ShowButton::clicks() const { return _button.clicks(); } } // namespace namespace Translate { std::vector 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 box, not_null 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( &peer->session().mtp()); struct State { rpl::event_stream locale; }; const auto state = box->lifetime().make_state(); text.entities = ranges::views::all( text.entities ) | ranges::views::filter([](const EntityInText &e) { return e.type() != EntityType::Spoiler; }) | ranges::to(); 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>( box, object_ptr(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>( container.get(), object_ptr(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>( box, object_ptr(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>( 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 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())); }); } void ChooseLanguageBox( not_null box, Fn)> callback, std::vector 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( 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*>>(); rows.reserve(langs.size()); for (const auto &lang : langs) { const auto locale = QLocale(lang); const auto button = container->add( object_ptr>( container, object_ptr(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( 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