1/*
2 This file is part of the KDE project.
3
4 Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
5 Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
6
7 Permission is hereby granted, free of charge, to any person obtaining a
8 copy of this software and associated documentation files (the "Software"),
9 to deal in the Software without restriction, including without limitation
10 the rights to use, copy, modify, merge, publish, distribute, sublicense,
11 and/or sell copies of the Software, and to permit persons to whom the
12 Software is furnished to do so, subject to the following conditions:
13
14 The above copyright notice and this permission notice shall be included in
15 all copies or substantial portions of the Software.
16
17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 DEALINGS IN THE SOFTWARE.
24*/
25
26#include "menuwidget.h"
27
28#include <QMenu>
29#include <QDesktopWidget>
30#include <QGraphicsView>
31#include <QGraphicsLinearLayout>
32
33#include <KWindowSystem>
34#include <KDebug>
35#include <KApplication>
36
37MenuWidget::MenuWidget(QGraphicsView *view) :
38 QGraphicsWidget(),
39 m_mouseTimer(new QTimer(this)),
40 m_actionTimer(new QTimer(this)),
41 m_view(view),
42 m_layout(new QGraphicsLinearLayout(this)),
43 m_currentButton(0),
44 m_contentBottomMargin(0),
45 m_mousePosition(-1, -1),
46 m_visibleMenu(0),
47 m_menu(0)
48{
49 connect(m_actionTimer, SIGNAL(timeout()), SLOT(slotUpdateActions()));
50 connect(m_mouseTimer, SIGNAL(timeout()), SLOT(slotCheckActiveItem()));
51}
52
53MenuWidget::~MenuWidget()
54{
55 while (!m_buttons.isEmpty()) {
56 delete m_buttons.front();
57 m_buttons.pop_front();
58 }
59}
60
61void MenuWidget::setMenu(QMenu *menu)
62{
63 if (m_menu) {
64 disconnect(m_menu, SIGNAL(destroyed()), this, SLOT(slotMenuDestroyed()));
65 m_menu->removeEventFilter(this);
66 }
67 if (menu) {
68 if (m_mouseTimer->isActive()) {
69 m_mouseTimer->stop();
70 }
71 m_visibleMenu = 0;
72 m_menu = menu;
73 connect(m_menu, SIGNAL(destroyed()), SLOT(slotMenuDestroyed()), Qt::UniqueConnection);
74 m_menu->installEventFilter(this);
75 slotUpdateActions();
76 }
77}
78
79void MenuWidget::initLayout()
80{
81 MenuButton* button = 0;
82
83 if (!m_menu) {
84 return;
85 }
86
87 foreach (QAction* action, m_menu->actions())
88 {
89 button = createButton(action);
90 if (button) {
91 m_layout->addItem(button);
92 button->setMenu(action->menu());
93 m_buttons << button;
94 }
95 }
96
97 //Assume all buttons have same margins
98 if (button) {
99 m_contentBottomMargin = button->bottomMargin();
100 }
101}
102
103bool MenuWidget::eventFilter(QObject* object, QEvent* event)
104{
105 bool filtered;
106 if (object == m_menu) {
107 filtered = menuEventFilter(event);
108 } else {
109 filtered = subMenuEventFilter(static_cast<QMenu*>(object), event);
110 }
111 return filtered ? true : QGraphicsWidget::eventFilter(object, event);
112}
113
114bool MenuWidget::menuEventFilter(QEvent* event)
115{
116 switch (event->type()) {
117 case QEvent::ActionAdded:
118 case QEvent::ActionRemoved:
119 case QEvent::ActionChanged:
120 // Try to limit layout updates
121 m_actionTimer->start(500);
122 break;
123 default:
124 break;
125 }
126 return false;
127}
128
129bool MenuWidget::subMenuEventFilter(QObject* object, QEvent* event)
130{
131 QMenu *menu = static_cast<QMenu*>(object);
132
133 if (event->type() == QEvent::KeyPress) {
134 menu->removeEventFilter(this);
135 QApplication::sendEvent(menu, event);
136 menu->installEventFilter(this);
137 if (!event->isAccepted()) {
138 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
139 switch (keyEvent->key()) {
140 case Qt::Key_Left:
141 showLeftRightMenu(false);
142 break;
143 case Qt::Key_Right:
144 showLeftRightMenu(true);
145 break;
146 case Qt::Key_Escape:
147 menu->hide();
148 break;
149 default:
150 break;
151 }
152 }
153 return true;
154 }
155 return false;
156}
157
158void MenuWidget::slotMenuDestroyed()
159{
160 m_menu = 0;
161 m_visibleMenu = 0;
162 m_currentButton = 0;
163}
164
165void MenuWidget::slotCheckActiveItem()
166{
167 MenuButton* buttonBelow = 0;
168 QPoint pos = m_view->mapFromGlobal(QCursor::pos());
169 QGraphicsItem* item = m_view->itemAt(pos);
170
171 if (pos == m_mousePosition) {
172 return;
173 } else {
174 m_mousePosition = pos;
175 }
176
177 if (item) {
178 buttonBelow = qobject_cast<MenuButton*>(item->toGraphicsObject());
179 }
180
181 if (!buttonBelow) {
182 return;
183 }
184
185 if (buttonBelow != m_currentButton) {
186 if (m_currentButton && m_currentButton->nativeWidget()) {
187 m_currentButton->nativeWidget()->setDown(false);
188 m_currentButton->setHovered(false);
189 }
190 m_currentButton = buttonBelow;
191 if (m_currentButton->nativeWidget()) {
192 m_currentButton->nativeWidget()->setDown(true);
193 }
194 m_visibleMenu = showMenu();
195 }
196}
197
198void MenuWidget::slotMenuAboutToHide()
199{
200 if (m_currentButton && m_currentButton->nativeWidget()) {
201 m_currentButton->nativeWidget()->setDown(false);
202 }
203
204 if (m_mouseTimer->isActive()) {
205 m_mouseTimer->stop();
206 }
207 m_visibleMenu = 0;
208 emit aboutToHide();
209}
210
211void MenuWidget::slotButtonClicked()
212{
213 m_currentButton = qobject_cast<MenuButton*>(sender());
214
215 if (m_currentButton && m_currentButton->nativeWidget()) {
216 m_currentButton->nativeWidget()->setDown(true);
217 }
218 m_visibleMenu = showMenu();
219 // Start auto navigation after click
220 if (!m_mouseTimer->isActive())
221 m_mouseTimer->start(100);
222}
223
224void MenuWidget::slotUpdateActions()
225{
226 if (m_visibleMenu) {
227 return; // Later
228 }
229
230 m_actionTimer->stop();
231 m_currentButton = 0;
232 foreach (MenuButton *button, m_buttons) {
233 disconnect(button, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
234 m_layout->removeItem(button);
235 button->hide();
236 m_buttons.removeOne(button);
237 delete button;
238 }
239 initLayout();
240 // Menu may be empty on application startup
241 // slotUpdateActions will be called later by eventFilter()
242 if (m_menu && m_menu->actions().length()) {
243 emit needResize();
244 }
245}
246
247void MenuWidget::setActiveAction(QAction *action)
248{
249 if (!m_menu) {
250 return;
251 }
252
253 m_currentButton = m_buttons.first();
254
255 if (action) {
256 QMenu *menu;
257 int i = 0;
258 foreach (MenuButton *button, m_buttons) {
259 menu = m_menu->actions()[i]->menu();
260 if (menu && menu == action->menu()) {
261 m_currentButton = button;
262 break;
263 }
264 if (++i >= m_menu->actions().length()) {
265 break;
266 }
267 }
268 }
269 m_currentButton->nativeWidget()->animateClick();
270}
271
272void MenuWidget::hide()
273{
274 if (m_mouseTimer->isActive()) {
275 m_mouseTimer->stop();
276 }
277 QGraphicsWidget::hide();
278}
279
280MenuButton* MenuWidget::createButton(QAction *action)
281{
282 if( action->isSeparator() || !action->menu() || !action->isVisible()) {
283 return 0;
284 }
285
286 action->setShortcut(QKeySequence());
287 MenuButton *button = new MenuButton(this);
288 button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
289 button->setText(action->text());
290 connect(button, SIGNAL(clicked()), SLOT(slotButtonClicked()));
291 return button;
292}
293
294QMenu* MenuWidget::showMenu()
295{
296 QMenu *menu = 0;
297
298 if (m_visibleMenu) {
299 disconnect(m_visibleMenu, SIGNAL(aboutToHide()), this, SLOT(slotMenuAboutToHide()));
300 m_visibleMenu->hide();
301 }
302
303 if (m_currentButton && m_menu) {
304 menu = m_currentButton->menu();
305 }
306
307 // Last chance to get menu
308 // Some applications like Firefox have empties menus on layout updates
309 // They should populate this menu later but in fact, they use another object
310 // So, we check here directly the button name, may fail with menubar with buttons with same name (test apps)
311 if (menu && menu->actions().length() == 0) {
312 foreach (QAction *action, m_menu->actions()) {
313 if (action->text() == m_currentButton->text()) {
314 menu = action->menu();
315 break;
316 }
317 }
318 }
319
320 if (menu) {
321 QPoint globalPos = m_view->mapToGlobal(QPoint(0,0));
322 QPointF parentPos = m_currentButton->mapFromParent(QPoint(0,0));
323 QRect screen = KApplication::desktop()->screenGeometry();
324 int x = globalPos.x() - parentPos.x();
325 int y = globalPos.y() + m_currentButton->size().height() - parentPos.y();
326
327 menu->popup(QPoint(x, y));
328
329 // Fix offscreen menu
330 if (menu->size().height() + y > screen.height() + screen.y()) {
331 y = globalPos.y() - parentPos.y() - menu->size().height();
332 if (menu->size().width() + x > screen.width() + screen.x())
333 x = screen.width() + screen.x() - menu->size().width();
334 else if (menu->size().width() + x < screen.x())
335 x = screen.x();
336 menu->move(x, y);
337 }
338
339 connect(menu, SIGNAL(aboutToHide()), this, SLOT(slotMenuAboutToHide()));
340
341 installEventFilterForAll(menu, this);
342 }
343 return menu;
344}
345
346void MenuWidget::showLeftRightMenu(bool next)
347{
348 if (!m_currentButton) {
349 return;
350 }
351
352 int index = m_buttons.indexOf(m_currentButton);
353 if (index == -1) {
354 kWarning() << "Couldn't find button!";
355 return;
356 }
357 if (next) {
358 index = (index + 1) % m_buttons.count();
359 } else {
360 index = (index == 0 ? m_buttons.count() : index) - 1;
361 }
362
363 if (m_currentButton && m_currentButton->nativeWidget()) {
364 m_currentButton->nativeWidget()->setDown(false);
365 }
366 m_currentButton = m_buttons.at(index);
367 if (m_currentButton && m_currentButton->nativeWidget()) {
368 m_currentButton->nativeWidget()->setDown(true);
369 }
370 m_visibleMenu = showMenu();
371}
372
373void MenuWidget::installEventFilterForAll(QMenu *menu, QObject *object)
374{
375 if (!menu) {
376 return;
377 }
378
379 menu->installEventFilter(this);
380
381 foreach (QAction *action, menu->actions()) {
382 if (action->menu())
383 installEventFilterForAll(action->menu(), object);
384 }
385}
386
387#include "menuwidget.moc"