menu.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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/menu/menu.h"
  8. #include "ui/widgets/menu/menu_action.h"
  9. #include "ui/widgets/menu/menu_item_base.h"
  10. #include "ui/widgets/menu/menu_separator.h"
  11. #include "ui/widgets/scroll_area.h"
  12. #include "styles/style_widgets.h"
  13. #include <QtGui/QtEvents>
  14. namespace Ui::Menu {
  15. Menu::Menu(QWidget *parent, const style::Menu &st)
  16. : RpWidget(parent)
  17. , _st(st) {
  18. init();
  19. }
  20. Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st)
  21. : RpWidget(parent)
  22. , _st(st)
  23. , _wappedMenu(menu) {
  24. init();
  25. _wappedMenu->setParent(this);
  26. for (auto action : _wappedMenu->actions()) {
  27. addAction(action);
  28. }
  29. _wappedMenu->hide();
  30. }
  31. Menu::~Menu() = default;
  32. void Menu::init() {
  33. resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
  34. setMouseTracking(true);
  35. if (_st.itemBg->c.alpha() == 255) {
  36. setAttribute(Qt::WA_OpaquePaintEvent);
  37. }
  38. paintRequest(
  39. ) | rpl::start_with_next([=](const QRect &clip) {
  40. QPainter(this).fillRect(clip, _st.itemBg);
  41. }, lifetime());
  42. positionValue(
  43. ) | rpl::start_with_next([=] {
  44. handleMouseMove(QCursor::pos());
  45. }, lifetime());
  46. }
  47. not_null<QAction*> Menu::addAction(
  48. const QString &text,
  49. Fn<void()> callback,
  50. const style::icon *icon,
  51. const style::icon *iconOver) {
  52. auto action = CreateAction(this, text, std::move(callback));
  53. return addAction(std::move(action), icon, iconOver);
  54. }
  55. not_null<QAction*> Menu::addAction(
  56. const QString &text,
  57. std::unique_ptr<QMenu> submenu,
  58. const style::icon *icon,
  59. const style::icon *iconOver) {
  60. const auto action = new QAction(text, this);
  61. action->setMenu(submenu.release());
  62. return addAction(action, icon, iconOver);
  63. }
  64. not_null<QAction*> Menu::addAction(
  65. not_null<QAction*> action,
  66. const style::icon *icon,
  67. const style::icon *iconOver) {
  68. if (action->isSeparator()) {
  69. return addSeparator();
  70. }
  71. auto item = base::make_unique_q<Action>(
  72. this,
  73. _st,
  74. std::move(action),
  75. icon,
  76. iconOver ? iconOver : icon);
  77. return addAction(std::move(item));
  78. }
  79. not_null<QAction*> Menu::addAction(base::unique_qptr<ItemBase> widget) {
  80. return insertAction(_actions.size(), std::move(widget));
  81. }
  82. not_null<QAction*> Menu::insertAction(
  83. int position,
  84. base::unique_qptr<ItemBase> widget) {
  85. Expects(position >= 0 && position <= _actions.size());
  86. Expects(position >= 0 && position <= _actionWidgets.size());
  87. const auto raw = widget.get();
  88. const auto action = raw->action();
  89. _actions.insert(begin(_actions) + position, action);
  90. raw->setParent(this);
  91. raw->show();
  92. raw->setIndex(position);
  93. for (auto i = position, to = int(_actionWidgets.size()); i != to; ++i) {
  94. _actionWidgets[i]->setIndex(i + 1);
  95. }
  96. _actionWidgets.insert(
  97. begin(_actionWidgets) + position,
  98. std::move(widget));
  99. raw->selects(
  100. ) | rpl::start_with_next([=](const CallbackData &data) {
  101. if (!data.selected) {
  102. if (!findSelectedAction()
  103. && data.index < _actionWidgets.size()
  104. && _childShownAction == data.action) {
  105. const auto widget = _actionWidgets[data.index].get();
  106. widget->setSelected(true, widget->lastTriggeredSource());
  107. }
  108. return;
  109. }
  110. _lastSelectedByMouse = (data.source == TriggeredSource::Mouse);
  111. for (auto i = 0; i < _actionWidgets.size(); i++) {
  112. if (i != data.index) {
  113. _actionWidgets[i]->setSelected(false);
  114. }
  115. }
  116. if (_activatedCallback) {
  117. _activatedCallback(data);
  118. }
  119. }, raw->lifetime());
  120. raw->clicks(
  121. ) | rpl::start_with_next([=](const CallbackData &data) {
  122. if (_triggeredCallback) {
  123. _triggeredCallback(data);
  124. }
  125. }, raw->lifetime());
  126. QObject::connect(action.get(), &QAction::changed, raw, [=] {
  127. // Select an item under mouse that was disabled and became enabled.
  128. if (_lastSelectedByMouse
  129. && !findSelectedAction()
  130. && action->isEnabled()) {
  131. updateSelected(QCursor::pos());
  132. }
  133. });
  134. raw->minWidthValue(
  135. ) | rpl::skip(1) | rpl::filter([=] {
  136. return !_forceWidth;
  137. }) | rpl::start_with_next([=] {
  138. resizeFromInner(recountWidth(), height());
  139. }, raw->lifetime());
  140. raw->heightValue(
  141. ) | rpl::skip(1) | rpl::start_with_next([=] {
  142. resizeFromInner(width(), recountHeight());
  143. }, raw->lifetime());
  144. resizeFromInner(recountWidth(), recountHeight());
  145. updateSelected(QCursor::pos());
  146. return action;
  147. }
  148. int Menu::recountWidth() const {
  149. return _forceWidth
  150. ? _forceWidth
  151. : std::clamp(
  152. (_actionWidgets.empty()
  153. ? 0
  154. : (*ranges::max_element(
  155. _actionWidgets,
  156. std::less<>(),
  157. &ItemBase::minWidth))->minWidth()),
  158. _st.widthMin,
  159. _st.widthMax);
  160. }
  161. int Menu::recountHeight() const {
  162. auto result = 0;
  163. for (const auto &widget : _actionWidgets) {
  164. if (widget->y() != result) {
  165. widget->move(0, result);
  166. }
  167. result += widget->height();
  168. }
  169. return result;
  170. }
  171. void Menu::removeAction(int position) {
  172. Expects(position >= 0 && position < actions().size());
  173. _actionWidgets.erase(begin(_actionWidgets) + position);
  174. if (_actions[position]->parent() == this) {
  175. delete _actions[position];
  176. }
  177. _actions.erase(begin(_actions) + position);
  178. resizeFromInner(width(), recountHeight());
  179. }
  180. not_null<QAction*> Menu::addSeparator(const style::MenuSeparator *st) {
  181. const auto separator = new QAction(this);
  182. separator->setSeparator(true);
  183. auto item = base::make_unique_q<Separator>(
  184. this,
  185. _st,
  186. st ? *st : _st.separator,
  187. separator);
  188. return addAction(std::move(item));
  189. }
  190. void Menu::clearActions() {
  191. _actionWidgets.clear();
  192. for (auto action : base::take(_actions)) {
  193. if (action->parent() == this) {
  194. delete action;
  195. }
  196. }
  197. resizeFromInner(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
  198. }
  199. void Menu::clearLastSeparator() {
  200. if (_actionWidgets.empty() || _actions.empty()) {
  201. return;
  202. }
  203. if (_actionWidgets.back()->action() == _actions.back()) {
  204. if (_actions.back()->isSeparator()) {
  205. resizeFromInner(
  206. width(),
  207. height() - _actionWidgets.back()->height());
  208. _actionWidgets.pop_back();
  209. if (_actions.back()->parent() == this) {
  210. delete _actions.back();
  211. _actions.pop_back();
  212. }
  213. }
  214. }
  215. }
  216. void Menu::finishAnimating() {
  217. for (const auto &widget : _actionWidgets) {
  218. widget->finishAnimating();
  219. }
  220. }
  221. bool Menu::empty() const {
  222. return _actionWidgets.empty();
  223. }
  224. void Menu::resizeFromInner(int w, int h) {
  225. if (const auto s = QSize(w, h); s != size()) {
  226. resize(s);
  227. _resizesFromInner.fire({});
  228. }
  229. }
  230. rpl::producer<> Menu::resizesFromInner() const {
  231. return _resizesFromInner.events();
  232. }
  233. rpl::producer<ScrollToRequest> Menu::scrollToRequests() const {
  234. return _scrollToRequests.events();
  235. }
  236. void Menu::setShowSource(TriggeredSource source) {
  237. const auto mouseSelection = (source == TriggeredSource::Mouse);
  238. setSelected(
  239. (mouseSelection || _actions.empty()) ? -1 : 0,
  240. mouseSelection);
  241. }
  242. const std::vector<not_null<QAction*>> &Menu::actions() const {
  243. return _actions;
  244. }
  245. void Menu::setForceWidth(int forceWidth) {
  246. _forceWidth = forceWidth;
  247. resizeFromInner(_forceWidth, height());
  248. }
  249. void Menu::updateSelected(QPoint globalPosition) {
  250. const auto p = mapFromGlobal(globalPosition) - QPoint(0, _st.skip);
  251. for (const auto &widget : _actionWidgets) {
  252. const auto widgetRect = QRect(widget->pos(), widget->size());
  253. if (widgetRect.contains(p)) {
  254. _lastSelectedByMouse = true;
  255. // It may actually fail to become selected (if it is disabled).
  256. widget->setSelected(true);
  257. break;
  258. }
  259. }
  260. }
  261. void Menu::itemPressed(TriggeredSource source) {
  262. if (const auto action = findSelectedAction()) {
  263. if (action->lastTriggeredSource() == source) {
  264. action->setClicked(source);
  265. }
  266. }
  267. }
  268. void Menu::keyPressEvent(QKeyEvent *e) {
  269. const auto key = e->key();
  270. if (!_keyPressDelegate || !_keyPressDelegate(key)) {
  271. handleKeyPress(e);
  272. }
  273. }
  274. ItemBase *Menu::findSelectedAction() const {
  275. const auto it = ranges::find_if(_actionWidgets, &ItemBase::isSelected);
  276. return (it == end(_actionWidgets)) ? nullptr : it->get();
  277. }
  278. void Menu::handleKeyPress(not_null<QKeyEvent*> e) {
  279. const auto key = e->key();
  280. const auto selected = findSelectedAction();
  281. if ((key != Qt::Key_Up && key != Qt::Key_Down) || _actions.empty()) {
  282. if (selected) {
  283. selected->handleKeyPress(e);
  284. }
  285. return;
  286. }
  287. const auto delta = (key == Qt::Key_Down ? 1 : -1);
  288. auto start = selected ? selected->index() : -1;
  289. if (start < 0 || start >= _actions.size()) {
  290. start = (delta > 0) ? (_actions.size() - 1) : 0;
  291. }
  292. auto newSelected = start;
  293. do {
  294. newSelected += delta;
  295. if (newSelected < 0) {
  296. newSelected += _actions.size();
  297. } else if (newSelected >= _actions.size()) {
  298. newSelected -= _actions.size();
  299. }
  300. } while (newSelected != start
  301. && (!_actionWidgets[newSelected]->isEnabled()));
  302. if (_actionWidgets[newSelected]->isEnabled()) {
  303. setSelected(newSelected, false);
  304. }
  305. }
  306. void Menu::clearSelection() {
  307. setSelected(-1, false);
  308. }
  309. void Menu::clearMouseSelection() {
  310. const auto selected = findSelectedAction();
  311. const auto mouseSelection = selected
  312. ? (selected->lastTriggeredSource() == TriggeredSource::Mouse)
  313. : false;
  314. if (mouseSelection && !_childShownAction) {
  315. clearSelection();
  316. }
  317. }
  318. void Menu::setSelected(int selected, bool isMouseSelection) {
  319. if (selected >= _actionWidgets.size()) {
  320. selected = -1;
  321. }
  322. const auto source = isMouseSelection
  323. ? TriggeredSource::Mouse
  324. : TriggeredSource::Keyboard;
  325. if (selected >= 0 && source == TriggeredSource::Keyboard) {
  326. const auto widget = _actionWidgets[selected].get();
  327. _scrollToRequests.fire({
  328. widget->y(),
  329. widget->y() + widget->height(),
  330. });
  331. }
  332. if (const auto selectedItem = findSelectedAction()) {
  333. if (selectedItem->index() == selected) {
  334. return;
  335. }
  336. selectedItem->setSelected(false, source);
  337. }
  338. if (selected >= 0) {
  339. _actionWidgets[selected].get()->setSelected(true, source);
  340. }
  341. }
  342. void Menu::mouseMoveEvent(QMouseEvent *e) {
  343. handleMouseMove(e->globalPos());
  344. }
  345. void Menu::handleMouseMove(QPoint globalPosition) {
  346. const auto margins = style::margins(0, _st.skip, 0, _st.skip);
  347. const auto inner = rect().marginsRemoved(margins);
  348. const auto localPosition = mapFromGlobal(globalPosition);
  349. if (inner.contains(localPosition)) {
  350. updateSelected(globalPosition);
  351. } else {
  352. clearMouseSelection();
  353. if (_mouseMoveDelegate) {
  354. _mouseMoveDelegate(globalPosition);
  355. }
  356. }
  357. }
  358. void Menu::mousePressEvent(QMouseEvent *e) {
  359. handleMousePress(e->globalPos());
  360. }
  361. void Menu::mouseReleaseEvent(QMouseEvent *e) {
  362. handleMouseRelease(e->globalPos());
  363. }
  364. void Menu::handleMousePress(QPoint globalPosition) {
  365. handleMouseMove(globalPosition);
  366. const auto margins = style::margins(0, _st.skip, 0, _st.skip);
  367. const auto inner = rect().marginsRemoved(margins);
  368. const auto localPosition = mapFromGlobal(globalPosition);
  369. const auto pressed = (inner.contains(localPosition)
  370. && _lastSelectedByMouse)
  371. ? findSelectedAction()
  372. : nullptr;
  373. if (pressed) {
  374. pressed->setClicked();
  375. } else {
  376. if (_mousePressDelegate) {
  377. _mousePressDelegate(globalPosition);
  378. }
  379. }
  380. }
  381. void Menu::handleMouseRelease(QPoint globalPosition) {
  382. if (!rect().contains(mapFromGlobal(globalPosition))
  383. && _mouseReleaseDelegate) {
  384. _mouseReleaseDelegate(globalPosition);
  385. }
  386. }
  387. } // namespace Ui::Menu