tabbed_panel.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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 "chat_helpers/tabbed_panel.h"
  8. #include "ui/widgets/shadow.h"
  9. #include "ui/image/image_prepare.h"
  10. #include "ui/ui_utility.h"
  11. #include "chat_helpers/tabbed_selector.h"
  12. #include "window/window_session_controller.h"
  13. #include "mainwindow.h"
  14. #include "core/application.h"
  15. #include "base/options.h"
  16. #include "styles/style_chat_helpers.h"
  17. namespace ChatHelpers {
  18. namespace {
  19. constexpr auto kHideTimeoutMs = 300;
  20. constexpr auto kDelayedHideTimeoutMs = 3000;
  21. base::options::toggle TabbedPanelShowOnClick({
  22. .id = kOptionTabbedPanelShowOnClick,
  23. .name = "Show tabbed panel by click",
  24. .description = "Show Emoji / Stickers / GIFs panel only after a click.",
  25. });
  26. } // namespace
  27. const char kOptionTabbedPanelShowOnClick[] = "tabbed-panel-show-on-click";
  28. bool ShowPanelOnClick() {
  29. return TabbedPanelShowOnClick.value();
  30. }
  31. TabbedPanel::TabbedPanel(
  32. QWidget *parent,
  33. not_null<Window::SessionController*> controller,
  34. not_null<TabbedSelector*> selector)
  35. : TabbedPanel(parent, {
  36. .regularWindow = controller,
  37. .nonOwnedSelector = selector,
  38. }) {
  39. }
  40. TabbedPanel::TabbedPanel(
  41. QWidget *parent,
  42. not_null<Window::SessionController*> controller,
  43. object_ptr<TabbedSelector> selector)
  44. : TabbedPanel(parent, {
  45. .regularWindow = controller,
  46. .ownedSelector = std::move(selector),
  47. }) {
  48. }
  49. TabbedPanel::TabbedPanel(
  50. QWidget *parent,
  51. TabbedPanelDescriptor &&descriptor)
  52. : RpWidget(parent)
  53. , _regularWindow(descriptor.regularWindow)
  54. , _ownedSelector(std::move(descriptor.ownedSelector))
  55. , _selector(descriptor.nonOwnedSelector
  56. ? descriptor.nonOwnedSelector
  57. : _ownedSelector.data())
  58. , _heightRatio(st::emojiPanHeightRatio)
  59. , _minContentHeight(st::emojiPanMinHeight)
  60. , _maxContentHeight(st::emojiPanMaxHeight) {
  61. Expects(_selector != nullptr);
  62. _selector->setParent(this);
  63. _selector->setRoundRadius(st::emojiPanRadius);
  64. _selector->setAfterShownCallback([=](SelectorTab tab) {
  65. if (_regularWindow) {
  66. _regularWindow->enableGifPauseReason(_selector->level());
  67. }
  68. _pauseAnimations.fire(true);
  69. });
  70. _selector->setBeforeHidingCallback([=](SelectorTab tab) {
  71. if (_regularWindow) {
  72. _regularWindow->disableGifPauseReason(_selector->level());
  73. }
  74. _pauseAnimations.fire(false);
  75. });
  76. _selector->showRequests(
  77. ) | rpl::start_with_next([=] {
  78. showFromSelector();
  79. }, lifetime());
  80. resize(
  81. QRect(0, 0, st::emojiPanWidth, st::emojiPanMaxHeight).marginsAdded(
  82. innerPadding()).size());
  83. _contentMaxHeight = st::emojiPanMaxHeight;
  84. _contentHeight = _contentMaxHeight;
  85. _selector->resize(st::emojiPanWidth, _contentHeight);
  86. _selector->move(innerRect().topLeft());
  87. _hideTimer.setCallback([this] { hideByTimerOrLeave(); });
  88. _selector->checkForHide(
  89. ) | rpl::start_with_next([=] {
  90. if (!rect().contains(mapFromGlobal(QCursor::pos()))) {
  91. _hideTimer.callOnce(kDelayedHideTimeoutMs);
  92. }
  93. }, lifetime());
  94. _selector->cancelled(
  95. ) | rpl::start_with_next([=] {
  96. hideAnimated();
  97. }, lifetime());
  98. _selector->slideFinished(
  99. ) | rpl::start_with_next([=] {
  100. InvokeQueued(this, [=] {
  101. if (_hideAfterSlide) {
  102. startOpacityAnimation(true);
  103. }
  104. });
  105. }, lifetime());
  106. macWindowDeactivateEvents(
  107. ) | rpl::filter([=] {
  108. return !isHidden() && !preventAutoHide();
  109. }) | rpl::start_with_next([=] {
  110. hideAnimated();
  111. }, lifetime());
  112. setAttribute(Qt::WA_OpaquePaintEvent, false);
  113. hideChildren();
  114. hide();
  115. }
  116. not_null<TabbedSelector*> TabbedPanel::selector() const {
  117. return _selector;
  118. }
  119. rpl::producer<bool> TabbedPanel::pauseAnimations() const {
  120. return _pauseAnimations.events();
  121. }
  122. bool TabbedPanel::isSelectorStolen() const {
  123. return (_selector->parent() != this);
  124. }
  125. void TabbedPanel::moveBottomRight(int bottom, int right) {
  126. const auto isNew = (_bottom != bottom || _right != right);
  127. _bottom = bottom;
  128. _right = right;
  129. // If the panel is already shown, update the position.
  130. if (!isHidden() && isNew) {
  131. moveHorizontally();
  132. } else {
  133. updateContentHeight();
  134. }
  135. }
  136. void TabbedPanel::moveTopRight(int top, int right) {
  137. const auto isNew = (_top != top || _right != right);
  138. _top = top;
  139. _right = right;
  140. // If the panel is already shown, update the position.
  141. if (!isHidden() && isNew) {
  142. moveHorizontally();
  143. } else {
  144. updateContentHeight();
  145. }
  146. }
  147. void TabbedPanel::setDesiredHeightValues(
  148. float64 ratio,
  149. int minHeight,
  150. int maxHeight) {
  151. _heightRatio = ratio;
  152. _minContentHeight = minHeight;
  153. _maxContentHeight = maxHeight;
  154. updateContentHeight();
  155. }
  156. void TabbedPanel::setDropDown(bool dropDown) {
  157. selector()->setDropDown(dropDown);
  158. _dropDown = dropDown;
  159. }
  160. void TabbedPanel::updateContentHeight() {
  161. auto addedHeight = innerPadding().top() + innerPadding().bottom();
  162. auto marginsHeight = _selector->marginTop() + _selector->marginBottom();
  163. auto availableHeight = _dropDown
  164. ? (parentWidget()->height() - _top - marginsHeight)
  165. : (_bottom - marginsHeight);
  166. auto wantedContentHeight = qRound(_heightRatio * availableHeight)
  167. - addedHeight;
  168. auto contentHeight = marginsHeight + std::clamp(
  169. wantedContentHeight,
  170. _minContentHeight,
  171. _maxContentHeight);
  172. auto resultTop = _dropDown
  173. ? _top
  174. : (_bottom - addedHeight - contentHeight);
  175. if (contentHeight == _contentHeight) {
  176. move(x(), resultTop);
  177. return;
  178. }
  179. _contentHeight = contentHeight;
  180. resize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());
  181. move(x(), resultTop);
  182. _selector->resize(innerRect().width(), _contentHeight);
  183. update();
  184. }
  185. void TabbedPanel::paintEvent(QPaintEvent *e) {
  186. auto p = QPainter(this);
  187. // This call can finish _a_show animation and destroy _showAnimation.
  188. auto opacityAnimating = _a_opacity.animating();
  189. auto showAnimating = _a_show.animating();
  190. if (_showAnimation && !showAnimating) {
  191. _showAnimation.reset();
  192. if (!opacityAnimating) {
  193. showChildren();
  194. _selector->afterShown();
  195. }
  196. }
  197. if (showAnimating) {
  198. Assert(_showAnimation != nullptr);
  199. if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
  200. _showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
  201. }
  202. } else if (opacityAnimating) {
  203. p.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));
  204. p.drawPixmap(0, 0, _cache);
  205. } else if (_hiding || isHidden()) {
  206. hideFinished();
  207. } else {
  208. if (!_cache.isNull()) _cache = QPixmap();
  209. Ui::Shadow::paint(p, innerRect(), width(), _selector->st().showAnimation.shadow);
  210. }
  211. }
  212. void TabbedPanel::moveHorizontally() {
  213. const auto padding = innerPadding();
  214. const auto width = innerRect().width() + padding.left() + padding.right();
  215. const auto right = std::max(
  216. parentWidget()->width() - std::max(_right, width),
  217. 0);
  218. moveToRight(right, y());
  219. updateContentHeight();
  220. }
  221. void TabbedPanel::enterEventHook(QEnterEvent *e) {
  222. Core::App().registerLeaveSubscription(this);
  223. showAnimated();
  224. }
  225. bool TabbedPanel::preventAutoHide() const {
  226. return _selector->preventAutoHide();
  227. }
  228. void TabbedPanel::leaveEventHook(QEvent *e) {
  229. Core::App().unregisterLeaveSubscription(this);
  230. if (preventAutoHide()) {
  231. return;
  232. }
  233. if (_a_show.animating() || _a_opacity.animating()) {
  234. hideAnimated();
  235. } else {
  236. _hideTimer.callOnce(kHideTimeoutMs);
  237. }
  238. return TWidget::leaveEventHook(e);
  239. }
  240. void TabbedPanel::otherEnter() {
  241. showAnimated();
  242. }
  243. void TabbedPanel::otherLeave() {
  244. if (preventAutoHide()) {
  245. return;
  246. }
  247. if (_a_opacity.animating()) {
  248. hideByTimerOrLeave();
  249. } else {
  250. // In case of animations disabled add some delay before hiding.
  251. // Otherwise if emoji suggestions panel is shown in between
  252. // (z-order wise) the emoji toggle button and tabbed panel,
  253. // we won't be able to move cursor from the button to the panel.
  254. _hideTimer.callOnce(anim::Disabled() ? kHideTimeoutMs : 0);
  255. }
  256. }
  257. void TabbedPanel::hideFast() {
  258. if (isHidden()) return;
  259. if (_selector && !_selector->isHidden()) {
  260. _selector->beforeHiding();
  261. }
  262. _hideTimer.cancel();
  263. _hiding = false;
  264. _a_opacity.stop();
  265. hideFinished();
  266. }
  267. void TabbedPanel::opacityAnimationCallback() {
  268. update();
  269. if (!_a_opacity.animating()) {
  270. if (_hiding) {
  271. _hiding = false;
  272. hideFinished();
  273. } else if (!_a_show.animating()) {
  274. showChildren();
  275. _selector->afterShown();
  276. }
  277. }
  278. }
  279. void TabbedPanel::hideByTimerOrLeave() {
  280. if (isHidden() || preventAutoHide()) {
  281. return;
  282. }
  283. hideAnimated();
  284. }
  285. void TabbedPanel::prepareCacheFor(bool hiding) {
  286. if (_a_opacity.animating()) {
  287. _hiding = hiding;
  288. return;
  289. }
  290. auto showAnimation = base::take(_a_show);
  291. auto showAnimationData = base::take(_showAnimation);
  292. _hiding = false;
  293. showChildren();
  294. _cache = Ui::GrabWidget(this);
  295. _a_show = base::take(showAnimation);
  296. _showAnimation = base::take(showAnimationData);
  297. _hiding = hiding;
  298. if (_a_show.animating()) {
  299. hideChildren();
  300. }
  301. }
  302. void TabbedPanel::startOpacityAnimation(bool hiding) {
  303. if (_selector && !_selector->isHidden()) {
  304. _selector->beforeHiding();
  305. }
  306. prepareCacheFor(hiding);
  307. hideChildren();
  308. _a_opacity.start(
  309. [=] { opacityAnimationCallback(); },
  310. _hiding ? 1. : 0.,
  311. _hiding ? 0. : 1.,
  312. st::emojiPanDuration);
  313. }
  314. void TabbedPanel::startShowAnimation() {
  315. if (!_a_show.animating()) {
  316. auto image = grabForAnimation();
  317. _showAnimation = std::make_unique<Ui::PanelAnimation>(
  318. _selector->st().showAnimation,
  319. (_dropDown
  320. ? Ui::PanelAnimation::Origin::TopRight
  321. : Ui::PanelAnimation::Origin::BottomRight));
  322. auto inner = rect().marginsRemoved(st::emojiPanMargins);
  323. _showAnimation->setFinalImage(
  324. std::move(image),
  325. QRect(
  326. inner.topLeft() * style::DevicePixelRatio(),
  327. inner.size() * style::DevicePixelRatio()));
  328. _showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius));
  329. _showAnimation->start();
  330. }
  331. hideChildren();
  332. _a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);
  333. }
  334. QImage TabbedPanel::grabForAnimation() {
  335. auto cache = base::take(_cache);
  336. auto opacityAnimation = base::take(_a_opacity);
  337. auto showAnimationData = base::take(_showAnimation);
  338. auto showAnimation = base::take(_a_show);
  339. showChildren();
  340. Ui::SendPendingMoveResizeEvents(this);
  341. auto result = QImage(
  342. size() * style::DevicePixelRatio(),
  343. QImage::Format_ARGB32_Premultiplied);
  344. result.setDevicePixelRatio(style::DevicePixelRatio());
  345. result.fill(Qt::transparent);
  346. if (_selector) {
  347. QPainter p(&result);
  348. Ui::RenderWidget(p, _selector, _selector->pos());
  349. }
  350. _a_show = base::take(showAnimation);
  351. _showAnimation = base::take(showAnimationData);
  352. _a_opacity = base::take(opacityAnimation);
  353. _cache = base::take(cache);
  354. return result;
  355. }
  356. void TabbedPanel::hideAnimated() {
  357. if (isHidden() || _hiding) {
  358. return;
  359. }
  360. _hideTimer.cancel();
  361. if (_selector->isSliding()) {
  362. _hideAfterSlide = true;
  363. } else {
  364. startOpacityAnimation(true);
  365. }
  366. // There is no reason to worry about the message scheduling box
  367. // while it moves the user to the separate scheduled section.
  368. _shouldFinishHide = _selector->hasMenu();
  369. }
  370. void TabbedPanel::toggleAnimated() {
  371. if (isHidden() || _hiding || _hideAfterSlide) {
  372. showAnimated();
  373. } else {
  374. hideAnimated();
  375. }
  376. }
  377. void TabbedPanel::hideFinished() {
  378. hide();
  379. _a_show.stop();
  380. _showAnimation.reset();
  381. _cache = QPixmap();
  382. _hiding = false;
  383. _shouldFinishHide = false;
  384. _selector->hideFinished();
  385. }
  386. void TabbedPanel::showAnimated() {
  387. _hideTimer.cancel();
  388. _hideAfterSlide = false;
  389. showStarted();
  390. }
  391. void TabbedPanel::showStarted() {
  392. if (_shouldFinishHide) {
  393. return;
  394. }
  395. if (isHidden()) {
  396. _selector->showStarted();
  397. moveHorizontally();
  398. raise();
  399. show();
  400. startShowAnimation();
  401. } else if (_hiding) {
  402. startOpacityAnimation(false);
  403. }
  404. }
  405. bool TabbedPanel::eventFilter(QObject *obj, QEvent *e) {
  406. if (TabbedPanelShowOnClick.value()) {
  407. return false;
  408. } else if (e->type() == QEvent::Enter) {
  409. otherEnter();
  410. } else if (e->type() == QEvent::Leave) {
  411. otherLeave();
  412. }
  413. return false;
  414. }
  415. void TabbedPanel::showFromSelector() {
  416. if (isHidden()) {
  417. moveHorizontally();
  418. startShowAnimation();
  419. show();
  420. }
  421. showChildren();
  422. showAnimated();
  423. }
  424. style::margins TabbedPanel::innerPadding() const {
  425. return st::emojiPanMargins;
  426. }
  427. QRect TabbedPanel::innerRect() const {
  428. return rect().marginsRemoved(innerPadding());
  429. }
  430. bool TabbedPanel::overlaps(const QRect &globalRect) const {
  431. if (isHidden() || !_cache.isNull()) return false;
  432. auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
  433. auto inner = rect().marginsRemoved(st::emojiPanMargins);
  434. const auto radius = st::emojiPanRadius;
  435. return inner.marginsRemoved(QMargins(radius, 0, radius, 0)).contains(testRect)
  436. || inner.marginsRemoved(QMargins(0, radius, 0, radius)).contains(testRect);
  437. }
  438. TabbedPanel::~TabbedPanel() {
  439. hideFast();
  440. if (!_ownedSelector && _regularWindow) {
  441. _regularWindow->takeTabbedSelectorOwnershipFrom(this);
  442. }
  443. }
  444. } // namespace ChatHelpers