1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#ifndef QMENU_P_H
5#define QMENU_P_H
6
7//
8// W A R N I N G
9// -------------
10//
11// This file is not part of the Qt API. It exists purely as an
12// implementation detail. This header file may change from version to
13// version without notice, or even be removed.
14//
15// We mean it.
16//
17
18#include <QtWidgets/private/qtwidgetsglobal_p.h>
19#include "QtWidgets/qmenu.h"
20#if QT_CONFIG(menubar)
21#include "QtWidgets/qmenubar.h"
22#endif
23#include "QtWidgets/qstyleoption.h"
24#include "QtCore/qdatetime.h"
25#include "QtCore/qmap.h"
26#include "QtCore/qhash.h"
27#include "QtCore/qbasictimer.h"
28#include "private/qwidget_p.h"
29
30#include <qpa/qplatformmenu.h>
31
32#include <QtCore/qpointer.h>
33
34#include <functional>
35
36QT_REQUIRE_CONFIG(menu);
37
38QT_BEGIN_NAMESPACE
39
40static inline int pick(Qt::Orientation o, const QPoint &pos)
41{ return o == Qt::Horizontal ? pos.x() : pos.y(); }
42
43static inline int pick(Qt::Orientation o, const QSize &size)
44{ return o == Qt::Horizontal ? size.width() : size.height(); }
45
46static inline int &rpick(Qt::Orientation o, QPoint &pos)
47{ return o == Qt::Horizontal ? pos.rx() : pos.ry(); }
48
49static inline int &rpick(Qt::Orientation o, QSize &size)
50{ return o == Qt::Horizontal ? size.rwidth() : size.rheight(); }
51
52static inline QSizePolicy::Policy pick(Qt::Orientation o, const QSizePolicy &policy)
53{ return o == Qt::Horizontal ? policy.horizontalPolicy() : policy.verticalPolicy(); }
54
55static inline int perp(Qt::Orientation o, const QPoint &pos)
56{ return o == Qt::Vertical ? pos.x() : pos.y(); }
57
58static inline int perp(Qt::Orientation o, const QSize &size)
59{ return o == Qt::Vertical ? size.width() : size.height(); }
60
61static inline int &rperp(Qt::Orientation o, QPoint &pos)
62{ return o == Qt::Vertical ? pos.rx() : pos.ry(); }
63
64static inline int &rperp(Qt::Orientation o, QSize &size)
65{ return o == Qt::Vertical ? size.rwidth() : size.rheight(); }
66
67static inline int pick(Qt::Orientation o, const QMargins &m)
68{ return o == Qt::Horizontal ? (m.left() + m.right()) : (m.top() + m.bottom()); }
69
70static inline int perp(Qt::Orientation o, const QMargins &m)
71{ return o == Qt::Vertical ? (m.left() + m.right()) : (m.top() + m.bottom()); }
72
73class QTornOffMenu;
74class QEventLoop;
75
76template <typename T>
77class QSetValueOnDestroy
78{
79public:
80 QSetValueOnDestroy(T &toSet, T value)
81 : toSet(toSet)
82 , value(value)
83 { }
84
85 ~QSetValueOnDestroy() { toSet = value; }
86private:
87 T &toSet;
88 T value;
89};
90
91class QMenuSloppyState
92{
93 Q_DISABLE_COPY_MOVE(QMenuSloppyState)
94public:
95 QMenuSloppyState()
96 : m_enabled(false)
97 , m_uni_directional(false)
98 , m_select_other_actions(false)
99 , m_use_reset_action(true)
100 { }
101
102 ~QMenuSloppyState() { reset(); }
103
104 void initialize(QMenu *menu)
105 {
106 const auto style = menu->style();
107 m_menu = menu;
108 m_uni_directional = style->styleHint(stylehint: QStyle::SH_Menu_SubMenuUniDirection, opt: nullptr, widget: menu);
109 m_uni_dir_fail_at_count = short(style->styleHint(stylehint: QStyle::SH_Menu_SubMenuUniDirectionFailCount, opt: nullptr, widget: menu));
110 m_select_other_actions = style->styleHint(stylehint: QStyle::SH_Menu_SubMenuSloppySelectOtherActions, opt: nullptr, widget: menu);
111 m_timeout = short(style->styleHint(stylehint: QStyle::SH_Menu_SubMenuSloppyCloseTimeout, opt: nullptr, widget: menu));
112 m_discard_state_when_entering_parent = style->styleHint(stylehint: QStyle::SH_Menu_SubMenuResetWhenReenteringParent, opt: nullptr, widget: menu);
113 m_dont_start_time_on_leave = style->styleHint(stylehint: QStyle::SH_Menu_SubMenuDontStartSloppyOnLeave, opt: nullptr, widget: menu);
114 reset();
115 }
116
117 void reset();
118 bool enabled() const { return m_enabled; }
119
120 enum MouseEventResult {
121 EventIsProcessed,
122 EventShouldBePropagated,
123 EventDiscardsSloppyState
124 };
125
126 void startTimer()
127 {
128 if (m_enabled)
129 m_time.start(msec: m_timeout, obj: m_menu);
130 }
131
132 void startTimerIfNotRunning()
133 {
134 if (!m_time.isActive())
135 startTimer();
136 }
137
138 void stopTimer()
139 {
140 m_time.stop();
141 }
142
143 void enter();
144 void childEnter();
145
146 void leave();
147 void childLeave();
148
149 static qreal slope(const QPointF &p1, const QPointF &p2)
150 {
151 const QPointF slope = p2 - p1;
152 if (qFuzzyIsNull(d: slope.x()))
153 return 9999;
154 return slope.y() / slope.x();
155 }
156
157 bool checkSlope(qreal oldS, qreal newS, bool wantSteeper)
158 {
159 if (wantSteeper)
160 return oldS <= newS;
161 return newS <= oldS;
162 }
163
164 MouseEventResult processMouseEvent(const QPointF &mousePos, QAction *resetAction, QAction *currentAction)
165 {
166 if (m_parent)
167 m_parent->stopTimer();
168
169 if (!m_enabled)
170 return EventShouldBePropagated;
171
172 startTimerIfNotRunning();
173
174 if (!m_sub_menu) {
175 reset();
176 return EventShouldBePropagated;
177 }
178
179 QSetValueOnDestroy<bool> setFirstMouse(m_first_mouse, false);
180 QSetValueOnDestroy<QPointF> setPreviousPoint(m_previous_point, mousePos);
181
182 if (resetAction && resetAction->isSeparator()) {
183 m_reset_action = nullptr;
184 m_use_reset_action = true;
185 } else if (m_reset_action != resetAction) {
186 if (m_use_reset_action && resetAction) {
187 const QList<QAction *> actions = m_menu->actions();
188 const int resetIdx = actions.indexOf(t: resetAction);
189 const int originIdx = actions.indexOf(t: m_origin_action);
190 if (resetIdx > -1 && originIdx > -1 && qAbs(t: resetIdx - originIdx) > 1)
191 m_use_reset_action = false;
192 }
193 m_reset_action = resetAction;
194 }
195
196 if (m_action_rect.contains(p: mousePos)) {
197 startTimer();
198 return currentAction == m_menu->menuAction() ? EventIsProcessed : EventShouldBePropagated;
199 }
200
201 if (m_uni_directional && !m_first_mouse && resetAction != m_origin_action) {
202 bool left_to_right = m_menu->layoutDirection() == Qt::LeftToRight;
203 QRect sub_menu_rect = m_sub_menu->geometry();
204 QPoint sub_menu_top =
205 left_to_right? sub_menu_rect.topLeft() : sub_menu_rect.topRight();
206 QPoint sub_menu_bottom =
207 left_to_right? sub_menu_rect.bottomLeft() : sub_menu_rect.bottomRight();
208 qreal prev_slope_top = slope(p1: m_previous_point, p2: sub_menu_top);
209 qreal prev_slope_bottom = slope(p1: m_previous_point, p2: sub_menu_bottom);
210
211 qreal current_slope_top = slope(p1: mousePos, p2: sub_menu_top);
212 qreal current_slope_bottom = slope(p1: mousePos, p2: sub_menu_bottom);
213
214 bool slopeTop = checkSlope(oldS: prev_slope_top, newS: current_slope_top, wantSteeper: sub_menu_top.y() < mousePos.y());
215 bool slopeBottom = checkSlope(oldS: prev_slope_bottom, newS: current_slope_bottom, wantSteeper: sub_menu_bottom.y() > mousePos.y());
216 bool rightDirection = false;
217 int mouseDir = int(m_previous_point.y() - mousePos.y());
218 if (mouseDir >= 0) {
219 rightDirection = rightDirection || slopeTop;
220 }
221 if (mouseDir <= 0) {
222 rightDirection = rightDirection || slopeBottom;
223 }
224
225 if (m_uni_dir_discarded_count >= m_uni_dir_fail_at_count && !rightDirection) {
226 m_uni_dir_discarded_count = 0;
227 return EventDiscardsSloppyState;
228 }
229
230 if (!rightDirection)
231 m_uni_dir_discarded_count++;
232 else
233 m_uni_dir_discarded_count = 0;
234
235 }
236
237 return m_select_other_actions ? EventShouldBePropagated : EventIsProcessed;
238 }
239
240 void setSubMenuPopup(const QRect &actionRect, QAction *resetAction, QMenu *subMenu);
241 bool hasParentActiveDelayTimer() const;
242 void timeout();
243 int timeForTimeout() const { return m_timeout; }
244
245 bool isTimerId(int timerId) const { return m_time.timerId() == timerId; }
246 QMenu *subMenu() const { return m_sub_menu; }
247
248private:
249 QMenu *m_menu = nullptr;
250 QAction *m_reset_action = nullptr;
251 QAction *m_origin_action = nullptr;
252 QRectF m_action_rect;
253 QPointF m_previous_point;
254 QPointer<QMenu> m_sub_menu;
255 QMenuSloppyState *m_parent = nullptr;
256 QBasicTimer m_time;
257 short m_uni_dir_discarded_count = 0;
258 short m_uni_dir_fail_at_count = 0;
259 short m_timeout = 0;
260 bool m_init_guard = false;
261 bool m_first_mouse = true;
262
263 bool m_enabled : 1;
264 bool m_uni_directional : 1;
265 bool m_select_other_actions : 1;
266 bool m_discard_state_when_entering_parent : 1;
267 bool m_dont_start_time_on_leave : 1;
268 bool m_use_reset_action : 1;
269};
270
271class QMenuPrivate : public QWidgetPrivate
272{
273 Q_DECLARE_PUBLIC(QMenu)
274public:
275 using PositionFunction = std::function<QPoint(const QSize &)>;
276
277 QMenuPrivate() :
278 itemsDirty(false),
279 hasCheckableItems(false),
280 lastContextMenu(false),
281 collapsibleSeparators(true),
282 toolTipsVisible(false),
283 delayedPopupGuard(false),
284 hasReceievedEnter(false),
285 hasHadMouse(false),
286 aboutToHide(false),
287 tearoff(false),
288 tornoff(false),
289 tearoffHighlighted(false),
290 doChildEffects(false)
291 { }
292
293 ~QMenuPrivate()
294 {
295 delete scroll;
296 if (!platformMenu.isNull() && !platformMenu->parent())
297 delete platformMenu.data();
298 }
299 void init();
300 QPlatformMenu *createPlatformMenu();
301 void setPlatformMenu(QPlatformMenu *menu);
302 void syncPlatformMenu();
303 void copyActionToPlatformItem(const QAction *action, QPlatformMenuItem *item);
304 QPlatformMenuItem *insertActionInPlatformMenu(const QAction *action, QPlatformMenuItem *beforeItem);
305
306#ifdef Q_OS_MACOS
307 void moveWidgetToPlatformItem(QWidget *w, QPlatformMenuItem* item);
308#endif
309
310 static QMenuPrivate *get(QMenu *m) { return m->d_func(); }
311 int scrollerHeight() const;
312
313 bool isContextMenu() const;
314
315 //item calculations
316 QRect actionRect(QAction *) const;
317
318 mutable QList<QRect> actionRects;
319 mutable QHash<QAction *, QWidget *> widgetItems;
320 void updateActionRects() const;
321 void updateActionRects(const QRect &screen) const;
322 QRect popupGeometry(QScreen *screen = nullptr) const;
323 bool useFullScreenForPopup() const;
324 int getLastVisibleAction() const;
325 void popup(const QPoint &p, QAction *atAction, PositionFunction positionFunction = {});
326 QAction *exec(const QPoint &p, QAction *action, PositionFunction positionFunction = {});
327
328 //selection
329 static QMenu *mouseDown;
330 QPointF mousePopupPos;
331
332 QAction *currentAction = nullptr;
333#ifdef QT_KEYPAD_NAVIGATION
334 QAction *selectAction = nullptr;
335 QAction *cancelAction = nullptr;
336#endif
337 struct DelayState {
338 DelayState()
339 { }
340 void initialize(QMenu *parent)
341 {
342 this->parent = parent;
343 }
344
345 void start(int timeout, QAction *toStartAction)
346 {
347 if (timer.isActive() && toStartAction == action)
348 return;
349 action = toStartAction;
350 timer.start(msec: timeout,obj: parent);
351 }
352 void stop()
353 {
354 action = nullptr;
355 timer.stop();
356 }
357
358 QMenu *parent = nullptr;
359 QAction *action = nullptr;
360 QBasicTimer timer;
361 } delayState;
362 enum SelectionReason {
363 SelectedFromKeyboard,
364 SelectedFromElsewhere
365 };
366 QWidget *topCausedWidget() const;
367 QAction *actionAt(QPoint p) const;
368 void setFirstActionActive();
369 void setCurrentAction(QAction *, int popup = -1, SelectionReason reason = SelectedFromElsewhere, bool activateFirst = false);
370 void popupAction(QAction *, int, bool);
371 void setSyncAction();
372
373 //scrolling support
374 struct QMenuScroller {
375 enum ScrollLocation { ScrollStay, ScrollBottom, ScrollTop, ScrollCenter };
376 enum ScrollDirection { ScrollNone=0, ScrollUp=0x01, ScrollDown=0x02 };
377 int scrollOffset = 0;
378 QBasicTimer scrollTimer;
379 quint8 scrollFlags = ScrollNone;
380 quint8 scrollDirection = ScrollNone;
381
382 QMenuScroller() { }
383 ~QMenuScroller() { }
384 } *scroll = nullptr;
385 void scrollMenu(QMenuScroller::ScrollLocation location, bool active=false);
386 void scrollMenu(QMenuScroller::ScrollDirection direction, bool page=false, bool active=false);
387 void scrollMenu(QAction *action, QMenuScroller::ScrollLocation location, bool active=false);
388
389 //synchronous operation (ie exec())
390 QEventLoop *eventLoop = nullptr;
391 QPointer<QAction> syncAction;
392
393 //search buffer
394 QString searchBuffer;
395 QBasicTimer searchBufferTimer;
396
397 //passing of mouse events up the parent hierarchy
398 QPointer<QMenu> activeMenu;
399 bool mouseEventTaken(QMouseEvent *);
400
401 //used to walk up the popup list
402 struct QMenuCaused {
403 QPointer<QWidget> widget;
404 QPointer<QAction> action;
405 };
406 virtual QList<QPointer<QWidget>> calcCausedStack() const;
407 QMenuCaused causedPopup;
408 void hideUpToMenuBar();
409 void hideMenu(QMenu *menu);
410 QWindow *transientParentWindow() const;
411
412 //index mappings
413 inline QAction *actionAt(int i) const { return q_func()->actions().at(i); }
414 inline int indexOf(QAction *act) const { return q_func()->actions().indexOf(t: act); }
415
416 //tear off support
417 QPointer<QTornOffMenu> tornPopup;
418
419 QMenuSloppyState sloppyState;
420
421 //default action
422 QPointer<QAction> defaultAction;
423
424 QAction *menuAction = nullptr;
425 QAction *defaultMenuAction = nullptr;
426
427 void setOverrideMenuAction(QAction *);
428 void _q_overrideMenuActionDestroyed();
429
430 //firing of events
431 void activateAction(QAction *, QAction::ActionEvent, bool self=true);
432 void activateCausedStack(const QList<QPointer<QWidget>> &, QAction *, QAction::ActionEvent,
433 bool);
434
435 void _q_actionTriggered();
436 void _q_actionHovered();
437 void _q_platformMenuAboutToShow();
438
439 bool hasMouseMoved(const QPoint &globalPos);
440
441 void updateLayoutDirection();
442
443 QPointer<QPlatformMenu> platformMenu;
444
445 QPointer<QAction> actionAboutToTrigger;
446
447 QPointer<QWidget> noReplayFor;
448
449 class ScrollerTearOffItem : public QWidget {
450 public:
451 enum Type { ScrollUp, ScrollDown };
452 ScrollerTearOffItem(Type type, QMenuPrivate *mPrivate,
453 QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
454 void paintEvent(QPaintEvent *e) override;
455 void updateScrollerRects(const QRect &rect);
456
457 private:
458 QMenuPrivate *menuPrivate;
459 Type scrollType;
460 };
461 ScrollerTearOffItem *scrollUpTearOffItem = nullptr;
462 ScrollerTearOffItem *scrollDownItem = nullptr;
463
464 void drawScroller(QPainter *painter, ScrollerTearOffItem::Type type, const QRect &rect);
465 void drawTearOff(QPainter *painter, const QRect &rect);
466 QRect rect() const;
467
468 bool considerAction(const QAction *action) const
469 {
470 Q_Q(const QMenu);
471 if (!action || action->isSeparator())
472 return false;
473 if (action->isEnabled())
474 return true;
475 return q->style()->styleHint(stylehint: QStyle::SH_Menu_AllowActiveAndDisabled, opt: nullptr, widget: q);
476 }
477
478 mutable uint maxIconWidth = 0;
479 mutable uint tabWidth = 0;
480 int motions = 0;
481 int mousePopupDelay = 0;
482
483 bool activationRecursionGuard = false;
484
485 mutable quint8 ncols = 0; // "255cols ought to be enough for anybody."
486
487 // Contains the screen of the popup point during popup(QPoint).
488 // This is to make sure the screen is remembered,
489 // when the menu contains many items on multiple screens
490 QPointer<QScreen> popupScreen;
491
492 mutable bool itemsDirty : 1;
493 mutable bool hasCheckableItems : 1;
494 bool lastContextMenu : 1;
495 bool collapsibleSeparators : 1;
496 bool toolTipsVisible : 1;
497 bool delayedPopupGuard : 1;
498 bool hasReceievedEnter : 1;
499 // Selection
500 bool hasHadMouse : 1;
501 bool aboutToHide : 1;
502 // Tear-off menus
503 bool tearoff : 1;
504 bool tornoff : 1;
505 bool tearoffHighlighted : 1;
506 //menu fading/scrolling effects
507 bool doChildEffects : 1;
508};
509
510QT_END_NAMESPACE
511
512#endif // QMENU_P_H
513

source code of qtbase/src/widgets/widgets/qmenu_p.h