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