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

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