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