Menu: Extract QQuickMenuPopupWindow core popup behavior
[qt:qtquickcontrols.git] / src / controls / qquickmenu.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the Qt Quick Controls 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 Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qquickmenu_p.h"
43 #include "qquickmenubar_p.h"
44 #include "qquickmenuitemcontainer_p.h"
45 #include "qquickmenupopupwindow_p.h"
46
47 #include <qdebug.h>
48 #include <qabstractitemmodel.h>
49 #include <qcursor.h>
50 #include <private/qguiapplication_p.h>
51 #include <QtGui/qpa/qplatformtheme.h>
52 #include <QtGui/qpa/qplatformmenu.h>
53 #include <qquickitem.h>
54
55 QT_BEGIN_NAMESPACE
56
57 /*!
58   \class QQuickMenu
59   \internal
60  */
61
62 /*!
63   \qmltype MenuPrivate
64   \instantiates QQuickMenu
65   \internal
66   \inqmlmodule QtQuick.Controls
67  */
68
69 /*!
70     \qmlproperty list<Object> Menu::items
71     \default
72
73     The list of items in the menu.
74
75     \l Menu only accepts objects of type \l Menu, \l MenuItem, and \l MenuSeparator
76     as children. It also supports \l Instantiator objects as long as the insertion is
77     being done manually using \l insertItem().
78
79     \code
80     Menu {
81         id: recentFilesMenu
82
83         Instantiator {
84             model: recentFilesModel
85             MenuItem {
86                 text: model.fileName
87             }
88             onObjectAdded: recentFilesMenu.insertItem(index, object)
89             onObjectRemoved: recentFilesMenu.removeItem(object)
90         }
91
92         MenuSeparator {
93             visible: recentFilesModel.count > 0
94         }
95
96         MenuItem {
97             text: "Clear menu"
98             enabled: recentFilesModel.count > 0
99             onTriggered: recentFilesModel.clear()
100         }
101     \endcode
102
103     Note that in this case, the \c index parameter passed to \l insertItem() is relative
104     to the position of the \l Instantiator in the menu, as opposed to absolute position
105     in the menu.
106
107     \sa MenuItem, MenuSeparator
108 */
109
110 /*!
111     \qmlproperty bool Menu::visible
112
113     Whether the menu should be visible. This is only enabled when the menu is used as
114     a submenu or in the menubar. Its value defaults to \c true.
115 */
116
117 /*!
118     \qmlproperty enumeration Menu::type
119
120     This property is read-only and constant, and its value is \l MenuItemType.Menu.
121 */
122
123 /*!
124     \qmlproperty string Menu::title
125
126     Title for the menu as a submenu or in a menubar.
127
128     Mnemonics are supported by prefixing the shortcut letter with \&.
129     For instance, \c "\&File" will bind the \c Alt-F shortcut to the
130     \c "File" menu. Note that not all platforms support mnemonics.
131
132     Its value defaults to the empty string.
133 */
134
135 /*!
136     \qmlproperty bool Menu::enabled
137
138     Whether the menu is enabled, and responsive to user interaction as a submenu.
139     Its value defaults to \c true.
140 */
141
142 /*!
143     \qmlproperty url Menu::iconSource
144
145     Sets the icon file or resource url for the menu icon as a submenu.
146     Defaults to the empty URL.
147
148     \sa iconName
149 */
150
151 /*!
152     \qmlproperty string Menu::iconName
153
154     Sets the icon name for the menu icon. This will pick the icon
155     with the given name from the current theme. Only works as a submenu.
156
157     Its value defaults to the empty string.
158
159     \sa iconSource
160 */
161
162 /*!
163     \qmlmethod void Menu::popup()
164
165     Opens this menu under the mouse cursor.
166     It can block on some platforms, so test it accordingly.
167 */
168
169 /*!
170     \qmlmethod MenuItem Menu::addItem(string text)
171
172     Adds an item to the menu. Returns the newly created \l MenuItem.
173
174     \sa insertItem()
175 */
176
177 /*!
178     \qmlmethod MenuItem Menu::insertItem(int before, string title)
179
180     Creates and inserts an item with title \c title at the index \c before in the current menu.
181     Returns the newly created \l MenuItem.
182
183     \sa addItem()
184 */
185
186 /*!
187     \qmlmethod void Menu::addSeparator()
188
189     Adds a separator to the menu.
190
191     \sa insertSeparator()
192 */
193
194 /*!
195     \qmlmethod void Menu::insertSeparator(int before)
196
197     Creates and inserts a separator at the index \c before in the current menu.
198
199     \sa addSeparator()
200 */
201
202 /*!
203     \qmlmethod Menu Menu::addMenu(string title)
204     Adds a submenu to the menu. Returns the newly created \l Menu.
205
206     \sa insertMenu()
207 */
208
209 /*!
210     \qmlmethod MenuItem Menu::insertMenu(int before, string title)
211
212     Creates and inserts a submenu with title \c title at the index \c before in the current menu.
213     Returns the newly created \l Menu.
214
215     \sa addMenu()
216 */
217
218 /*!
219     \qmlmethod void Menu::insertItem(int before, object item)
220
221     Inserts the \c item at the index \c before in the current menu.
222     In this case, \c item can be either a \l MenuItem, a \l MenuSeparator,
223     or a \l Menu.
224
225     \sa removeItem()
226 */
227
228 /*!
229     \qmlmethod void Menu::removeItem(item)
230
231     Removes the \c item from the menu.
232     In this case, \c item can be either a \l MenuItem, a \l MenuSeparator,
233     or a \l Menu.
234
235     \sa insertItem()
236 */
237
238 QQuickMenu::QQuickMenu(QObject *parent)
239     : QQuickMenuText(parent),
240       m_itemsCount(0),
241       m_selectedIndex(-1),
242       m_parentWindow(0),
243       m_minimumWidth(0),
244       m_popupWindow(0),
245       m_menuContentItem(0),
246       m_popupVisible(false),
247       m_containersCount(0),
248       m_xOffset(0),
249       m_yOffset(0)
250 {
251     connect(this, SIGNAL(__textChanged()), this, SIGNAL(titleChanged()));
252
253     m_platformMenu = QGuiApplicationPrivate::platformTheme()->createPlatformMenu();
254     if (m_platformMenu) {
255         connect(m_platformMenu, SIGNAL(aboutToHide()), this, SLOT(__closeMenu()));
256         if (platformItem())
257             platformItem()->setMenu(m_platformMenu);
258     }
259     if (const QFont *font = QGuiApplicationPrivate::platformTheme()->font(QPlatformTheme::MenuItemFont))
260         m_font = *const_cast<QFont*>(font);
261 }
262
263 QQuickMenu::~QQuickMenu()
264 {
265     while (!m_menuItems.empty()) {
266         QQuickMenuBase *item = m_menuItems.takeFirst();
267         if (item)
268             item->setParentMenu(0);
269     }
270
271     delete m_platformMenu;
272     m_platformMenu = 0;
273 }
274
275 void QQuickMenu::setVisible(bool v)
276 {
277     QQuickMenuBase::setVisible(v);
278     if (m_platformMenu) {
279         m_platformMenu->setVisible(v);
280         QQuickMenuBar *menubar = qobject_cast<QQuickMenuBar *>(parent());
281         if (menubar && menubar->platformMenuBar())
282             menubar->platformMenuBar()->syncMenu(m_platformMenu);
283     }
284 }
285
286 void QQuickMenu::updateText()
287 {
288     if (m_platformMenu)
289         m_platformMenu->setText(this->text());
290     QQuickMenuText::updateText();
291 }
292
293 void QQuickMenu::setMinimumWidth(int w)
294 {
295     if (w == m_minimumWidth)
296         return;
297
298     m_minimumWidth = w;
299     if (m_platformMenu)
300         m_platformMenu->setMinimumWidth(w);
301
302     emit minimumWidthChanged();
303 }
304
305 void QQuickMenu::setFont(const QFont &arg)
306 {
307     if (arg == m_font)
308         return;
309
310     m_font = arg;
311     if (m_platformMenu)
312         m_platformMenu->setFont(arg);
313 }
314
315 void QQuickMenu::setXOffset(qreal x)
316 {
317     m_xOffset = x;
318 }
319
320 void QQuickMenu::setYOffset(qreal y)
321 {
322     m_yOffset = y;
323 }
324
325 void QQuickMenu::setSelectedIndex(int index)
326 {
327     if (m_selectedIndex == index)
328         return;
329
330     m_selectedIndex = index;
331     emit __selectedIndexChanged();
332 }
333
334 void QQuickMenu::updateSelectedIndex()
335 {
336     if (QQuickMenuItem *menuItem = qobject_cast<QQuickMenuItem*>(sender())) {
337         int index = indexOfMenuItem(menuItem);
338         setSelectedIndex(index);
339     }
340 }
341
342 QQuickMenuItems QQuickMenu::menuItems()
343 {
344     return QQuickMenuItems(this, 0, &QQuickMenu::append_menuItems, &QQuickMenu::count_menuItems,
345                        &QQuickMenu::at_menuItems, &QQuickMenu::clear_menuItems);
346 }
347
348 QQuickWindow *QQuickMenu::findParentWindow()
349 {
350     if (!m_parentWindow) {
351         QQuickItem *parentAsItem = qobject_cast<QQuickItem *>(parent());
352         m_parentWindow = visualItem() ? visualItem()->window() :    // Menu as menu item case
353                          parentAsItem ? parentAsItem->window() : 0; //Menu as context menu/popup case
354     }
355     return m_parentWindow;
356 }
357
358 void QQuickMenu::popup()
359 {
360     QPoint mousePos = QCursor::pos();
361     if (QQuickWindow *parentWindow = findParentWindow())
362         mousePos = parentWindow->mapFromGlobal(mousePos);
363
364     __popup(mousePos.x(), mousePos.y());
365 }
366
367 void QQuickMenu::__popup(qreal x, qreal y, int atItemIndex)
368 {
369     if (popupVisible()) {
370         __closeMenu();
371         // Mac and Windows would normally move the menu under the cursor, so we should not
372         // return here. However, very often we want to re-contextualize the menu, and this
373         // has to be done at the application level.
374         return;
375     }
376
377     setPopupVisible(true);
378
379     QQuickMenuBase *atItem = menuItemAtIndex(atItemIndex);
380
381     QQuickWindow *parentWindow = findParentWindow();
382
383     if (m_platformMenu) {
384         QPointF screenPosition(x + m_xOffset, y + m_yOffset);
385         if (visualItem()) {
386             if (qGuiApp->isRightToLeft())
387                 screenPosition.rx() -= qMax(static_cast<qreal>(m_minimumWidth), m_menuContentItem->width());
388             screenPosition = visualItem()->mapToScene(screenPosition);
389         }
390         m_platformMenu->showPopup(parentWindow, screenPosition.toPoint(), atItem ? atItem->platformItem() : 0);
391     } else {
392         m_popupWindow = new QQuickMenuPopupWindow();
393         if (visualItem())
394             m_popupWindow->setParentItem(visualItem());
395         else
396             m_popupWindow->setParentWindow(parentWindow);
397         m_popupWindow->setPopupContentItem(m_menuContentItem);
398         m_popupWindow->setItemAt(atItem ? atItem->visualItem() : 0);
399
400         connect(m_popupWindow, SIGNAL(visibleChanged(bool)), this, SLOT(windowVisibleChanged(bool)));
401
402         m_popupWindow->setPosition(x + m_xOffset, y + m_yOffset);
403         m_popupWindow->show();
404     }
405 }
406
407 void QQuickMenu::setMenuContentItem(QQuickItem *item)
408 {
409     if (m_menuContentItem != item) {
410         m_menuContentItem = item;
411         emit menuContentItemChanged();
412     }
413 }
414
415 void QQuickMenu::setPopupVisible(bool v)
416 {
417     if (m_popupVisible != v) {
418         m_popupVisible = v;
419         emit popupVisibleChanged();
420     }
421 }
422
423 void QQuickMenu::__closeMenu()
424 {
425     setPopupVisible(false);
426     if (m_popupWindow)
427         m_popupWindow->setVisible(false);
428     m_parentWindow = 0;
429     emit __menuClosed();
430 }
431
432 void QQuickMenu::__dismissMenu()
433 {
434     QQuickMenuPopupWindow *topMenuWindow = m_popupWindow;
435     while (topMenuWindow) {
436         QQuickMenuPopupWindow *pw = qobject_cast<QQuickMenuPopupWindow *>(topMenuWindow->transientParent());
437         if (!pw)
438             topMenuWindow->dismissPopup();
439         topMenuWindow = pw;
440     }
441 }
442
443 void QQuickMenu::windowVisibleChanged(bool v)
444 {
445     if (!v) {
446         if (qobject_cast<QQuickMenuPopupWindow *>(m_popupWindow->transientParent())) {
447             m_popupWindow->transientParent()->setMouseGrabEnabled(true);
448             m_popupWindow->transientParent()->setKeyboardGrabEnabled(true);
449         }
450         m_popupWindow->deleteLater();
451         m_popupWindow = 0;
452         __closeMenu();
453     }
454 }
455
456 void QQuickMenu::itemIndexToListIndex(int itemIndex, int *listIndex, int *containerIndex) const
457 {
458     *listIndex = -1;
459     QQuickMenuItemContainer *container = 0;
460     while (itemIndex >= 0 && ++*listIndex < m_menuItems.count())
461         if ((container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[*listIndex])))
462             itemIndex -= container->items().count();
463         else
464             --itemIndex;
465
466     if (container)
467         *containerIndex = container->items().count() + itemIndex;
468     else
469         *containerIndex = -1;
470 }
471
472 int QQuickMenu::itemIndexForListIndex(int listIndex) const
473 {
474     int index = 0;
475     int i = 0;
476     while (i < listIndex && i < m_menuItems.count())
477         if (QQuickMenuItemContainer *container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[i++]))
478             index += container->items().count();
479         else
480             ++index;
481
482     return index;
483 }
484
485 QQuickMenuBase *QQuickMenu::nextMenuItem(QQuickMenu::MenuItemIterator *it) const
486 {
487     if (it->containerIndex != -1) {
488         QQuickMenuItemContainer *container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[it->index]);
489         if (++it->containerIndex < container->items().count())
490             return container->items()[it->containerIndex];
491     }
492
493     if (++it->index < m_menuItems.count()) {
494         if (QQuickMenuItemContainer *container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[it->index])) {
495             it->containerIndex = 0;
496             return container->items()[0];
497         } else {
498             it->containerIndex = -1;
499             return m_menuItems[it->index];
500         }
501     }
502
503     return 0;
504 }
505
506 QQuickMenuBase *QQuickMenu::menuItemAtIndex(int index) const
507 {
508     if (0 <= index && index < m_itemsCount) {
509         if (!m_containersCount) {
510             return m_menuItems[index];
511         } else if (m_containersCount == 1 && m_menuItems.count() == 1) {
512             QQuickMenuItemContainer *container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[0]);
513             return container->items()[index];
514         } else {
515             int containerIndex;
516             int i;
517             itemIndexToListIndex(index, &i, &containerIndex);
518             if (containerIndex != -1) {
519                 QQuickMenuItemContainer *container = qobject_cast<QQuickMenuItemContainer *>(m_menuItems[i]);
520                 return container->items()[containerIndex];
521             } else {
522                 return m_menuItems[i];
523             }
524         }
525     }
526
527     return 0;
528 }
529
530 bool QQuickMenu::contains(QQuickMenuBase *item)
531 {
532     if (item->container())
533         return item->container()->items().contains(item);
534
535     return m_menuItems.contains(item);
536 }
537
538 int QQuickMenu::indexOfMenuItem(QQuickMenuBase *item) const
539 {
540     if (!item)
541         return -1;
542     if (item->container()) {
543         int containerIndex = m_menuItems.indexOf(item->container());
544         if (containerIndex == -1)
545             return -1;
546         int index = item->container()->items().indexOf(item);
547         return index == -1 ? -1 : itemIndexForListIndex(containerIndex) + index;
548     } else {
549         int index = m_menuItems.indexOf(item);
550         return index == -1 ? -1 : itemIndexForListIndex(index);
551     }
552 }
553
554 QQuickMenuItem *QQuickMenu::addItem(QString title)
555 {
556     return insertItem(m_itemsCount, title);
557 }
558
559 QQuickMenuItem *QQuickMenu::insertItem(int index, QString title)
560 {
561     QQuickMenuItem *item = new QQuickMenuItem(this);
562     item->setText(title);
563     insertItem(index, item);
564     return item;
565 }
566
567 void QQuickMenu::addSeparator()
568 {
569     insertSeparator(m_itemsCount);
570 }
571
572 void QQuickMenu::insertSeparator(int index)
573 {
574     QQuickMenuSeparator *item = new QQuickMenuSeparator(this);
575     insertItem(index, item);
576 }
577
578 void QQuickMenu::insertItem(int index, QQuickMenuBase *menuItem)
579 {
580     if (!menuItem)
581         return;
582     int itemIndex;
583     if (m_containersCount) {
584         QQuickMenuItemContainer *container = menuItem->parent() != this ? m_containers[menuItem->parent()] : 0;
585         if (container) {
586             container->insertItem(index, menuItem);
587             itemIndex = itemIndexForListIndex(m_menuItems.indexOf(container)) + index;
588         } else {
589             itemIndex = itemIndexForListIndex(index);
590             m_menuItems.insert(itemIndex, menuItem);
591         }
592     } else {
593         itemIndex = index;
594         m_menuItems.insert(index, menuItem);
595     }
596
597     setupMenuItem(menuItem, itemIndex);
598     emit itemsChanged();
599 }
600
601 void QQuickMenu::removeItem(QQuickMenuBase *menuItem)
602 {
603     if (!menuItem)
604         return;
605     menuItem->setParentMenu(0);
606
607     QQuickMenuItemContainer *container = menuItem->parent() != this ? m_containers[menuItem->parent()] : 0;
608     if (container)
609         container->removeItem(menuItem);
610     else
611         m_menuItems.removeOne(menuItem);
612
613     --m_itemsCount;
614     emit itemsChanged();
615 }
616
617 void QQuickMenu::clear()
618 {
619     m_containers.clear();
620     m_containersCount = 0;
621
622     while (!m_menuItems.empty())
623         delete m_menuItems.takeFirst();
624     m_itemsCount = 0;
625 }
626
627 void QQuickMenu::setupMenuItem(QQuickMenuBase *item, int platformIndex)
628 {
629     item->setParentMenu(this);
630     if (m_platformMenu) {
631         QPlatformMenuItem *before = 0;
632         if (platformIndex != -1)
633             before = m_platformMenu->menuItemAt(platformIndex);
634         m_platformMenu->insertMenuItem(item->platformItem(), before);
635     }
636     ++m_itemsCount;
637 }
638
639 void QQuickMenu::append_menuItems(QQuickMenuItems *list, QObject *o)
640 {
641     if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(list->object)) {
642         Q_ASSERT(o->parent() == menu);
643
644         if (QQuickMenuBase *menuItem = qobject_cast<QQuickMenuBase *>(o)) {
645             menu->m_menuItems.append(menuItem);
646             menu->setupMenuItem(menuItem);
647         } else {
648             QQuickMenuItemContainer *menuItemContainer = new QQuickMenuItemContainer(menu);
649             menu->m_menuItems.append(menuItemContainer);
650             menu->m_containers.insert(o, menuItemContainer);
651             menuItemContainer->setParentMenu(menu);
652             ++menu->m_containersCount;
653             foreach (QObject *child, o->children()) {
654                 if (QQuickMenuBase *item = qobject_cast<QQuickMenuBase *>(child)) {
655                     menuItemContainer->insertItem(-1, item);
656                     menu->setupMenuItem(item);
657                 }
658             }
659         }
660     }
661 }
662
663 int QQuickMenu::count_menuItems(QQuickMenuItems *list)
664 {
665     if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(list->object))
666         return menu->m_itemsCount;
667
668     return 0;
669 }
670
671 QObject *QQuickMenu::at_menuItems(QQuickMenuItems *list, int index)
672 {
673     if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(list->object))
674         return menu->menuItemAtIndex(index);
675
676     return 0;
677 }
678
679 void QQuickMenu::clear_menuItems(QQuickMenuItems *list)
680 {
681     if (QQuickMenu *menu = qobject_cast<QQuickMenu *>(list->object))
682         menu->clear();
683 }
684
685 QT_END_NAMESPACE