popup_menu.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // For license and copyright information please follow this link:
  5. // https://github.com/desktop-app/legal/blob/master/LEGAL
  6. //
  7. #include "ui/widgets/popup_menu.h"
  8. #include "ui/image/image_prepare.h"
  9. #include "ui/platform/ui_platform_utility.h"
  10. #include "ui/widgets/shadow.h"
  11. #include "ui/widgets/menu/menu_item_base.h"
  12. #include "ui/widgets/scroll_area.h"
  13. #include "ui/wrap/padding_wrap.h"
  14. #include "ui/ui_utility.h"
  15. #include "ui/delayed_activation.h"
  16. #include "ui/painter.h"
  17. #include "ui/integration.h"
  18. #include "base/invoke_queued.h"
  19. #include "base/platform/base_platform_info.h"
  20. #include <QtGui/QtEvents>
  21. #include <QtGui/QPainter>
  22. #include <QtGui/QScreen>
  23. #include <QtGui/QWindow>
  24. #include <QtWidgets/QApplication>
  25. #include <private/qapplication_p.h>
  26. namespace Ui {
  27. namespace {
  28. constexpr auto kShadowCornerMultiplier = 3;
  29. [[nodiscard]] not_null<QImage*> PrepareCachedShadow(
  30. style::margins padding,
  31. not_null<const style::Shadow*> shadow,
  32. not_null<const RoundRect*> body,
  33. int radius,
  34. rpl::lifetime &lifetime) {
  35. const auto side = radius * kShadowCornerMultiplier;
  36. const auto middle = radius;
  37. const auto size = side * 2 + middle;
  38. const auto rect = QRect(0, 0, size, size);
  39. const auto result = lifetime.make_state<QImage>(
  40. rect.marginsAdded(padding).size() * style::DevicePixelRatio(),
  41. QImage::Format_ARGB32_Premultiplied);
  42. result->setDevicePixelRatio(style::DevicePixelRatio());
  43. const auto render = [=] {
  44. result->fill(Qt::transparent);
  45. auto p = QPainter(result);
  46. const auto inner = QRect(padding.left(), padding.top(), size, size);
  47. const auto outerWidth = padding.left() + size + padding.right();
  48. Shadow::paint(p, inner, outerWidth, *shadow);
  49. p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
  50. body->paint(p, inner);
  51. };
  52. render();
  53. style::PaletteChanged(
  54. ) | rpl::start_with_next(render, lifetime);
  55. return result;
  56. }
  57. void PaintCachedShadow(
  58. QPainter &p,
  59. QSize outer,
  60. int radius,
  61. style::margins padding,
  62. const QImage &cached) {
  63. const auto fill = [&](
  64. int dstx, int dsty, int dstw, int dsth,
  65. int srcx, int srcy, int srcw, int srch) {
  66. p.drawImage(
  67. QRect(dstx, dsty, dstw, dsth),
  68. cached,
  69. QRect(
  70. QPoint(srcx, srcy) * style::DevicePixelRatio(),
  71. QSize(srcw, srch) * style::DevicePixelRatio()));
  72. };
  73. const auto paintCorner = [&](
  74. int width, int height,
  75. int dstx, int dsty,
  76. int srcx, int srcy) {
  77. fill(dstx, dsty, width, height, srcx, srcy, width, height);
  78. };
  79. const auto side = radius * kShadowCornerMultiplier;
  80. const auto middle = radius;
  81. const auto size = side * 2 + middle;
  82. paintCorner( // Top-Left
  83. padding.left() + side,
  84. padding.top() + side,
  85. 0,
  86. 0,
  87. 0,
  88. 0);
  89. paintCorner( // Top-Right
  90. side + padding.right(),
  91. padding.top() + side,
  92. outer.width() - side - padding.right(),
  93. 0,
  94. padding.left() + size - side,
  95. 0);
  96. paintCorner( // Bottom-Right
  97. side + padding.right(),
  98. side + padding.bottom(),
  99. outer.width() - side - padding.right(),
  100. outer.height() - side - padding.bottom(),
  101. padding.left() + size - side,
  102. padding.top() + size - side);
  103. paintCorner( // Bottom-Left
  104. padding.left() + side,
  105. side + padding.bottom(),
  106. 0,
  107. outer.height() - side - padding.bottom(),
  108. 0,
  109. padding.top() + size - side);
  110. const auto fillx = outer.width()
  111. - padding.left()
  112. - padding.right()
  113. - 2 * side;
  114. fill( // Top
  115. padding.left() + side,
  116. 0,
  117. fillx,
  118. padding.top(),
  119. padding.left() + side + (middle / 2),
  120. 0,
  121. 1,
  122. padding.top());
  123. fill( // Bottom
  124. padding.left() + side,
  125. outer.height() - padding.bottom(),
  126. fillx,
  127. padding.bottom(),
  128. padding.left() + side + (middle / 2),
  129. padding.top() + size,
  130. 1,
  131. padding.bottom());
  132. const auto filly = outer.height()
  133. - padding.top()
  134. - padding.bottom()
  135. - 2 * side;
  136. fill( // Left
  137. 0,
  138. padding.top() + side,
  139. padding.left(),
  140. filly,
  141. 0,
  142. padding.top() + side + (middle / 2),
  143. padding.left(),
  144. 1);
  145. fill( // Right
  146. outer.width() - padding.right(),
  147. padding.top() + side,
  148. padding.right(),
  149. filly,
  150. padding.left() + size,
  151. padding.top() + side + (middle / 2),
  152. padding.right(),
  153. 1);
  154. }
  155. } // namespace
  156. PopupMenu::PopupMenu(QWidget *parent, const style::PopupMenu &st)
  157. : RpWidget(parent)
  158. , _st(st)
  159. , _roundRect(_st.radius, _st.menu.itemBg)
  160. , _scroll(this, st::defaultMultiSelect.scroll)
  161. , _menu(_scroll->setOwnedWidget(
  162. object_ptr<PaddingWrap<Menu::Menu>>(
  163. _scroll.data(),
  164. object_ptr<Menu::Menu>(_scroll.data(), _st.menu),
  165. _st.scrollPadding))->entity()) {
  166. init();
  167. }
  168. PopupMenu::PopupMenu(QWidget *parent, QMenu *menu, const style::PopupMenu &st)
  169. : RpWidget(parent)
  170. , _st(st)
  171. , _roundRect(_st.radius, _st.menu.itemBg)
  172. , _scroll(this, st::defaultMultiSelect.scroll)
  173. , _menu(_scroll->setOwnedWidget(
  174. object_ptr<PaddingWrap<Menu::Menu>>(
  175. _scroll.data(),
  176. object_ptr<Menu::Menu>(_scroll.data(), menu, _st.menu),
  177. _st.scrollPadding))->entity()) {
  178. init();
  179. for (const auto &action : actions()) {
  180. if (const auto submenu = action->menu()) {
  181. _submenus.emplace(
  182. action,
  183. base::make_unique_q<PopupMenu>(parentWidget(), submenu, st)
  184. ).first->second->deleteOnHide(false);
  185. }
  186. }
  187. }
  188. void PopupMenu::init() {
  189. using namespace rpl::mappers;
  190. Integration::Instance().forcePopupMenuHideRequests(
  191. ) | rpl::start_with_next([=] {
  192. hideMenu(true);
  193. }, lifetime());
  194. installEventFilter(this);
  195. const auto paddingWrap = static_cast<PaddingWrap<Menu::Menu>*>(
  196. _menu->parentWidget());
  197. paddingWrap->paintRequest(
  198. ) | rpl::start_with_next([=](QRect clip) {
  199. const auto top = clip.intersected(
  200. QRect(0, 0, paddingWrap->width(), _st.scrollPadding.top()));
  201. const auto bottom = clip.intersected(QRect(
  202. 0,
  203. paddingWrap->height() - _st.scrollPadding.bottom(),
  204. paddingWrap->width(),
  205. _st.scrollPadding.bottom()));
  206. auto p = QPainter(paddingWrap);
  207. if (!top.isEmpty()) {
  208. p.fillRect(top, _st.menu.itemBg);
  209. }
  210. if (!bottom.isEmpty()) {
  211. p.fillRect(bottom, _st.menu.itemBg);
  212. }
  213. }, paddingWrap->lifetime());
  214. _menu->scrollToRequests(
  215. ) | rpl::start_with_next([=](ScrollToRequest request) {
  216. _scroll->scrollTo({
  217. request.ymin ? (_st.scrollPadding.top() + request.ymin) : 0,
  218. (request.ymax == _menu->height()
  219. ? paddingWrap->height()
  220. : (_st.scrollPadding.top() + request.ymax)),
  221. });
  222. }, _menu->lifetime());
  223. _menu->resizesFromInner(
  224. ) | rpl::start_with_next([=] {
  225. handleMenuResize();
  226. }, _menu->lifetime());
  227. _menu->setActivatedCallback([this](const Menu::CallbackData &data) {
  228. handleActivated(data);
  229. });
  230. _menu->setTriggeredCallback([this](const Menu::CallbackData &data) {
  231. handleTriggered(data);
  232. });
  233. _menu->setKeyPressDelegate([this](int key) { return handleKeyPress(key); });
  234. _menu->setMouseMoveDelegate([this](QPoint globalPosition) { handleMouseMove(globalPosition); });
  235. _menu->setMousePressDelegate([this](QPoint globalPosition) { handleMousePress(globalPosition); });
  236. _menu->setMouseReleaseDelegate([this](QPoint globalPosition) { handleMouseRelease(globalPosition); });
  237. setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::BypassWindowManagerHint | Qt::Popup | Qt::NoDropShadowWindowHint);
  238. setMouseTracking(true);
  239. hide();
  240. setAttribute(Qt::WA_NoSystemBackground, true);
  241. _useTransparency = Platform::TranslucentWindowsSupported();
  242. if (_useTransparency) {
  243. setAttribute(Qt::WA_TranslucentBackground, true);
  244. } else {
  245. setAttribute(Qt::WA_TranslucentBackground, false);
  246. setAttribute(Qt::WA_OpaquePaintEvent, true);
  247. }
  248. }
  249. not_null<PopupMenu*> PopupMenu::ensureSubmenu(
  250. not_null<QAction*> action,
  251. const style::PopupMenu &st) {
  252. const auto &list = actions();
  253. const auto i = ranges::find(list, action);
  254. Assert(i != end(list));
  255. const auto j = _submenus.find(action);
  256. if (j != end(_submenus)) {
  257. return j->second.get();
  258. }
  259. const auto result = _submenus.emplace(
  260. action,
  261. base::make_unique_q<PopupMenu>(parentWidget(), st)
  262. ).first->second.get();
  263. result->deleteOnHide(false);
  264. return result;
  265. }
  266. void PopupMenu::removeSubmenu(not_null<QAction*> action) {
  267. const auto menu = _submenus.take(action);
  268. if (menu && menu->get() == _activeSubmenu) {
  269. base::take(_activeSubmenu)->hideMenu(true);
  270. }
  271. }
  272. void PopupMenu::checkSubmenuShow() {
  273. if (_activeSubmenu) {
  274. return;
  275. } else if (const auto item = _menu->findSelectedAction()) {
  276. if (item->lastTriggeredSource() == Menu::TriggeredSource::Mouse) {
  277. if (_submenus.contains(item->action())) {
  278. item->setClicked(Menu::TriggeredSource::Mouse);
  279. }
  280. }
  281. }
  282. }
  283. void PopupMenu::validateCompositingSupport() {
  284. const auto line = st::lineWidth;
  285. const auto &additional = _additionalMenuPadding;
  286. if (!_useTransparency) {
  287. _padding = QMargins(
  288. std::max(line, additional.left()),
  289. std::max(line, additional.top()),
  290. std::max(line, additional.right()),
  291. std::max(line, additional.bottom()));
  292. _margins = QMargins();
  293. } else {
  294. _padding = QMargins(
  295. std::max(_st.shadow.extend.left(), additional.left()),
  296. std::max(_st.shadow.extend.top(), additional.top()),
  297. std::max(_st.shadow.extend.right(), additional.right()),
  298. std::max(_st.shadow.extend.bottom(), additional.bottom()));
  299. _margins = _padding - (additional - _additionalMenuMargins);
  300. }
  301. Platform::SetWindowMargins(this, _margins);
  302. _scroll->moveToLeft(_padding.left(), _padding.top());
  303. handleMenuResize();
  304. updateRoundingOverlay();
  305. }
  306. void PopupMenu::updateRoundingOverlay() {
  307. if (!_useTransparency) {
  308. _roundingOverlay.destroy();
  309. return;
  310. } else if (_roundingOverlay) {
  311. return;
  312. }
  313. _roundingOverlay.create(this);
  314. sizeValue(
  315. ) | rpl::start_with_next([=](QSize size) {
  316. _roundingOverlay->setGeometry(QRect(QPoint(), size));
  317. }, _roundingOverlay->lifetime());
  318. const auto shadow = PrepareCachedShadow(
  319. _padding,
  320. &_st.shadow,
  321. &_roundRect,
  322. _st.radius,
  323. _roundingOverlay->lifetime());
  324. _roundingOverlay->paintRequest(
  325. ) | rpl::start_with_next([=](QRect clip) {
  326. auto p = QPainter(_roundingOverlay.data());
  327. auto hq = PainterHighQualityEnabler(p);
  328. p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  329. _roundRect.paint(p, _inner, RectPart::AllCorners);
  330. if (!_grabbingForPanelAnimation) {
  331. p.setCompositionMode(QPainter::CompositionMode_SourceOver);
  332. PaintCachedShadow(p, size(), _st.radius, _padding, *shadow);
  333. }
  334. }, _roundingOverlay->lifetime());
  335. _roundingOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
  336. }
  337. void PopupMenu::handleMenuResize() {
  338. auto newWidth = _padding.left() + _st.scrollPadding.left() + _menu->width() + _st.scrollPadding.right() + _padding.right();
  339. auto newHeight = _padding.top() + _st.scrollPadding.top() + _menu->height() + _st.scrollPadding.bottom() + _padding.bottom();
  340. const auto wantedHeight = newHeight - _padding.top() - _padding.bottom();
  341. const auto scrollHeight = _st.maxHeight
  342. ? std::min(_st.maxHeight, wantedHeight)
  343. : wantedHeight;
  344. _scroll->resize(
  345. newWidth - _padding.left() - _padding.right(),
  346. scrollHeight);
  347. {
  348. const auto newSize = QSize(
  349. newWidth,
  350. _padding.top() + scrollHeight + _padding.bottom());
  351. setFixedSize(newSize);
  352. resize(newSize);
  353. }
  354. _inner = rect().marginsRemoved(_padding);
  355. }
  356. not_null<QAction*> PopupMenu::addAction(
  357. base::unique_qptr<Menu::ItemBase> widget) {
  358. return _menu->addAction(std::move(widget));
  359. }
  360. not_null<QAction*> PopupMenu::addAction(
  361. const QString &text,
  362. Fn<void()> callback,
  363. const style::icon *icon,
  364. const style::icon *iconOver) {
  365. return _menu->addAction(text, std::move(callback), icon, iconOver);
  366. }
  367. not_null<QAction*> PopupMenu::addAction(
  368. const QString &text,
  369. std::unique_ptr<PopupMenu> submenu,
  370. const style::icon *icon,
  371. const style::icon *iconOver) {
  372. const auto action = _menu->addAction(
  373. text,
  374. std::make_unique<QMenu>(),
  375. icon,
  376. iconOver);
  377. const auto saved = _submenus.emplace(
  378. action,
  379. base::unique_qptr<PopupMenu>(submenu.release())
  380. ).first->second.get();
  381. saved->setParent(parentWidget());
  382. saved->deleteOnHide(false);
  383. return action;
  384. }
  385. not_null<QAction*> PopupMenu::addSeparator(const style::MenuSeparator *st) {
  386. return _menu->addSeparator(st);
  387. }
  388. not_null<QAction*> PopupMenu::insertAction(
  389. int position,
  390. base::unique_qptr<Menu::ItemBase> widget) {
  391. return _menu->insertAction(position, std::move(widget));
  392. }
  393. void PopupMenu::removeAction(int position) {
  394. const auto i = _submenus.find(_menu->actions()[position]);
  395. if (i != end(_submenus)) {
  396. _submenus.erase(i);
  397. }
  398. _menu->removeAction(position);
  399. }
  400. void PopupMenu::clearActions() {
  401. _submenus.clear();
  402. return _menu->clearActions();
  403. }
  404. void PopupMenu::setTopShift(int topShift) {
  405. _topShift = topShift;
  406. }
  407. void PopupMenu::setForceWidth(int forceWidth) {
  408. _menu->setForceWidth(forceWidth);
  409. }
  410. const std::vector<not_null<QAction*>> &PopupMenu::actions() const {
  411. return _menu->actions();
  412. }
  413. bool PopupMenu::empty() const {
  414. return _menu->empty();
  415. }
  416. void PopupMenu::paintEvent(QPaintEvent *e) {
  417. QPainter p(this);
  418. if (_a_show.animating()) {
  419. const auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
  420. const auto progress = _a_show.value(1.);
  421. const auto state = (opacity > 0.)
  422. ? _showAnimation->paintFrame(p, 0, 0, width(), progress, opacity)
  423. : PanelAnimation::PaintState();
  424. _showStateChanges.fire({
  425. .opacity = state.opacity,
  426. .widthProgress = state.widthProgress,
  427. .heightProgress = state.heightProgress,
  428. .appearingWidth = state.width,
  429. .appearingHeight = state.height,
  430. .appearing = true,
  431. });
  432. } else if (_a_opacity.animating()) {
  433. if (_showAnimation) {
  434. _showAnimation.reset();
  435. _showStateChanges.fire({
  436. .toggling = true,
  437. });
  438. }
  439. p.setOpacity(_a_opacity.value(0.));
  440. p.drawPixmap(0, 0, _cache);
  441. } else if (_hiding || isHidden()) {
  442. hideFinished();
  443. } else if (_showAnimation) {
  444. _showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
  445. _showAnimation.reset();
  446. _showStateChanges.fire({});
  447. PostponeCall(this, [=] {
  448. showChildren();
  449. _animatePhase = AnimatePhase::Shown;
  450. Platform::AcceptAllMouseInput(this);
  451. });
  452. } else {
  453. paintBg(p);
  454. }
  455. }
  456. void PopupMenu::paintBg(QPainter &p) {
  457. if (!_useTransparency) {
  458. p.fillRect(0, 0, width() - _padding.right(), _padding.top(), _st.shadow.fallback);
  459. p.fillRect(width() - _padding.right(), 0, _padding.right(), height() - _padding.bottom(), _st.shadow.fallback);
  460. p.fillRect(_padding.left(), height() - _padding.bottom(), width() - _padding.left(), _padding.bottom(), _st.shadow.fallback);
  461. p.fillRect(0, _padding.top(), _padding.left(), height() - _padding.top(), _st.shadow.fallback);
  462. }
  463. }
  464. void PopupMenu::handleActivated(const Menu::CallbackData &data) {
  465. if (data.source == TriggeredSource::Mouse) {
  466. if (!popupSubmenuFromAction(data)) {
  467. if (const auto currentSubmenu = base::take(_activeSubmenu)) {
  468. currentSubmenu->hideMenu(true);
  469. }
  470. }
  471. }
  472. }
  473. void PopupMenu::handleTriggered(const Menu::CallbackData &data) {
  474. if (!popupSubmenuFromAction(data)) {
  475. _triggering = true;
  476. hideMenu();
  477. data.action->trigger();
  478. _triggering = false;
  479. if (_deleteLater) {
  480. _deleteLater = false;
  481. deleteLater();
  482. }
  483. }
  484. }
  485. bool PopupMenu::popupSubmenuFromAction(const Menu::CallbackData &data) {
  486. if (!data.action) {
  487. return false;
  488. }
  489. if (const auto i = _submenus.find(data.action); i != end(_submenus)) {
  490. const auto submenu = i->second.get();
  491. if (_activeSubmenu != submenu) {
  492. popupSubmenu(data.action, submenu, data.actionTop, data.source);
  493. }
  494. return true;
  495. }
  496. return false;
  497. }
  498. void PopupMenu::popupSubmenu(
  499. not_null<QAction*> action,
  500. not_null<PopupMenu*> submenu,
  501. int actionTop,
  502. TriggeredSource source) {
  503. if (auto currentSubmenu = base::take(_activeSubmenu)) {
  504. currentSubmenu->hideMenu(true);
  505. }
  506. if (submenu) {
  507. const auto padding = _useTransparency
  508. ? _st.shadow.extend
  509. : QMargins(st::lineWidth, 0, st::lineWidth, 0);
  510. QPoint p(_inner.x() + (style::RightToLeft() ? padding.right() : (_inner.width() - padding.left())), _inner.y() + actionTop);
  511. _activeSubmenu = submenu;
  512. if (_activeSubmenu->prepareGeometryFor(geometry().topLeft() + p, this)) {
  513. _activeSubmenu->showPrepared(source);
  514. _menu->setChildShownAction(action);
  515. } else {
  516. _activeSubmenu = nullptr;
  517. }
  518. }
  519. }
  520. void PopupMenu::forwardKeyPress(not_null<QKeyEvent*> e) {
  521. if (!handleKeyPress(e->key())) {
  522. _menu->handleKeyPress(e);
  523. }
  524. }
  525. bool PopupMenu::handleKeyPress(int key) {
  526. if (_activeSubmenu) {
  527. _activeSubmenu->handleKeyPress(key);
  528. return true;
  529. } else if (key == Qt::Key_Escape) {
  530. hideMenu(_parent ? true : false);
  531. return true;
  532. } else if (key == (style::RightToLeft() ? Qt::Key_Right : Qt::Key_Left)) {
  533. if (_parent) {
  534. hideMenu(true);
  535. return true;
  536. }
  537. } else if (key == (style::RightToLeft() ? Qt::Key_Left : Qt::Key_Right)) {
  538. if (const auto item = _menu->findSelectedAction()) {
  539. if (_submenus.contains(item->action())) {
  540. item->setClicked(Menu::TriggeredSource::Keyboard);
  541. }
  542. }
  543. }
  544. return false;
  545. }
  546. void PopupMenu::handleMouseMove(QPoint globalPosition) {
  547. if (_parent) {
  548. _parent->forwardMouseMove(globalPosition);
  549. }
  550. }
  551. void PopupMenu::handleMousePress(QPoint globalPosition) {
  552. if (_parent) {
  553. _parent->forwardMousePress(globalPosition);
  554. } else {
  555. hideMenu();
  556. }
  557. }
  558. void PopupMenu::handleMouseRelease(QPoint globalPosition) {
  559. if (_parent) {
  560. _parent->forwardMouseRelease(globalPosition);
  561. } else {
  562. hideMenu();
  563. }
  564. }
  565. void PopupMenu::focusOutEvent(QFocusEvent *e) {
  566. hideMenu();
  567. }
  568. void PopupMenu::hideEvent(QHideEvent *e) {
  569. if (_deleteOnHide) {
  570. if (_triggering) {
  571. _deleteLater = true;
  572. } else {
  573. deleteLater();
  574. }
  575. }
  576. }
  577. void PopupMenu::keyPressEvent(QKeyEvent *e) {
  578. forwardKeyPress(e);
  579. }
  580. void PopupMenu::mouseMoveEvent(QMouseEvent *e) {
  581. forwardMouseMove(e->globalPos());
  582. }
  583. void PopupMenu::mousePressEvent(QMouseEvent *e) {
  584. forwardMousePress(e->globalPos());
  585. }
  586. bool PopupMenu::eventFilter(QObject *o, QEvent *e) {
  587. const auto type = e->type();
  588. if (type == QEvent::TouchBegin
  589. || type == QEvent::TouchUpdate
  590. || type == QEvent::TouchEnd) {
  591. if (o == windowHandle() && isActiveWindow()) {
  592. const auto event = static_cast<QTouchEvent*>(e);
  593. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  594. e->setAccepted(
  595. QApplicationPrivate::translateRawTouchEvent(
  596. this,
  597. event->device(),
  598. event->touchPoints(),
  599. event->timestamp()));
  600. #elif QT_VERSION < QT_VERSION_CHECK(6, 2, 0) // Qt < 6.0.0
  601. e->setAccepted(
  602. QApplicationPrivate::translateRawTouchEvent(
  603. this,
  604. event->pointingDevice(),
  605. const_cast<QList<QEventPoint> &>(event->points()),
  606. event->timestamp()));
  607. #else // Qt < 6.2.0
  608. e->setAccepted(
  609. QApplicationPrivate::translateRawTouchEvent(this, event));
  610. #endif
  611. return e->isAccepted();
  612. }
  613. }
  614. return false;
  615. }
  616. void PopupMenu::hideMenu(bool fast) {
  617. if (isHidden() || (_hiding && !fast)) {
  618. return;
  619. }
  620. if (_parent && !_a_opacity.animating()) {
  621. _parent->childHiding(this);
  622. }
  623. if (fast) {
  624. hideFast();
  625. } else {
  626. hideAnimated();
  627. if (_parent) {
  628. _parent->hideMenu();
  629. }
  630. }
  631. if (_activeSubmenu) {
  632. _activeSubmenu->hideMenu(fast);
  633. }
  634. }
  635. void PopupMenu::childHiding(PopupMenu *child) {
  636. if (_activeSubmenu && _activeSubmenu == child) {
  637. _activeSubmenu = nullptr;
  638. }
  639. if (!_activeSubmenu) {
  640. _menu->setChildShownAction(nullptr);
  641. }
  642. if (!_hiding && !isHidden()) {
  643. raise();
  644. activateWindow();
  645. }
  646. }
  647. void PopupMenu::setOrigin(PanelAnimation::Origin origin) {
  648. _origin = _forcedOrigin.value_or(origin);
  649. }
  650. void PopupMenu::setForcedOrigin(PanelAnimation::Origin origin) {
  651. _forcedOrigin = origin;
  652. }
  653. void PopupMenu::setForcedVerticalOrigin(VerticalOrigin origin) {
  654. _forcedVerticalOrigin = origin;
  655. }
  656. void PopupMenu::setAdditionalMenuPadding(
  657. QMargins padding,
  658. QMargins margins) {
  659. Expects(padding.left() >= margins.left()
  660. && padding.right() >= margins.right()
  661. && padding.top() >= margins.top()
  662. && padding.bottom() >= margins.bottom());
  663. if (_additionalMenuPadding != padding
  664. || _additionalMenuMargins != margins) {
  665. _additionalMenuPadding = padding;
  666. _additionalMenuMargins = margins;
  667. _roundingOverlay = nullptr;
  668. }
  669. }
  670. void PopupMenu::showAnimated(PanelAnimation::Origin origin) {
  671. setOrigin(origin);
  672. showStarted();
  673. }
  674. void PopupMenu::hideAnimated() {
  675. if (isHidden()) return;
  676. if (_hiding) return;
  677. startOpacityAnimation(true);
  678. }
  679. void PopupMenu::hideFast() {
  680. if (isHidden()) return;
  681. _a_opacity.stop();
  682. hideFinished();
  683. }
  684. void PopupMenu::hideFinished() {
  685. _hiding = false;
  686. _a_show.stop();
  687. _cache = QPixmap();
  688. _animatePhase = AnimatePhase::Hidden;
  689. if (!isHidden()) {
  690. hide();
  691. }
  692. }
  693. void PopupMenu::prepareCache() {
  694. if (_a_opacity.animating()) return;
  695. auto showAnimation = base::take(_a_show);
  696. auto showAnimationData = base::take(_showAnimation);
  697. if (showAnimation.animating()) {
  698. _showStateChanges.fire({});
  699. }
  700. showChildren();
  701. _cache = GrabWidget(this);
  702. _showAnimation = base::take(showAnimationData);
  703. _a_show = base::take(showAnimation);
  704. if (_a_show.animating()) {
  705. fireCurrentShowState();
  706. }
  707. }
  708. void PopupMenu::startOpacityAnimation(bool hiding) {
  709. if (!_useTransparency) {
  710. _a_opacity.stop();
  711. _hiding = hiding;
  712. if (_hiding) {
  713. InvokeQueued(this, [=] {
  714. if (_hiding) {
  715. hideFinished();
  716. }
  717. });
  718. } else {
  719. update();
  720. }
  721. return;
  722. }
  723. _hiding = false;
  724. prepareCache();
  725. _hiding = hiding;
  726. _animatePhase = hiding
  727. ? AnimatePhase::StartHide
  728. : AnimatePhase::StartShow;
  729. hideChildren();
  730. _a_opacity.start(
  731. [=] { opacityAnimationCallback(); },
  732. _hiding ? 1. : 0.,
  733. _hiding ? 0. : 1.,
  734. _st.duration);
  735. }
  736. void PopupMenu::showStarted() {
  737. if (isHidden()) {
  738. show();
  739. startShowAnimation();
  740. return;
  741. } else if (!_hiding) {
  742. return;
  743. }
  744. startOpacityAnimation(false);
  745. }
  746. void PopupMenu::startShowAnimation() {
  747. if (!_useTransparency) {
  748. _a_show.stop();
  749. update();
  750. return;
  751. }
  752. if (!_a_show.animating()) {
  753. auto opacityAnimation = base::take(_a_opacity);
  754. showChildren();
  755. auto cache = grabForPanelAnimation();
  756. _a_opacity = base::take(opacityAnimation);
  757. const auto pixelRatio = style::DevicePixelRatio();
  758. _showAnimation = std::make_unique<PanelAnimation>(_st.animation, _origin);
  759. _showAnimation->setFinalImage(std::move(cache), QRect(_inner.topLeft() * pixelRatio, _inner.size() * pixelRatio));
  760. if (_useTransparency) {
  761. _showAnimation->setCornerMasks(Images::CornersMask(_st.radius));
  762. } else {
  763. _showAnimation->setSkipShadow(true);
  764. }
  765. _showAnimation->start();
  766. }
  767. _animatePhase = AnimatePhase::StartShow;
  768. hideChildren();
  769. _a_show.start([this] { showAnimationCallback(); }, 0., 1., _st.showDuration);
  770. fireCurrentShowState();
  771. }
  772. void PopupMenu::fireCurrentShowState() {
  773. const auto state = _showAnimation->computeState(
  774. _a_show.value(1.),
  775. _a_opacity.value(1.));
  776. _showStateChanges.fire({
  777. .opacity = state.opacity,
  778. .widthProgress = state.widthProgress,
  779. .heightProgress = state.heightProgress,
  780. .appearingWidth = state.width,
  781. .appearingHeight = state.height,
  782. .appearing = true,
  783. });
  784. }
  785. void PopupMenu::opacityAnimationCallback() {
  786. update();
  787. if (!_a_opacity.animating()) {
  788. if (_hiding) {
  789. hideFinished();
  790. } else {
  791. showChildren();
  792. _animatePhase = AnimatePhase::Shown;
  793. }
  794. }
  795. }
  796. void PopupMenu::showAnimationCallback() {
  797. update();
  798. }
  799. QImage PopupMenu::grabForPanelAnimation() {
  800. SendPendingMoveResizeEvents(this);
  801. const auto pixelRatio = style::DevicePixelRatio();
  802. auto result = QImage(size() * pixelRatio, QImage::Format_ARGB32_Premultiplied);
  803. result.setDevicePixelRatio(pixelRatio);
  804. result.fill(Qt::transparent);
  805. {
  806. QPainter p(&result);
  807. _grabbingForPanelAnimation = true;
  808. p.fillRect(_inner, _st.menu.itemBg);
  809. for (const auto child : children()) {
  810. if (const auto widget = qobject_cast<QWidget*>(child)) {
  811. RenderWidget(p, widget, widget->pos());
  812. }
  813. }
  814. _grabbingForPanelAnimation = false;
  815. }
  816. return result;
  817. }
  818. void PopupMenu::deleteOnHide(bool del) {
  819. _deleteOnHide = del;
  820. }
  821. void PopupMenu::popup(const QPoint &p) {
  822. if (prepareGeometryFor(p)) {
  823. popupPrepared();
  824. return;
  825. }
  826. _hiding = false;
  827. _a_opacity.stop();
  828. _a_show.stop();
  829. _cache = QPixmap();
  830. hide();
  831. if (_deleteOnHide) {
  832. deleteLater();
  833. }
  834. }
  835. void PopupMenu::popupPrepared() {
  836. showPrepared(TriggeredSource::Mouse);
  837. }
  838. PanelAnimation::Origin PopupMenu::preparedOrigin() const {
  839. return _origin;
  840. }
  841. QMargins PopupMenu::preparedPadding() const {
  842. return _padding;
  843. }
  844. QMargins PopupMenu::preparedMargins() const {
  845. return _margins;
  846. }
  847. bool PopupMenu::useTransparency() const {
  848. return _useTransparency;
  849. }
  850. int PopupMenu::scrollTop() const {
  851. return _scroll->scrollTop();
  852. }
  853. rpl::producer<int> PopupMenu::scrollTopValue() const {
  854. return _scroll->scrollTopValue();
  855. }
  856. rpl::producer<PopupMenu::ShowState> PopupMenu::showStateValue() const {
  857. return _showStateChanges.events();
  858. }
  859. bool PopupMenu::prepareGeometryFor(const QPoint &p) {
  860. return prepareGeometryFor(p, nullptr);
  861. }
  862. bool PopupMenu::prepareGeometryFor(const QPoint &p, PopupMenu *parent) {
  863. if (_clearLastSeparator) {
  864. _menu->clearLastSeparator();
  865. for (const auto &[action, submenu] : _submenus) {
  866. submenu->menu()->clearLastSeparator();
  867. }
  868. }
  869. if (!parent
  870. && ::Platform::IsMac()
  871. && !Platform::IsApplicationActive()) {
  872. return false;
  873. }
  874. _parent = parent;
  875. const auto screen = QGuiApplication::screenAt(p);
  876. createWinId();
  877. windowHandle()->removeEventFilter(this);
  878. windowHandle()->installEventFilter(this);
  879. if (_parent) {
  880. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  881. setScreen(_parent->screen());
  882. #else // Qt >= 6.0.0
  883. windowHandle()->setScreen(_parent->screen());
  884. #endif // Qt < 6.0.0
  885. } else if (screen) {
  886. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  887. setScreen(screen);
  888. #else // Qt >= 6.0.0
  889. windowHandle()->setScreen(screen);
  890. #endif // Qt < 6.0.0
  891. }
  892. validateCompositingSupport();
  893. using Origin = PanelAnimation::Origin;
  894. auto origin = Origin::TopLeft;
  895. const auto forceLeft = _forcedOrigin
  896. && (*_forcedOrigin == Origin::TopLeft
  897. || *_forcedOrigin == Origin::BottomLeft);
  898. const auto forceTop = (_forcedVerticalOrigin
  899. && (*_forcedVerticalOrigin == VerticalOrigin::Top))
  900. || (_forcedOrigin
  901. && (*_forcedOrigin == Origin::TopLeft
  902. || *_forcedOrigin == Origin::TopRight));
  903. const auto forceRight = _forcedOrigin
  904. && (*_forcedOrigin == Origin::TopRight
  905. || *_forcedOrigin == Origin::BottomRight);
  906. const auto forceBottom = (_forcedVerticalOrigin
  907. && (*_forcedVerticalOrigin == VerticalOrigin::Bottom))
  908. || (_forcedOrigin
  909. && (*_forcedOrigin == Origin::BottomLeft
  910. || *_forcedOrigin == Origin::BottomRight));
  911. auto w = p - QPoint(
  912. std::max(
  913. _additionalMenuPadding.left() - _st.shadow.extend.left(),
  914. 0),
  915. _padding.top() - _topShift);
  916. auto r = screen ? screen->availableGeometry() : QRect();
  917. const auto parentWidth = _parent ? _parent->inner().width() : 0;
  918. if (style::RightToLeft()) {
  919. const auto badLeft = !r.isNull() && w.x() - width() < r.x() - _margins.left();
  920. if (forceRight || (badLeft && !forceLeft)) {
  921. if (_parent && (r.isNull() || w.x() + parentWidth - _margins.left() - _margins.right() + width() - _margins.right() <= r.x() + r.width())) {
  922. w.setX(w.x() + parentWidth - _margins.left() - _margins.right());
  923. } else {
  924. w.setX(r.x() - _margins.left());
  925. }
  926. } else {
  927. w.setX(w.x() - width());
  928. }
  929. } else {
  930. const auto badLeft = !r.isNull() && w.x() + width() - _margins.right() > r.x() + r.width();
  931. if (forceRight || (badLeft && !forceLeft)) {
  932. if (_parent && (r.isNull() || w.x() - parentWidth + _margins.left() + _margins.right() - width() + _margins.right() >= r.x() - _margins.left())) {
  933. w.setX(w.x() + _margins.left() + _margins.right() - parentWidth - width() + _margins.left() + _margins.right());
  934. } else {
  935. w.setX(p.x() - width() + std::max(
  936. _additionalMenuPadding.right() - _st.shadow.extend.right(),
  937. 0));
  938. }
  939. origin = PanelAnimation::Origin::TopRight;
  940. }
  941. }
  942. const auto badTop = !r.isNull() && w.y() + height() - _margins.bottom() > r.y() + r.height();
  943. if (forceBottom || (badTop && !forceTop)) {
  944. if (_parent) {
  945. w.setY(r.y() + r.height() - height() + _margins.bottom());
  946. } else {
  947. w.setY(p.y() - height() + _margins.bottom());
  948. origin = (origin == PanelAnimation::Origin::TopRight)
  949. ? PanelAnimation::Origin::BottomRight
  950. : PanelAnimation::Origin::BottomLeft;
  951. }
  952. }
  953. if (!r.isNull()) {
  954. if (w.x() + width() - _margins.right() > r.x() + r.width()) {
  955. w.setX(r.x() + r.width() + _margins.right() - width());
  956. }
  957. if (w.x() + _margins.left() < r.x()) {
  958. w.setX(r.x() - _margins.left());
  959. }
  960. if (w.y() + height() - _margins.bottom() > r.y() + r.height()) {
  961. w.setY(r.y() + r.height() + _margins.bottom() - height());
  962. }
  963. if (w.y() + _margins.top() < r.y()) {
  964. w.setY(r.y() - _margins.top());
  965. }
  966. }
  967. move(w);
  968. setOrigin(origin);
  969. return true;
  970. }
  971. void PopupMenu::showPrepared(TriggeredSource source) {
  972. _menu->setShowSource(source);
  973. startShowAnimation();
  974. if (::Platform::IsWindows()) {
  975. ForceFullRepaintSync(this);
  976. }
  977. show();
  978. Platform::ShowOverAll(this);
  979. raise();
  980. activateWindow();
  981. }
  982. void PopupMenu::setClearLastSeparator(bool clear) {
  983. _clearLastSeparator = clear;
  984. }
  985. PopupMenu::~PopupMenu() {
  986. for (const auto &[action, submenu] : base::take(_submenus)) {
  987. delete submenu;
  988. }
  989. if (const auto parent = parentWidget()) {
  990. const auto focused = QApplication::focusWidget();
  991. if (_reactivateParent
  992. && focused != nullptr
  993. && Ui::InFocusChain(parent->window())) {
  994. ActivateWindowDelayed(parent);
  995. }
  996. }
  997. if (_destroyedCallback) {
  998. _destroyedCallback();
  999. }
  1000. }
  1001. } // namespace Ui