source: trunk/src/gui/widgets/qmenu.cpp@ 944

Last change on this file since 944 was 846, checked in by Dmitry A. Kuminov, 15 years ago

trunk: Merged in qt 4.7.2 sources from branches/vendor/nokia/qt.

File size: 112.1 KB
Line 
1/****************************************************************************
2**
3** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4** All rights reserved.
5** Contact: Nokia Corporation ([email protected])
6**
7** This file is part of the QtGui module of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:LGPL$
10** Commercial Usage
11** Licensees holding valid Qt Commercial licenses may use this file in
12** accordance with the Qt Commercial License Agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and Nokia.
15**
16** GNU Lesser General Public License Usage
17** Alternatively, this file may be used under the terms of the GNU Lesser
18** General Public License version 2.1 as published by the Free Software
19** Foundation and appearing in the file LICENSE.LGPL included in the
20** packaging of this file. Please review the following information to
21** ensure the GNU Lesser General Public License version 2.1 requirements
22** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23**
24** In addition, as a special exception, Nokia gives you certain additional
25** rights. These rights are described in the Nokia Qt LGPL Exception
26** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27**
28** GNU General Public License Usage
29** Alternatively, this file may be used under the terms of the GNU
30** General Public License version 3.0 as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL included in the
32** packaging of this file. Please review the following information to
33** ensure the GNU General Public License version 3.0 requirements will be
34** met: http://www.gnu.org/copyleft/gpl.html.
35**
36** If you have questions regarding the use of this file, please contact
37** Nokia at [email protected].
38** $QT_END_LICENSE$
39**
40****************************************************************************/
41
42#include "qmenu.h"
43
44#ifndef QT_NO_MENU
45
46#include "qdebug.h"
47#include "qstyle.h"
48#include "qevent.h"
49#include "qtimer.h"
50#include "qlayout.h"
51#include "qpainter.h"
52#include "qapplication.h"
53#include "qdesktopwidget.h"
54#ifndef QT_NO_ACCESSIBILITY
55# include "qaccessible.h"
56#endif
57#ifndef QT_NO_EFFECTS
58# include <private/qeffects_p.h>
59#endif
60#ifndef QT_NO_WHATSTHIS
61# include <qwhatsthis.h>
62#endif
63
64#include "qmenu_p.h"
65#include "qmenubar_p.h"
66#include "qwidgetaction.h"
67#include "qtoolbutton.h"
68#include "qpushbutton.h"
69#include <private/qpushbutton_p.h>
70#include <private/qaction_p.h>
71#include <private/qsoftkeymanager_p.h>
72#ifdef QT3_SUPPORT
73#include <qmenudata.h>
74#endif // QT3_SUPPORT
75
76#ifdef Q_WS_X11
77# include <private/qt_x11_p.h>
78#endif
79
80#if defined(Q_WS_MAC) && !defined(QT_NO_EFFECTS)
81# include <private/qcore_mac_p.h>
82# include <private/qt_cocoa_helpers_mac_p.h>
83#endif
84
85
86QT_BEGIN_NAMESPACE
87
88QMenu *QMenuPrivate::mouseDown = 0;
89int QMenuPrivate::sloppyDelayTimer = 0;
90
91/* QMenu code */
92// internal class used for the torn off popup
93class QTornOffMenu : public QMenu
94{
95 Q_OBJECT
96 class QTornOffMenuPrivate : public QMenuPrivate
97 {
98 Q_DECLARE_PUBLIC(QMenu)
99 public:
100 QTornOffMenuPrivate(QMenu *p) : causedMenu(p) {
101 tornoff = 1;
102 causedPopup.widget = 0;
103 causedPopup.action = ((QTornOffMenu*)p)->d_func()->causedPopup.action;
104 causedStack = ((QTornOffMenu*)p)->d_func()->calcCausedStack();
105 }
106 QList<QPointer<QWidget> > calcCausedStack() const { return causedStack; }
107 QPointer<QMenu> causedMenu;
108 QList<QPointer<QWidget> > causedStack;
109 };
110public:
111 QTornOffMenu(QMenu *p) : QMenu(*(new QTornOffMenuPrivate(p)))
112 {
113 Q_D(QTornOffMenu);
114 // make the torn-off menu a sibling of p (instead of a child)
115 QWidget *parentWidget = d->causedStack.isEmpty() ? p : d->causedStack.last();
116 if (parentWidget->parentWidget())
117 parentWidget = parentWidget->parentWidget();
118 setParent(parentWidget, Qt::Window | Qt::Tool);
119 setAttribute(Qt::WA_DeleteOnClose, true);
120 setAttribute(Qt::WA_X11NetWmWindowTypeMenu, true);
121 setWindowTitle(p->windowTitle());
122 setEnabled(p->isEnabled());
123 //QObject::connect(this, SIGNAL(triggered(QAction*)), this, SLOT(onTrigger(QAction*)));
124 //QObject::connect(this, SIGNAL(hovered(QAction*)), this, SLOT(onHovered(QAction*)));
125 QList<QAction*> items = p->actions();
126 for(int i = 0; i < items.count(); i++)
127 addAction(items.at(i));
128 }
129 void syncWithMenu(QMenu *menu, QActionEvent *act)
130 {
131 Q_D(QTornOffMenu);
132 if(menu != d->causedMenu)
133 return;
134 if (act->type() == QEvent::ActionAdded) {
135 insertAction(act->before(), act->action());
136 } else if (act->type() == QEvent::ActionRemoved)
137 removeAction(act->action());
138 }
139 void actionEvent(QActionEvent *e)
140 {
141 QMenu::actionEvent(e);
142 setFixedSize(sizeHint());
143 }
144public slots:
145 void onTrigger(QAction *action) { d_func()->activateAction(action, QAction::Trigger, false); }
146 void onHovered(QAction *action) { d_func()->activateAction(action, QAction::Hover, false); }
147private:
148 Q_DECLARE_PRIVATE(QTornOffMenu)
149 friend class QMenuPrivate;
150};
151
152void QMenuPrivate::init()
153{
154 Q_Q(QMenu);
155#ifndef QT_NO_WHATSTHIS
156 q->setAttribute(Qt::WA_CustomWhatsThis);
157#endif
158 q->setAttribute(Qt::WA_X11NetWmWindowTypePopupMenu);
159 defaultMenuAction = menuAction = new QAction(q);
160 menuAction->d_func()->menu = q;
161 q->setMouseTracking(q->style()->styleHint(QStyle::SH_Menu_MouseTracking, 0, q));
162 if (q->style()->styleHint(QStyle::SH_Menu_Scrollable, 0, q)) {
163 scroll = new QMenuPrivate::QMenuScroller;
164 scroll->scrollFlags = QMenuPrivate::QMenuScroller::ScrollNone;
165 }
166
167#ifdef QT_SOFTKEYS_ENABLED
168 selectAction = QSoftKeyManager::createKeyedAction(QSoftKeyManager::SelectSoftKey, Qt::Key_Select, q);
169 cancelAction = QSoftKeyManager::createKeyedAction(QSoftKeyManager::CancelSoftKey, Qt::Key_Back, q);
170 selectAction->setPriority(QAction::HighPriority);
171 cancelAction->setPriority(QAction::HighPriority);
172 q->addAction(selectAction);
173 q->addAction(cancelAction);
174#endif
175}
176
177int QMenuPrivate::scrollerHeight() const
178{
179 Q_Q(const QMenu);
180 return qMax(QApplication::globalStrut().height(), q->style()->pixelMetric(QStyle::PM_MenuScrollerHeight, 0, q));
181}
182
183// Windows, OS/2 and KDE allows menus to cover the taskbar, while GNOME and Mac don't
184QRect QMenuPrivate::popupGeometry(const QWidget *widget) const
185{
186#if defined(Q_WS_WIN) || defined(Q_WS_PM)
187 return QApplication::desktop()->screenGeometry(widget);
188#elif defined Q_WS_X11
189 if (X11->desktopEnvironment == DE_KDE)
190 return QApplication::desktop()->screenGeometry(widget);
191 else
192 return QApplication::desktop()->availableGeometry(widget);
193#else
194 return QApplication::desktop()->availableGeometry(widget);
195#endif
196}
197
198// Windows, OS/2 and KDE allows menus to cover the taskbar, while GNOME and Mac don't
199QRect QMenuPrivate::popupGeometry(int screen) const
200{
201#if defined(Q_WS_WIN) || defined(Q_WS_PM)
202 return QApplication::desktop()->screenGeometry(screen);
203#elif defined Q_WS_X11
204 if (X11->desktopEnvironment == DE_KDE)
205 return QApplication::desktop()->screenGeometry(screen);
206 else
207 return QApplication::desktop()->availableGeometry(screen);
208#else
209 return QApplication::desktop()->availableGeometry(screen);
210#endif
211}
212
213QList<QPointer<QWidget> > QMenuPrivate::calcCausedStack() const
214{
215 QList<QPointer<QWidget> > ret;
216 for(QWidget *widget = causedPopup.widget; widget; ) {
217 ret.append(widget);
218 if (QTornOffMenu *qtmenu = qobject_cast<QTornOffMenu*>(widget))
219 ret += qtmenu->d_func()->causedStack;
220 if (QMenu *qmenu = qobject_cast<QMenu*>(widget))
221 widget = qmenu->d_func()->causedPopup.widget;
222 else
223 break;
224 }
225 return ret;
226}
227
228void QMenuPrivate::updateActionRects() const
229{
230 Q_Q(const QMenu);
231 if (!itemsDirty)
232 return;
233
234 q->ensurePolished();
235
236 //let's reinitialize the buffer
237 actionRects.resize(actions.count());
238 actionRects.fill(QRect());
239
240 //let's try to get the last visible action
241 int lastVisibleAction = actions.count() - 1;
242 for(;lastVisibleAction >= 0; --lastVisibleAction) {
243 const QAction *action = actions.at(lastVisibleAction);
244 if (action->isVisible()) {
245 //removing trailing separators
246 if (action->isSeparator() && collapsibleSeparators)
247 continue;
248 break;
249 }
250 }
251
252 int max_column_width = 0,
253 dh = popupGeometry(q).height(),
254 y = 0;
255 QStyle *style = q->style();
256 QStyleOption opt;
257 opt.init(q);
258 const int hmargin = style->pixelMetric(QStyle::PM_MenuHMargin, &opt, q),
259 vmargin = style->pixelMetric(QStyle::PM_MenuVMargin, &opt, q),
260 icone = style->pixelMetric(QStyle::PM_SmallIconSize, &opt, q);
261 const int fw = style->pixelMetric(QStyle::PM_MenuPanelWidth, &opt, q);
262 const int deskFw = style->pixelMetric(QStyle::PM_MenuDesktopFrameWidth, &opt, q);
263 const int tearoffHeight = tearoff ? style->pixelMetric(QStyle::PM_MenuTearoffHeight, &opt, q) : 0;
264
265 //for compatibility now - will have to refactor this away
266 tabWidth = 0;
267 maxIconWidth = 0;
268 hasCheckableItems = false;
269 ncols = 1;
270 sloppyAction = 0;
271
272 for (int i = 0; i < actions.count(); ++i) {
273 QAction *action = actions.at(i);
274 if (action->isSeparator() || !action->isVisible() || widgetItems.contains(action))
275 continue;
276 //..and some members
277 hasCheckableItems |= action->isCheckable();
278 QIcon is = action->icon();
279 if (!is.isNull()) {
280 maxIconWidth = qMax<uint>(maxIconWidth, icone + 4);
281 }
282 }
283
284 //calculate size
285 QFontMetrics qfm = q->fontMetrics();
286 bool previousWasSeparator = true; // this is true to allow removing the leading separators
287 for(int i = 0; i <= lastVisibleAction; i++) {
288 QAction *action = actions.at(i);
289
290 if (!action->isVisible() ||
291 (collapsibleSeparators && previousWasSeparator && action->isSeparator()))
292 continue; // we continue, this action will get an empty QRect
293
294 previousWasSeparator = action->isSeparator();
295
296 //let the style modify the above size..
297 QStyleOptionMenuItem opt;
298 q->initStyleOption(&opt, action);
299 const QFontMetrics &fm = opt.fontMetrics;
300
301 QSize sz;
302 if (QWidget *w = widgetItems.value(action)) {
303 sz = w->sizeHint().expandedTo(w->minimumSize()).expandedTo(w->minimumSizeHint()).boundedTo(w->maximumSize());
304 } else {
305 //calc what I think the size is..
306 if (action->isSeparator()) {
307 sz = QSize(2, 2);
308 } else {
309 QString s = action->text();
310 int t = s.indexOf(QLatin1Char('\t'));
311 if (t != -1) {
312 tabWidth = qMax(int(tabWidth), qfm.width(s.mid(t+1)));
313 s = s.left(t);
314 #ifndef QT_NO_SHORTCUT
315 } else {
316 QKeySequence seq = action->shortcut();
317 if (!seq.isEmpty())
318 tabWidth = qMax(int(tabWidth), qfm.width(seq));
319 #endif
320 }
321 sz.setWidth(fm.boundingRect(QRect(), Qt::TextSingleLine | Qt::TextShowMnemonic, s).width());
322 sz.setHeight(qMax(fm.height(), qfm.height()));
323
324 QIcon is = action->icon();
325 if (!is.isNull()) {
326 QSize is_sz = QSize(icone, icone);
327 if (is_sz.height() > sz.height())
328 sz.setHeight(is_sz.height());
329 }
330 }
331 sz = style->sizeFromContents(QStyle::CT_MenuItem, &opt, sz, q);
332 }
333
334
335 if (!sz.isEmpty()) {
336 max_column_width = qMax(max_column_width, sz.width());
337 //wrapping
338 if (!scroll &&
339 y+sz.height()+vmargin > dh - (deskFw * 2)) {
340 ncols++;
341 y = vmargin;
342 }
343 y += sz.height();
344 //update the item
345 actionRects[i] = QRect(0, 0, sz.width(), sz.height());
346 }
347 }
348
349 max_column_width += tabWidth; //finally add in the tab width
350 const int sfcMargin = style->sizeFromContents(QStyle::CT_Menu, &opt, QApplication::globalStrut(), q).width() - QApplication::globalStrut().width();
351 const int min_column_width = q->minimumWidth() - (sfcMargin + leftmargin + rightmargin + 2 * (fw + hmargin));
352 max_column_width = qMax(min_column_width, max_column_width);
353
354
355 //calculate position
356 const int base_y = vmargin + fw + topmargin +
357 (scroll ? scroll->scrollOffset : 0) +
358 tearoffHeight;
359 int x = hmargin + fw + leftmargin;
360 y = base_y;
361
362 for(int i = 0; i < actions.count(); i++) {
363 QRect &rect = actionRects[i];
364 if (rect.isNull())
365 continue;
366 if (!scroll &&
367 y+rect.height() > dh - deskFw * 2) {
368 x += max_column_width + hmargin;
369 y = base_y;
370 }
371 rect.translate(x, y); //move
372 rect.setWidth(max_column_width); //uniform width
373
374 //we need to update the widgets geometry
375 if (QWidget *widget = widgetItems.value(actions.at(i))) {
376 widget->setGeometry(rect);
377 widget->setVisible(actions.at(i)->isVisible());
378 }
379
380 y += rect.height();
381 }
382 itemsDirty = 0;
383}
384
385QRect QMenuPrivate::actionRect(QAction *act) const
386{
387 int index = actions.indexOf(act);
388 if (index == -1)
389 return QRect();
390
391 updateActionRects();
392
393 //we found the action
394 return actionRects.at(index);
395}
396
397#if defined(Q_WS_MAC)
398static const qreal MenuFadeTimeInSec = 0.150;
399#endif
400
401void QMenuPrivate::hideUpToMenuBar()
402{
403 Q_Q(QMenu);
404 bool fadeMenus = q->style()->styleHint(QStyle::SH_Menu_FadeOutOnHide);
405 if (!tornoff) {
406 QWidget *caused = causedPopup.widget;
407 hideMenu(q); //hide after getting causedPopup
408 while(caused) {
409#ifndef QT_NO_MENUBAR
410 if (QMenuBar *mb = qobject_cast<QMenuBar*>(caused)) {
411 mb->d_func()->setCurrentAction(0);
412 mb->d_func()->setKeyboardMode(false);
413 caused = 0;
414 } else
415#endif
416 if (QMenu *m = qobject_cast<QMenu*>(caused)) {
417 caused = m->d_func()->causedPopup.widget;
418 if (!m->d_func()->tornoff)
419 hideMenu(m, fadeMenus);
420 if (!fadeMenus) // Mac doesn't clear the action until after hidden.
421 m->d_func()->setCurrentAction(0);
422 } else { caused = 0;
423 }
424 }
425#if defined(Q_WS_MAC)
426 if (fadeMenus) {
427 QEventLoop eventLoop;
428 QTimer::singleShot(int(MenuFadeTimeInSec * 1000), &eventLoop, SLOT(quit()));
429 QMacWindowFader::currentFader()->performFade();
430 eventLoop.exec();
431 }
432#endif
433 }
434 setCurrentAction(0);
435}
436
437void QMenuPrivate::hideMenu(QMenu *menu, bool justRegister)
438{
439 if (!menu)
440 return;
441#if !defined(QT_NO_EFFECTS)
442 menu->blockSignals(true);
443 aboutToHide = true;
444 // Flash item which is about to trigger (if any).
445 if (menu->style()->styleHint(QStyle::SH_Menu_FlashTriggeredItem)
446 && currentAction && currentAction == actionAboutToTrigger
447 && menu->actions().contains(currentAction)) {
448 QEventLoop eventLoop;
449 QAction *activeAction = currentAction;
450
451 menu->setActiveAction(0);
452 QTimer::singleShot(60, &eventLoop, SLOT(quit()));
453 eventLoop.exec();
454
455 // Select and wait 20 ms.
456 menu->setActiveAction(activeAction);
457 QTimer::singleShot(20, &eventLoop, SLOT(quit()));
458 eventLoop.exec();
459 }
460
461 // Fade out.
462 if (menu->style()->styleHint(QStyle::SH_Menu_FadeOutOnHide)) {
463 // ### Qt 4.4:
464 // Should be something like: q->transitionWindow(Qt::FadeOutTransition, MenuFadeTimeInSec);
465 // Hopefully we'll integrate qt/research/windowtransitions into main before 4.4.
466 // Talk to Richard, Trenton or Bjoern.
467#if defined(Q_WS_MAC)
468 if (justRegister) {
469 QMacWindowFader::currentFader()->setFadeDuration(MenuFadeTimeInSec);
470 QMacWindowFader::currentFader()->registerWindowToFade(menu);
471 } else {
472 macWindowFade(qt_mac_window_for(menu), MenuFadeTimeInSec);
473 }
474
475#endif // Q_WS_MAC
476 }
477 aboutToHide = false;
478 menu->blockSignals(false);
479#endif // QT_NO_EFFECTS
480 if (!justRegister)
481 menu->hide();
482}
483
484void QMenuPrivate::popupAction(QAction *action, int delay, bool activateFirst)
485{
486 Q_Q(QMenu);
487 if (action && action->isEnabled()) {
488 if (!delay)
489 q->internalDelayedPopup();
490 else if (!menuDelayTimer.isActive() && (!action->menu() || !action->menu()->isVisible()))
491 menuDelayTimer.start(delay, q);
492 if (activateFirst && action->menu())
493 action->menu()->d_func()->setFirstActionActive();
494 } else if (QMenu *menu = activeMenu) { //hide the current item
495 activeMenu = 0;
496 hideMenu(menu);
497 }
498}
499
500void QMenuPrivate::setSyncAction()
501{
502 Q_Q(QMenu);
503 QAction *current = currentAction;
504 if(current && (!current->isEnabled() || current->menu() || current->isSeparator()))
505 current = 0;
506 for(QWidget *caused = q; caused;) {
507 if (QMenu *m = qobject_cast<QMenu*>(caused)) {
508 caused = m->d_func()->causedPopup.widget;
509 if (m->d_func()->eventLoop)
510 m->d_func()->syncAction = current; // synchronous operation
511 } else {
512 break;
513 }
514 }
515}
516
517
518void QMenuPrivate::setFirstActionActive()
519{
520 Q_Q(QMenu);
521 updateActionRects();
522 for(int i = 0, saccum = 0; i < actions.count(); i++) {
523 const QRect &rect = actionRects.at(i);
524 if (rect.isNull())
525 continue;
526 if (scroll && scroll->scrollFlags & QMenuScroller::ScrollUp) {
527 saccum -= rect.height();
528 if (saccum > scroll->scrollOffset - scrollerHeight())
529 continue;
530 }
531 QAction *act = actions.at(i);
532 if (!act->isSeparator() &&
533 (q->style()->styleHint(QStyle::SH_Menu_AllowActiveAndDisabled, 0, q)
534 || act->isEnabled())) {
535 setCurrentAction(act);
536 break;
537 }
538 }
539}
540
541// popup == -1 means do not popup, 0 means immediately, others mean use a timer
542void QMenuPrivate::setCurrentAction(QAction *action, int popup, SelectionReason reason, bool activateFirst)
543{
544 Q_Q(QMenu);
545 tearoffHighlighted = 0;
546 if (currentAction)
547 q->update(actionRect(currentAction));
548
549 sloppyAction = 0;
550 if (!sloppyRegion.isEmpty())
551 sloppyRegion = QRegion();
552 QMenu *hideActiveMenu = activeMenu;
553#ifndef QT_NO_STATUSTIP
554 QAction *previousAction = currentAction;
555#endif
556#ifdef QT3_SUPPORT
557 emitHighlighted = action;
558#endif
559
560 currentAction = action;
561 if (action) {
562 if (!action->isSeparator()) {
563 activateAction(action, QAction::Hover);
564 if (popup != -1) {
565 hideActiveMenu = 0; //will be done "later"
566 // if the menu is visible then activate the required action,
567 // otherwise we just mark the action as currentAction
568 // and activate it when the menu will be popuped.
569 if (q->isVisible())
570 popupAction(currentAction, popup, activateFirst);
571 }
572 q->update(actionRect(action));
573
574 if (reason == SelectedFromKeyboard) {
575 QWidget *widget = widgetItems.value(action);
576 if (widget) {
577 if (widget->focusPolicy() != Qt::NoFocus)
578 widget->setFocus(Qt::TabFocusReason);
579 } else {
580 //when the action has no QWidget, the QMenu itself should
581 // get the focus
582 // Since the menu is a pop-up, it uses the popup reason.
583 if (!q->hasFocus()) {
584 q->setFocus(Qt::PopupFocusReason);
585 }
586 }
587 }
588 } else { //action is a separator
589 if (popup != -1)
590 hideActiveMenu = 0; //will be done "later"
591 }
592#ifndef QT_NO_STATUSTIP
593 } else if (previousAction) {
594 previousAction->d_func()->showStatusText(topCausedWidget(), QString());
595#endif
596 }
597 if (hideActiveMenu) {
598 activeMenu = 0;
599#ifndef QT_NO_EFFECTS
600 // kill any running effect
601 qFadeEffect(0);
602 qScrollEffect(0);
603#endif
604 hideMenu(hideActiveMenu);
605 }
606}
607
608//return the top causedPopup.widget that is not a QMenu
609QWidget *QMenuPrivate::topCausedWidget() const
610{
611 QWidget* top = causedPopup.widget;
612 while (QMenu* m = qobject_cast<QMenu *>(top))
613 top = m->d_func()->causedPopup.widget;
614 return top;
615}