translate_box.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "boxes/translate_box.h"
  8. #include "api/api_text_entities.h" // Api::EntitiesToMTP / EntitiesFromMTP.
  9. #include "core/application.h"
  10. #include "core/core_settings.h"
  11. #include "core/ui_integration.h"
  12. #include "data/data_peer.h"
  13. #include "data/data_session.h"
  14. #include "history/history.h"
  15. #include "lang/lang_instance.h"
  16. #include "lang/lang_keys.h"
  17. #include "main/main_session.h"
  18. #include "mtproto/sender.h"
  19. #include "spellcheck/platform/platform_language.h"
  20. #include "ui/boxes/choose_language_box.h"
  21. #include "ui/effects/loading_element.h"
  22. #include "ui/layers/generic_box.h"
  23. #include "ui/text/text_utilities.h"
  24. #include "ui/vertical_list.h"
  25. #include "ui/painter.h"
  26. #include "ui/power_saving.h"
  27. #include "ui/widgets/buttons.h"
  28. #include "ui/widgets/labels.h"
  29. #include "ui/widgets/multi_select.h"
  30. #include "ui/wrap/fade_wrap.h"
  31. #include "ui/wrap/slide_wrap.h"
  32. #include "styles/style_boxes.h"
  33. #include "styles/style_chat_helpers.h"
  34. #include "styles/style_info.h" // inviteLinkListItem.
  35. #include "styles/style_layers.h"
  36. #include <QLocale>
  37. namespace Ui {
  38. namespace {
  39. constexpr auto kSkipAtLeastOneDuration = 3 * crl::time(1000);
  40. class ShowButton final : public RpWidget {
  41. public:
  42. ShowButton(not_null<Ui::RpWidget*> parent);
  43. [[nodiscard]] rpl::producer<Qt::MouseButton> clicks() const;
  44. protected:
  45. void paintEvent(QPaintEvent *e) override;
  46. private:
  47. LinkButton _button;
  48. };
  49. ShowButton::ShowButton(not_null<Ui::RpWidget*> parent)
  50. : RpWidget(parent)
  51. , _button(this, tr::lng_usernames_activate_confirm(tr::now)) {
  52. _button.sizeValue(
  53. ) | rpl::start_with_next([=](const QSize &s) {
  54. resize(
  55. s.width() + st::defaultEmojiSuggestions.fadeRight.width(),
  56. s.height());
  57. _button.moveToRight(0, 0);
  58. }, lifetime());
  59. _button.show();
  60. }
  61. void ShowButton::paintEvent(QPaintEvent *e) {
  62. auto p = QPainter(this);
  63. const auto clip = e->rect();
  64. const auto &icon = st::defaultEmojiSuggestions.fadeRight;
  65. const auto fade = QRect(0, 0, icon.width(), height());
  66. if (fade.intersects(clip)) {
  67. icon.fill(p, fade);
  68. }
  69. const auto fill = clip.intersected(
  70. { icon.width(), 0, width() - icon.width(), height() });
  71. if (!fill.isEmpty()) {
  72. p.fillRect(fill, st::boxBg);
  73. }
  74. }
  75. rpl::producer<Qt::MouseButton> ShowButton::clicks() const {
  76. return _button.clicks();
  77. }
  78. } // namespace
  79. void TranslateBox(
  80. not_null<Ui::GenericBox*> box,
  81. not_null<PeerData*> peer,
  82. MsgId msgId,
  83. TextWithEntities text,
  84. bool hasCopyRestriction) {
  85. box->setWidth(st::boxWideWidth);
  86. box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
  87. const auto container = box->verticalLayout();
  88. struct State {
  89. State(not_null<Main::Session*> session) : api(&session->mtp()) {
  90. }
  91. MTP::Sender api;
  92. rpl::variable<LanguageId> to;
  93. };
  94. const auto state = box->lifetime().make_state<State>(&peer->session());
  95. state->to = ChooseTranslateTo(peer->owner().history(peer));
  96. if (!IsServerMsgId(msgId)) {
  97. msgId = 0;
  98. }
  99. using Flag = MTPmessages_TranslateText::Flag;
  100. const auto flags = msgId
  101. ? (Flag::f_peer | Flag::f_id)
  102. : !text.text.isEmpty()
  103. ? Flag::f_text
  104. : Flag(0);
  105. const auto &stLabel = st::aboutLabel;
  106. const auto lineHeight = stLabel.style.lineHeight;
  107. Ui::AddSkip(container);
  108. // Ui::AddSubsectionTitle(
  109. // container,
  110. // tr::lng_translate_box_original());
  111. const auto animationsPaused = [] {
  112. using Which = FlatLabel::WhichAnimationsPaused;
  113. const auto emoji = On(PowerSaving::kEmojiChat);
  114. const auto spoiler = On(PowerSaving::kChatSpoiler);
  115. return emoji
  116. ? (spoiler ? Which::All : Which::CustomEmoji)
  117. : (spoiler ? Which::Spoiler : Which::None);
  118. };
  119. const auto original = box->addRow(object_ptr<SlideWrap<FlatLabel>>(
  120. box,
  121. object_ptr<FlatLabel>(box, stLabel)));
  122. {
  123. if (hasCopyRestriction) {
  124. original->entity()->setContextMenuHook([](auto&&) {
  125. });
  126. }
  127. original->entity()->setAnimationsPausedCallback(animationsPaused);
  128. original->entity()->setMarkedText(
  129. text,
  130. Core::TextContext({ .session = &peer->session() }));
  131. original->setMinimalHeight(lineHeight);
  132. original->hide(anim::type::instant);
  133. const auto show = Ui::CreateChild<FadeWrap<ShowButton>>(
  134. container.get(),
  135. object_ptr<ShowButton>(container));
  136. show->hide(anim::type::instant);
  137. rpl::combine(
  138. container->widthValue(),
  139. original->geometryValue()
  140. ) | rpl::start_with_next([=](int width, const QRect &rect) {
  141. show->moveToLeft(
  142. width - show->width() - st::boxRowPadding.right(),
  143. rect.y() + std::abs(lineHeight - show->height()) / 2);
  144. }, show->lifetime());
  145. original->entity()->heightValue(
  146. ) | rpl::filter([](int height) {
  147. return height > 0;
  148. }) | rpl::take(1) | rpl::start_with_next([=](int height) {
  149. if (height > lineHeight) {
  150. show->show(anim::type::instant);
  151. }
  152. }, show->lifetime());
  153. show->toggleOn(show->entity()->clicks() | rpl::map_to(false));
  154. original->toggleOn(show->entity()->clicks() | rpl::map_to(true));
  155. }
  156. Ui::AddSkip(container);
  157. Ui::AddSkip(container);
  158. Ui::AddDivider(container);
  159. Ui::AddSkip(container);
  160. {
  161. const auto padding = st::defaultSubsectionTitlePadding;
  162. const auto subtitle = Ui::AddSubsectionTitle(
  163. container,
  164. state->to.value() | rpl::map(LanguageName));
  165. // Workaround.
  166. state->to.value() | rpl::start_with_next([=] {
  167. subtitle->resizeToWidth(container->width()
  168. - padding.left()
  169. - padding.right());
  170. }, subtitle->lifetime());
  171. }
  172. const auto translated = box->addRow(object_ptr<SlideWrap<FlatLabel>>(
  173. box,
  174. object_ptr<FlatLabel>(box, stLabel)));
  175. translated->entity()->setSelectable(!hasCopyRestriction);
  176. translated->entity()->setAnimationsPausedCallback(animationsPaused);
  177. constexpr auto kMaxLines = 3;
  178. container->resizeToWidth(box->width());
  179. const auto loading = box->addRow(object_ptr<SlideWrap<RpWidget>>(
  180. box,
  181. CreateLoadingTextWidget(
  182. box,
  183. st::aboutLabel,
  184. std::min(original->entity()->height() / lineHeight, kMaxLines),
  185. state->to.value() | rpl::map([=](LanguageId id) {
  186. return id.locale().textDirection() == Qt::RightToLeft;
  187. }))));
  188. const auto showText = [=](TextWithEntities text) {
  189. const auto label = translated->entity();
  190. label->setMarkedText(
  191. text,
  192. Core::TextContext({ .session = &peer->session() }));
  193. translated->show(anim::type::instant);
  194. loading->hide(anim::type::instant);
  195. };
  196. const auto send = [=](LanguageId to) {
  197. loading->show(anim::type::instant);
  198. translated->hide(anim::type::instant);
  199. state->api.request(MTPmessages_TranslateText(
  200. MTP_flags(flags),
  201. msgId ? peer->input : MTP_inputPeerEmpty(),
  202. (msgId
  203. ? MTP_vector<MTPint>(1, MTP_int(msgId))
  204. : MTPVector<MTPint>()),
  205. (msgId
  206. ? MTPVector<MTPTextWithEntities>()
  207. : MTP_vector<MTPTextWithEntities>(1, MTP_textWithEntities(
  208. MTP_string(text.text),
  209. Api::EntitiesToMTP(
  210. &peer->session(),
  211. text.entities,
  212. Api::ConvertOption::SkipLocal)))),
  213. MTP_string(to.twoLetterCode())
  214. )).done([=](const MTPmessages_TranslatedText &result) {
  215. const auto &data = result.data();
  216. const auto &list = data.vresult().v;
  217. if (list.isEmpty()) {
  218. showText(
  219. Ui::Text::Italic(tr::lng_translate_box_error(tr::now)));
  220. } else {
  221. showText(TextWithEntities{
  222. .text = qs(list.front().data().vtext()),
  223. .entities = Api::EntitiesFromMTP(
  224. &peer->session(),
  225. list.front().data().ventities().v),
  226. });
  227. }
  228. }).fail([=](const MTP::Error &error) {
  229. showText(
  230. Ui::Text::Italic(tr::lng_translate_box_error(tr::now)));
  231. }).send();
  232. };
  233. state->to.value() | rpl::start_with_next(send, box->lifetime());
  234. box->addLeftButton(tr::lng_settings_language(), [=] {
  235. if (loading->toggled()) {
  236. return;
  237. }
  238. box->uiShow()->showBox(ChooseTranslateToBox(
  239. state->to.current(),
  240. crl::guard(box, [=](LanguageId id) { state->to = id; })));
  241. });
  242. }
  243. bool SkipTranslate(TextWithEntities textWithEntities) {
  244. const auto &text = textWithEntities.text;
  245. if (text.isEmpty()) {
  246. return true;
  247. }
  248. if (!Core::App().settings().translateButtonEnabled()) {
  249. return true;
  250. }
  251. constexpr auto kFirstChunk = size_t(100);
  252. auto hasLetters = (text.size() >= kFirstChunk);
  253. for (auto i = 0; i < kFirstChunk; i++) {
  254. if (i >= text.size()) {
  255. break;
  256. }
  257. if (text.at(i).isLetter()) {
  258. hasLetters = true;
  259. break;
  260. }
  261. }
  262. if (!hasLetters) {
  263. return true;
  264. }
  265. #ifndef TDESKTOP_DISABLE_SPELLCHECK
  266. const auto result = Platform::Language::Recognize(text);
  267. const auto skip = Core::App().settings().skipTranslationLanguages();
  268. return result.known() && ranges::contains(skip, result);
  269. #else
  270. return false;
  271. #endif
  272. }
  273. object_ptr<BoxContent> EditSkipTranslationLanguages() {
  274. auto title = tr::lng_translate_settings_choose();
  275. const auto selected = std::make_shared<std::vector<LanguageId>>(
  276. Core::App().settings().skipTranslationLanguages());
  277. const auto weak = std::make_shared<QPointer<BoxContent>>();
  278. const auto check = [=](LanguageId id) {
  279. const auto already = ranges::contains(*selected, id);
  280. if (already) {
  281. selected->erase(ranges::remove(*selected, id), selected->end());
  282. } else {
  283. selected->push_back(id);
  284. }
  285. if (already && selected->empty()) {
  286. if (const auto strong = weak->data()) {
  287. strong->showToast(
  288. tr::lng_translate_settings_one(tr::now),
  289. kSkipAtLeastOneDuration);
  290. }
  291. return false;
  292. }
  293. return true;
  294. };
  295. auto result = Box(ChooseLanguageBox, std::move(title), [=](
  296. std::vector<LanguageId> &&list) {
  297. Core::App().settings().setSkipTranslationLanguages(
  298. std::move(list));
  299. Core::App().saveSettingsDelayed();
  300. }, *selected, true, check);
  301. *weak = result.data();
  302. return result;
  303. }
  304. object_ptr<BoxContent> ChooseTranslateToBox(
  305. LanguageId bringUp,
  306. Fn<void(LanguageId)> callback) {
  307. auto &settings = Core::App().settings();
  308. auto selected = std::vector<LanguageId>{
  309. settings.translateTo(),
  310. };
  311. for (const auto &id : settings.skipTranslationLanguages()) {
  312. if (id != selected.front()) {
  313. selected.push_back(id);
  314. }
  315. }
  316. if (bringUp && ranges::contains(selected, bringUp)) {
  317. selected.push_back(bringUp);
  318. }
  319. return Box(ChooseLanguageBox, tr::lng_languages(), [=](
  320. const std::vector<LanguageId> &ids) {
  321. Expects(!ids.empty());
  322. const auto id = ids.front();
  323. Core::App().settings().setTranslateTo(id);
  324. Core::App().saveSettingsDelayed();
  325. callback(id);
  326. }, selected, false, nullptr);
  327. }
  328. LanguageId ChooseTranslateTo(not_null<History*> history) {
  329. return ChooseTranslateTo(history->translateOfferedFrom());
  330. }
  331. LanguageId ChooseTranslateTo(LanguageId offeredFrom) {
  332. auto &settings = Core::App().settings();
  333. return ChooseTranslateTo(
  334. offeredFrom,
  335. settings.translateTo(),
  336. settings.skipTranslationLanguages());
  337. }
  338. LanguageId ChooseTranslateTo(
  339. not_null<History*> history,
  340. LanguageId savedTo,
  341. const std::vector<LanguageId> &skip) {
  342. return ChooseTranslateTo(history->translateOfferedFrom(), savedTo, skip);
  343. }
  344. LanguageId ChooseTranslateTo(
  345. LanguageId offeredFrom,
  346. LanguageId savedTo,
  347. const std::vector<LanguageId> &skip) {
  348. return (offeredFrom != savedTo) ? savedTo : skip.front();
  349. }
  350. } // namespace Ui