chat_search_in.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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 "dialogs/ui/chat_search_in.h"
  8. #include "lang/lang_keys.h"
  9. #include "ui/effects/ripple_animation.h"
  10. #include "ui/text/text_utilities.h"
  11. #include "ui/widgets/buttons.h"
  12. #include "ui/widgets/popup_menu.h"
  13. #include "ui/widgets/shadow.h"
  14. #include "ui/widgets/menu/menu_item_base.h"
  15. #include "ui/dynamic_image.h"
  16. #include "ui/painter.h"
  17. #include "styles/style_dialogs.h"
  18. #include "styles/style_window.h"
  19. namespace Dialogs {
  20. namespace {
  21. class Action final : public Ui::Menu::ItemBase {
  22. public:
  23. Action(
  24. not_null<Ui::PopupMenu*> parentMenu,
  25. std::shared_ptr<Ui::DynamicImage> icon,
  26. const QString &label,
  27. bool chosen);
  28. ~Action();
  29. bool isEnabled() const override;
  30. not_null<QAction*> action() const override;
  31. void handleKeyPress(not_null<QKeyEvent*> e) override;
  32. protected:
  33. QPoint prepareRippleStartPosition() const override;
  34. QImage prepareRippleMask() const override;
  35. int contentHeight() const override;
  36. private:
  37. void paint(Painter &p);
  38. void resolveMinWidth();
  39. void refreshDimensions();
  40. const not_null<Ui::PopupMenu*> _parentMenu;
  41. const not_null<QAction*> _dummyAction;
  42. const style::Menu &_st;
  43. const int _height = 0;
  44. std::shared_ptr<Ui::DynamicImage> _icon;
  45. Ui::Text::String _text;
  46. bool _checked = false;
  47. };
  48. [[nodiscard]] QString TabLabel(
  49. ChatSearchTab tab,
  50. ChatSearchPeerTabType type = {}) {
  51. switch (tab) {
  52. case ChatSearchTab::MyMessages:
  53. return tr::lng_search_tab_my_messages(tr::now);
  54. case ChatSearchTab::ThisTopic:
  55. return tr::lng_search_tab_this_topic(tr::now);
  56. case ChatSearchTab::ThisPeer:
  57. switch (type) {
  58. case ChatSearchPeerTabType::Chat:
  59. return tr::lng_search_tab_this_chat(tr::now);
  60. case ChatSearchPeerTabType::Channel:
  61. return tr::lng_search_tab_this_channel(tr::now);
  62. case ChatSearchPeerTabType::Group:
  63. return tr::lng_search_tab_this_group(tr::now);
  64. }
  65. Unexpected("Type in Dialogs::TabLabel.");
  66. case ChatSearchTab::PublicPosts:
  67. return tr::lng_search_tab_public_posts(tr::now);
  68. }
  69. Unexpected("Tab in Dialogs::TabLabel.");
  70. }
  71. Action::Action(
  72. not_null<Ui::PopupMenu*> parentMenu,
  73. std::shared_ptr<Ui::DynamicImage> icon,
  74. const QString &label,
  75. bool chosen)
  76. : ItemBase(parentMenu->menu(), parentMenu->menu()->st())
  77. , _parentMenu(parentMenu)
  78. , _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
  79. , _st(parentMenu->menu()->st())
  80. , _height(st::dialogsSearchInHeight)
  81. , _icon(std::move(icon))
  82. , _checked(chosen) {
  83. const auto parent = parentMenu->menu();
  84. _text.setText(st::semiboldTextStyle, label);
  85. _icon->subscribeToUpdates([=] { update(); });
  86. initResizeHook(parent->sizeValue());
  87. resolveMinWidth();
  88. paintRequest(
  89. ) | rpl::start_with_next([=] {
  90. Painter p(this);
  91. paint(p);
  92. }, lifetime());
  93. enableMouseSelecting();
  94. }
  95. Action::~Action() {
  96. _icon->subscribeToUpdates(nullptr);
  97. }
  98. void Action::resolveMinWidth() {
  99. const auto maxWidth = st::dialogsSearchInPhotoPadding
  100. + st::dialogsSearchInPhotoSize
  101. + st::dialogsSearchInSkip
  102. + _text.maxWidth()
  103. + st::dialogsSearchInCheckSkip
  104. + st::dialogsSearchInCheck.width()
  105. + st::dialogsSearchInCheckSkip;
  106. setMinWidth(maxWidth);
  107. }
  108. void Action::paint(Painter &p) {
  109. const auto enabled = isEnabled();
  110. const auto selected = isSelected();
  111. if (selected && _st.itemBgOver->c.alpha() < 255) {
  112. p.fillRect(0, 0, width(), _height, _st.itemBg);
  113. }
  114. const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
  115. p.fillRect(0, 0, width(), _height, bg);
  116. if (enabled) {
  117. paintRipple(p, 0, 0);
  118. }
  119. auto x = st::dialogsSearchInPhotoPadding;
  120. const auto photos = st::dialogsSearchInPhotoSize;
  121. const auto photoy = (height() - photos) / 2;
  122. p.drawImage(QRect{ x, photoy, photos, photos }, _icon->image(photos));
  123. x += photos + st::dialogsSearchInSkip;
  124. const auto available = width()
  125. - x
  126. - st::dialogsSearchInCheckSkip
  127. - st::dialogsSearchInCheck.width()
  128. - st::dialogsSearchInCheckSkip;
  129. p.setPen(!enabled
  130. ? _st.itemFgDisabled
  131. : selected
  132. ? _st.itemFgOver
  133. : _st.itemFg);
  134. _text.drawLeftElided(
  135. p,
  136. x,
  137. st::dialogsSearchInNameTop,
  138. available,
  139. width());
  140. x += available;
  141. if (_checked) {
  142. x += st::dialogsSearchInCheckSkip;
  143. const auto &icon = st::dialogsSearchInCheck;
  144. const auto icony = (height() - icon.height()) / 2;
  145. icon.paint(p, x, icony, width());
  146. }
  147. }
  148. bool Action::isEnabled() const {
  149. return true;
  150. }
  151. not_null<QAction*> Action::action() const {
  152. return _dummyAction;
  153. }
  154. QPoint Action::prepareRippleStartPosition() const {
  155. return mapFromGlobal(QCursor::pos());
  156. }
  157. QImage Action::prepareRippleMask() const {
  158. return Ui::RippleAnimation::RectMask(size());
  159. }
  160. int Action::contentHeight() const {
  161. return _height;
  162. }
  163. void Action::handleKeyPress(not_null<QKeyEvent*> e) {
  164. if (!isSelected()) {
  165. return;
  166. }
  167. const auto key = e->key();
  168. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  169. setClicked(Ui::Menu::TriggeredSource::Keyboard);
  170. }
  171. }
  172. } // namespace
  173. FixedHashtagSearchQuery FixHashtagSearchQuery(
  174. const QString &query,
  175. int cursorPosition,
  176. HashOrCashtag tag) {
  177. const auto trimmed = query.trimmed();
  178. const auto hash = int(trimmed.isEmpty()
  179. ? query.size()
  180. : query.indexOf(trimmed));
  181. const auto start = std::min(cursorPosition, hash);
  182. const auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#');
  183. auto result = query.mid(0, start);
  184. for (const auto &ch : query.mid(start)) {
  185. if (ch.isSpace()) {
  186. if (cursorPosition > result.size()) {
  187. --cursorPosition;
  188. }
  189. continue;
  190. } else if (result.size() == start) {
  191. result += first;
  192. if (ch != first) {
  193. ++cursorPosition;
  194. }
  195. }
  196. if (ch != first) {
  197. result += ch;
  198. }
  199. }
  200. if (result.size() == start) {
  201. result += first;
  202. ++cursorPosition;
  203. }
  204. return { result, cursorPosition };
  205. }
  206. HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) {
  207. const auto trimmed = query.trimmed();
  208. const auto first = trimmed.isEmpty() ? QChar() : trimmed[0];
  209. if (first == '#') {
  210. for (const auto &ch : trimmed) {
  211. if (ch.isSpace()) {
  212. return HashOrCashtag::None;
  213. }
  214. }
  215. return HashOrCashtag::Hashtag;
  216. } else if (first == '$') {
  217. for (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) {
  218. if ((*it) < 'A' || (*it) > 'Z') {
  219. return HashOrCashtag::None;
  220. }
  221. }
  222. return HashOrCashtag::Cashtag;
  223. }
  224. return HashOrCashtag::None;
  225. }
  226. void ChatSearchIn::Section::update() {
  227. outer->update();
  228. }
  229. ChatSearchIn::ChatSearchIn(QWidget *parent)
  230. : RpWidget(parent) {
  231. _in.clicks.events() | rpl::start_with_next([=] {
  232. showMenu();
  233. }, lifetime());
  234. }
  235. ChatSearchIn::~ChatSearchIn() = default;
  236. void ChatSearchIn::apply(
  237. std::vector<PossibleTab> tabs,
  238. ChatSearchTab active,
  239. ChatSearchPeerTabType peerTabType,
  240. std::shared_ptr<Ui::DynamicImage> fromUserpic,
  241. QString fromName) {
  242. _tabs = std::move(tabs);
  243. _peerTabType = peerTabType;
  244. _active = active;
  245. const auto i = ranges::find(_tabs, active, &PossibleTab::tab);
  246. Assert(i != end(_tabs));
  247. Assert(i->icon != nullptr);
  248. updateSection(
  249. &_in,
  250. i->icon->clone(),
  251. Ui::Text::Semibold(TabLabel(active, peerTabType)));
  252. auto text = tr::lng_dlg_search_from(
  253. tr::now,
  254. lt_user,
  255. Ui::Text::Semibold(fromName),
  256. Ui::Text::WithEntities);
  257. updateSection(&_from, std::move(fromUserpic), std::move(text));
  258. resizeToWidth(width());
  259. }
  260. rpl::producer<> ChatSearchIn::cancelInRequests() const {
  261. return _in.cancelRequests.events();
  262. }
  263. rpl::producer<> ChatSearchIn::cancelFromRequests() const {
  264. return _from.cancelRequests.events();
  265. }
  266. rpl::producer<> ChatSearchIn::changeFromRequests() const {
  267. return _from.clicks.events();
  268. }
  269. rpl::producer<ChatSearchTab> ChatSearchIn::tabChanges() const {
  270. return _active.changes();
  271. }
  272. void ChatSearchIn::showMenu() {
  273. _menu = base::make_unique_q<Ui::PopupMenu>(
  274. this,
  275. st::dialogsSearchInMenu);
  276. const auto active = _active.current();
  277. auto activeIndex = 0;
  278. for (const auto &tab : _tabs) {
  279. if (!tab.icon) {
  280. continue;
  281. }
  282. const auto value = tab.tab;
  283. if (value == active) {
  284. activeIndex = _menu->actions().size();
  285. }
  286. auto action = base::make_unique_q<Action>(
  287. _menu.get(),
  288. tab.icon,
  289. TabLabel(value, _peerTabType),
  290. (value == active));
  291. action->setClickedCallback([=] {
  292. _active = value;
  293. });
  294. _menu->addAction(std::move(action));
  295. }
  296. const auto count = int(_menu->actions().size());
  297. const auto bottomLeft = (activeIndex * 2 >= count);
  298. const auto single = st::dialogsSearchInHeight;
  299. const auto in = mapToGlobal(_in.outer->pos()
  300. + QPoint(0, bottomLeft ? count * single : 0));
  301. _menu->setForcedOrigin(bottomLeft
  302. ? Ui::PanelAnimation::Origin::BottomLeft
  303. : Ui::PanelAnimation::Origin::TopLeft);
  304. if (_menu->prepareGeometryFor(in)) {
  305. _menu->move(_menu->pos() - QPoint(_menu->inner().x(), activeIndex * single));
  306. _menu->popupPrepared();
  307. }
  308. }
  309. void ChatSearchIn::paintEvent(QPaintEvent *e) {
  310. auto p = Painter(this);
  311. const auto top = QRect(0, 0, width(), st::searchedBarHeight);
  312. p.fillRect(top, st::searchedBarBg);
  313. p.fillRect(rect().translated(0, st::searchedBarHeight), st::dialogsBg);
  314. p.setFont(st::searchedBarFont);
  315. p.setPen(st::searchedBarFg);
  316. p.drawTextLeft(
  317. st::searchedBarPosition.x(),
  318. st::searchedBarPosition.y(),
  319. width(),
  320. tr::lng_dlg_search_in(tr::now));
  321. }
  322. int ChatSearchIn::resizeGetHeight(int newWidth) {
  323. auto result = st::searchedBarHeight;
  324. if (const auto raw = _in.outer.get()) {
  325. raw->resizeToWidth(newWidth);
  326. raw->move(0, result);
  327. result += raw->height();
  328. _in.shadow->setGeometry(0, result, newWidth, st::lineWidth);
  329. result += st::lineWidth;
  330. }
  331. if (const auto raw = _from.outer.get()) {
  332. raw->resizeToWidth(newWidth);
  333. raw->move(0, result);
  334. result += raw->height();
  335. _from.shadow->setGeometry(0, result, newWidth, st::lineWidth);
  336. result += st::lineWidth;
  337. }
  338. return result;
  339. }
  340. void ChatSearchIn::updateSection(
  341. not_null<Section*> section,
  342. std::shared_ptr<Ui::DynamicImage> image,
  343. TextWithEntities text) {
  344. if (section->subscribed) {
  345. section->image->subscribeToUpdates(nullptr);
  346. section->subscribed = false;
  347. }
  348. if (!image) {
  349. if (section->outer) {
  350. section->cancel = nullptr;
  351. section->shadow = nullptr;
  352. section->outer = nullptr;
  353. section->subscribed = false;
  354. }
  355. return;
  356. } else if (!section->outer) {
  357. auto button = std::make_unique<Ui::AbstractButton>(this);
  358. const auto raw = button.get();
  359. section->outer = std::move(button);
  360. raw->resize(
  361. st::columnMinimalWidthLeft,
  362. st::dialogsSearchInHeight);
  363. raw->paintRequest() | rpl::start_with_next([=] {
  364. auto p = QPainter(raw);
  365. if (!section->subscribed) {
  366. section->subscribed = true;
  367. section->image->subscribeToUpdates([=] {
  368. raw->update();
  369. });
  370. }
  371. const auto outer = raw->width();
  372. const auto size = st::dialogsSearchInPhotoSize;
  373. const auto left = st::dialogsSearchInPhotoPadding;
  374. const auto top = (st::dialogsSearchInHeight - size) / 2;
  375. p.drawImage(
  376. QRect{ left, top, size, size },
  377. section->image->image(size));
  378. const auto x = left + size + st::dialogsSearchInSkip;
  379. const auto available = outer
  380. - st::dialogsSearchInSkip
  381. - section->cancel->width()
  382. - 2 * st::dialogsSearchInDownSkip
  383. - st::dialogsSearchInDown.width()
  384. - x;
  385. const auto use = std::min(section->text.maxWidth(), available);
  386. const auto iconx = x + use + st::dialogsSearchInDownSkip;
  387. const auto icony = st::dialogsSearchInDownTop;
  388. st::dialogsSearchInDown.paint(p, iconx, icony, outer);
  389. p.setPen(st::windowBoldFg);
  390. section->text.draw(p, {
  391. .position = QPoint(x, st::dialogsSearchInNameTop),
  392. .outerWidth = outer,
  393. .availableWidth = available,
  394. .elisionLines = 1,
  395. });
  396. }, raw->lifetime());
  397. section->shadow = std::make_unique<Ui::PlainShadow>(this);
  398. section->shadow->show();
  399. const auto st = &st::dialogsCancelSearchInPeer;
  400. section->cancel = std::make_unique<Ui::IconButton>(raw, *st);
  401. section->cancel->show();
  402. raw->sizeValue() | rpl::start_with_next([=](QSize size) {
  403. const auto left = size.width() - section->cancel->width();
  404. const auto top = (size.height() - st->height) / 2;
  405. section->cancel->moveToLeft(left, top);
  406. }, section->cancel->lifetime());
  407. section->cancel->clicks() | rpl::to_empty | rpl::start_to_stream(
  408. section->cancelRequests,
  409. section->cancel->lifetime());
  410. raw->clicks() | rpl::to_empty | rpl::start_to_stream(
  411. section->clicks,
  412. raw->lifetime());
  413. raw->show();
  414. }
  415. section->image = std::move(image);
  416. section->text.setMarkedText(st::dialogsSearchFromStyle, std::move(text));
  417. }
  418. } // namespace Dialogs