attach_bot_downloads.cpp 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 "ui/chat/attach/attach_bot_downloads.h"
  8. #include "lang/lang_keys.h"
  9. #include "ui/widgets/menu/menu_item_base.h"
  10. #include "ui/widgets/labels.h"
  11. #include "ui/widgets/popup_menu.h"
  12. #include "ui/effects/ripple_animation.h"
  13. #include "ui/text/format_values.h"
  14. #include "ui/text/text_utilities.h"
  15. #include "ui/painter.h"
  16. #include "styles/style_chat.h"
  17. namespace Ui::BotWebView {
  18. namespace {
  19. class Action final : public Menu::ItemBase {
  20. public:
  21. Action(
  22. not_null<RpWidget*> parent,
  23. const DownloadsEntry &entry,
  24. Fn<void(DownloadsAction)> callback);
  25. bool isEnabled() const override;
  26. not_null<QAction*> action() const override { return _dummyAction; }
  27. void handleKeyPress(not_null<QKeyEvent*> e) override;
  28. void refresh(const DownloadsEntry &entry);
  29. private:
  30. QPoint prepareRippleStartPosition() const override {
  31. return mapFromGlobal(QCursor::pos());
  32. }
  33. QImage prepareRippleMask() const override {
  34. return Ui::RippleAnimation::RectMask(size());
  35. }
  36. int contentHeight() const override { return _height; }
  37. void prepare();
  38. void paint(Painter &p);
  39. const not_null<QAction*> _dummyAction;
  40. const style::Menu &_st = st::defaultMenu;
  41. DownloadsEntry _entry;
  42. Text::String _name;
  43. FlatLabel _progress;
  44. IconButton _cancel;
  45. int _textWidth = 0;
  46. const int _height;
  47. };
  48. Action::Action(
  49. not_null<RpWidget*> parent,
  50. const DownloadsEntry &entry,
  51. Fn<void(DownloadsAction)> callback)
  52. : ItemBase(parent, st::defaultMenu)
  53. , _dummyAction(new QAction(parent))
  54. , _progress(this, st::botDownloadProgress)
  55. , _cancel(this, st::botDownloadCancel)
  56. , _height(st::ttlItemPadding.top()
  57. + _st.itemStyle.font->height
  58. + st::ttlItemTimerFont->height
  59. + st::ttlItemPadding.bottom()) {
  60. setAcceptBoth(true);
  61. initResizeHook(parent->sizeValue());
  62. setClickedCallback([=] {
  63. if (isEnabled()) {
  64. callback(DownloadsAction::Open);
  65. }
  66. });
  67. _cancel.setClickedCallback([=] {
  68. callback(DownloadsAction::Cancel);
  69. });
  70. paintRequest(
  71. ) | rpl::start_with_next([=] {
  72. Painter p(this);
  73. paint(p);
  74. }, lifetime());
  75. widthValue() | rpl::start_with_next([=](int width) {
  76. _progress.moveToLeft(
  77. _st.itemPadding.left(),
  78. st::ttlItemPadding.top() + _st.itemStyle.font->height,
  79. width);
  80. _cancel.moveToRight(
  81. _st.itemPadding.right(),
  82. (_height - _cancel.height()) / 2,
  83. width);
  84. }, lifetime());
  85. _progress.setClickHandlerFilter([=](const auto &...) {
  86. callback(DownloadsAction::Retry);
  87. return false;
  88. });
  89. enableMouseSelecting();
  90. refresh(entry);
  91. }
  92. void Action::paint(Painter &p) {
  93. const auto selected = isSelected();
  94. if (selected && _st.itemBgOver->c.alpha() < 255) {
  95. p.fillRect(0, 0, width(), _height, _st.itemBg);
  96. }
  97. p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
  98. if (isEnabled()) {
  99. paintRipple(p, 0, 0);
  100. }
  101. p.setPen(selected ? _st.itemFgOver : _st.itemFg);
  102. _name.drawLeftElided(
  103. p,
  104. _st.itemPadding.left(),
  105. st::ttlItemPadding.top(),
  106. _textWidth,
  107. width());
  108. _progress.setTextColorOverride(
  109. selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
  110. }
  111. void Action::prepare() {
  112. const auto filenameWidth = _name.maxWidth();
  113. const auto progressWidth = _progress.textMaxWidth();
  114. const auto &padding = _st.itemPadding;
  115. const auto goodWidth = std::max(filenameWidth, progressWidth);
  116. // Example max width: "4000 / 4000 MB"
  117. const auto countWidth = [&](const QString &text) {
  118. return st::ttlItemTimerFont->width(text);
  119. };
  120. const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
  121. tr::now,
  122. lt_ready,
  123. "0000",
  124. lt_total,
  125. "0000",
  126. lt_mb,
  127. "MB"));
  128. const auto maxStartingWidth = countWidth(
  129. tr::lng_bot_download_starting(tr::now));
  130. const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
  131. tr::now,
  132. lt_retry,
  133. tr::lng_bot_download_retry(tr::now)));
  134. const auto cancel = _cancel.width() + padding.right();
  135. const auto paddings = padding.left() + padding.right() + cancel;
  136. const auto w = std::clamp(
  137. paddings + std::max({
  138. goodWidth,
  139. maxProgressWidth,
  140. maxStartingWidth,
  141. maxFailedWidth,
  142. }),
  143. _st.widthMin,
  144. _st.widthMax);
  145. _textWidth = w - paddings;
  146. _progress.resizeToWidth(_textWidth);
  147. setMinWidth(w);
  148. update();
  149. }
  150. bool Action::isEnabled() const {
  151. return _entry.total > 0 && _entry.ready == _entry.total;
  152. }
  153. void Action::handleKeyPress(not_null<QKeyEvent*> e) {
  154. if (!isSelected()) {
  155. return;
  156. }
  157. const auto key = e->key();
  158. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  159. setClicked(Menu::TriggeredSource::Keyboard);
  160. }
  161. }
  162. void Action::refresh(const DownloadsEntry &entry) {
  163. _entry = entry;
  164. const auto filename = entry.path.split('/').last();
  165. _name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
  166. const auto progressText = (entry.total && entry.total == entry.ready)
  167. ? TextWithEntities{ FormatSizeText(entry.total) }
  168. : entry.loading
  169. ? (entry.total
  170. ? TextWithEntities{
  171. FormatProgressText(entry.ready, entry.total),
  172. }
  173. : tr::lng_bot_download_starting(tr::now, Text::WithEntities))
  174. : tr::lng_bot_download_failed(
  175. tr::now,
  176. lt_retry,
  177. Text::Link(tr::lng_bot_download_retry(tr::now)),
  178. Text::WithEntities);
  179. _progress.setMarkedText(progressText);
  180. const auto enabled = isEnabled();
  181. setCursor(enabled ? style::cur_pointer : style::cur_default);
  182. _cancel.setVisible(!enabled && _entry.loading);
  183. _progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
  184. prepare();
  185. }
  186. } // namespace
  187. FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
  188. rpl::producer<std::vector<DownloadsEntry>> content,
  189. Fn<void(uint32, DownloadsAction)> callback) {
  190. return [callback, moved = std::move(content)](
  191. not_null<PopupMenu*> menu) mutable {
  192. struct Row {
  193. not_null<Action*> action;
  194. uint32 id = 0;
  195. };
  196. struct State {
  197. std::vector<Row> rows;
  198. };
  199. const auto state = menu->lifetime().make_state<State>();
  200. std::move(
  201. moved
  202. ) | rpl::start_with_next([=](
  203. const std::vector<DownloadsEntry> &entries) {
  204. auto found = base::flat_set<uint32>();
  205. for (const auto &entry : entries | ranges::views::reverse) {
  206. const auto id = entry.id;
  207. const auto path = entry.path;
  208. const auto i = ranges::find(state->rows, id, &Row::id);
  209. found.emplace(id);
  210. if (i != end(state->rows)) {
  211. i->action->refresh(entry);
  212. } else {
  213. auto action = base::make_unique_q<Action>(
  214. menu,
  215. entry,
  216. [=](DownloadsAction type) { callback(id, type); });
  217. state->rows.push_back({
  218. .action = action.get(),
  219. .id = id,
  220. });
  221. menu->addAction(std::move(action));
  222. }
  223. }
  224. for (auto i = begin(state->rows); i != end(state->rows);) {
  225. if (!found.contains(i->id)) {
  226. menu->removeAction(i - begin(state->rows));
  227. i = state->rows.erase(i);
  228. } else {
  229. ++i;
  230. }
  231. }
  232. }, menu->lifetime());
  233. };
  234. }
  235. } // namespace Ui::BotWebView