Plasma-framework

qmenu.cpp
1/*
2 SPDX-FileCopyrightText: 2011 Viranch Mehta <viranch.mehta@gmail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "qmenu.h"
8
9#include <QApplication>
10#include <QDebug>
11#include <QQmlProperty>
12#include <QQuickItem>
13#include <QQuickRenderControl>
14#include <QQuickWindow>
15#include <QScreen>
16
17#include <KAcceleratorManager>
18#include <optional>
19
20#include "plasma.h"
21
22QMenuProxy::QMenuProxy(QObject *parent)
23 : QObject(parent)
24 , m_menu(nullptr)
25 , m_status(Closed)
26 , m_placement(LeftPosedTopAlignedPopup)
27 , m_preferSeamlessEdges(false)
28{
29 if (qobject_cast<QApplication *>(QCoreApplication::instance())) {
30 m_menu = new QMenu(nullptr);
31 // Breeze and Oxygen have rounded corners on menus. They set this attribute in polish()
32 // but at that time the underlying surface has already been created where setting this
33 // flag makes no difference anymore (Bug 385311)
34 m_menu->setAttribute(Qt::WA_TranslucentBackground);
35
37 connect(m_menu, &QMenu::triggered, this, &QMenuProxy::itemTriggered);
38 connect(m_menu, &QMenu::aboutToHide, this, [this]() {
39 m_status = Closed;
40 Q_EMIT statusChanged();
41 });
42 }
43}
44
45QMenuProxy::~QMenuProxy()
46{
47 delete m_menu;
48}
49
50QQmlListProperty<QMenuItem> QMenuProxy::content()
51{
52 return QQmlListProperty<QMenuItem>(this, &m_items);
53}
54
55int QMenuProxy::actionCount() const
56{
57 return m_items.count();
58}
59
60QMenuItem *QMenuProxy::action(int index) const
61{
62 return m_items.at(index);
63}
64
65QMenuProxy::Status QMenuProxy::status() const
66{
67 return m_status;
68}
69
70QObject *QMenuProxy::visualParent() const
71{
72 return m_visualParent.data();
73}
74
75void QMenuProxy::setVisualParent(QObject *parent)
76{
77 if (m_visualParent.data() == parent) {
78 return;
79 }
80
81 // if the old parent was a QAction, disconnect the menu from it
82 QAction *action = qobject_cast<QAction *>(m_visualParent.data());
83 if (action) {
84 action->setMenu(nullptr);
85 m_menu->clear();
86 }
87 // if parent is a QAction, become a submenu
88 action = qobject_cast<QAction *>(parent);
89 if (action) {
90 action->setMenu(m_menu);
91 m_menu->clear();
92 for (QMenuItem *item : std::as_const(m_items)) {
93 if (item->section()) {
94 if (!item->isVisible()) {
95 continue;
96 }
97
98 m_menu->addSection(item->text());
99 } else {
100 m_menu->addAction(item->action());
101 }
102 }
103 m_menu->updateGeometry();
104 }
105
106 m_visualParent = parent;
107 Q_EMIT visualParentChanged();
108}
109
110QWindow *QMenuProxy::transientParent()
111{
112 if (!m_menu || !m_menu->windowHandle()) {
113 return nullptr;
114 }
115 return m_menu->windowHandle()->transientParent();
116}
117
118void QMenuProxy::setTransientParent(QWindow *parent)
119{
120 if (!m_menu || !m_menu->windowHandle() || parent == m_menu->windowHandle()->transientParent()) {
121 return;
122 }
123
125 Q_EMIT transientParentChanged();
126}
127
128QMenuProxy::PopupPlacement QMenuProxy::placement() const
129{
130 return m_placement;
131}
132
133void QMenuProxy::setPlacement(QMenuProxy::PopupPlacement placement)
134{
135 if (m_placement != placement) {
136 m_placement = placement;
137
138 Q_EMIT placementChanged();
139 }
140}
141
142bool QMenuProxy::preferSeamlessEdges() const
143{
144 return m_preferSeamlessEdges;
145}
146
147void QMenuProxy::setPreferSeamlessEdges(bool request)
148{
149 if (m_preferSeamlessEdges != request) {
150 m_preferSeamlessEdges = request;
151
152 Q_EMIT preferSeamlessEdgesChanged();
153 }
154}
155
156int QMenuProxy::minimumWidth() const
157{
158 return m_menu->minimumWidth();
159}
160
161void QMenuProxy::setMinimumWidth(int width)
162{
163 if (m_menu->minimumWidth() != width) {
164 m_menu->setMinimumWidth(width);
165
166 Q_EMIT minimumWidthChanged();
167 }
168}
169
170int QMenuProxy::maximumWidth() const
171{
172 return m_menu->maximumWidth();
173}
174
175void QMenuProxy::setMaximumWidth(int width)
176{
177 if (m_menu->maximumWidth() != width) {
178 m_menu->setMaximumWidth(width);
179
180 Q_EMIT maximumWidthChanged();
181 }
182}
183
184void QMenuProxy::resetMaximumWidth()
185{
186 setMaximumWidth(QWIDGETSIZE_MAX);
187}
188
189bool QMenuProxy::event(QEvent *event)
190{
191 switch (event->type()) {
192 case QEvent::ChildAdded: {
193 QChildEvent *ce = static_cast<QChildEvent *>(event);
194 QMenuItem *mi = qobject_cast<QMenuItem *>(ce->child());
195 // FIXME: linear complexity here
196 if (mi && !m_items.contains(mi)) {
197 if (mi->separator()) {
198 m_menu->addSection(mi->text());
199 } else {
200 m_menu->addAction(mi->action());
201 }
202 m_items << mi;
203 }
204 break;
205 }
206
208 QChildEvent *ce = static_cast<QChildEvent *>(event);
209 QMenuItem *mi = qobject_cast<QMenuItem *>(ce->child());
210
211 // FIXME: linear complexity here
212 if (mi) {
213 m_menu->removeAction(mi->action());
214 m_items.removeAll(mi);
215 }
216 break;
217 }
218
219 default:
220 break;
221 }
222
223 return QObject::event(event);
224}
225
226void QMenuProxy::clearMenuItems()
227{
228 qDeleteAll(m_items);
229 m_items.clear();
230}
231
232void QMenuProxy::addMenuItem(const QString &text)
233{
234 QMenuItem *item = new QMenuItem();
235 item->setText(text);
236 m_menu->addAction(item->action());
237 m_items << item;
238}
239
240void QMenuProxy::addMenuItem(QMenuItem *item, QMenuItem *before)
241{
242 if (before) {
243 if (m_items.contains(item)) {
244 m_menu->removeAction(item->action());
245 m_items.removeAll(item);
246 }
247
248 m_menu->insertAction(before->action(), item->action());
249
250 const int index = m_items.indexOf(before);
251
252 if (index != -1) {
253 m_items.insert(index, item);
254 } else {
255 m_items << item;
256 }
257
258 } else if (!m_items.contains(item)) {
259 m_menu->addAction(item->action());
260 m_items << item;
261 }
262 connect(item, &QMenuItem::destroyed, this, [this, item]() {
263 removeMenuItem(item);
264 });
265}
266
267void QMenuProxy::addSection(const QString &text)
268{
269 m_menu->addSection(text);
270}
271
272void QMenuProxy::removeMenuItem(QMenuItem *item)
273{
274 if (!item) {
275 return;
276 }
277
278 m_menu->removeAction(item->action());
279 m_items.removeOne(item);
280}
281
282void QMenuProxy::itemTriggered(QAction *action)
283{
284 for (int i = 0; i < m_items.count(); ++i) {
285 QMenuItem *item = m_items.at(i);
286 if (item->action() == action) {
287 Q_EMIT triggered(item);
288 Q_EMIT triggeredIndex(i);
289 break;
290 }
291 }
292}
293
294void QMenuProxy::rebuildMenu()
295{
296 m_menu->clear();
297
298 for (QMenuItem *item : std::as_const(m_items)) {
299 if (item->section()) {
300 if (!item->isVisible()) {
301 continue;
302 }
303
304 m_menu->addSection(item->text());
305 } else {
306 m_menu->addAction(item->action());
307 if (item->action()->menu()) {
308 // This ensures existence of the QWindow
309 m_menu->winId();
310 item->action()->menu()->winId();
311 item->action()->menu()->windowHandle()->setTransientParent(m_menu->windowHandle());
312 }
313 }
314 }
315
316 m_menu->adjustSize();
317}
318
319// Map to global coordinate space, accounting for embedded offscreen windows, e.g. QQuickWidget.
320static QPoint mapToGlobalUsingRenderWindowOfItem(const QQuickItem *parentItem, QPointF posF)
321{
322 QPoint pos = posF.toPoint(); // XXX: Drop rounding if mapToGlobal ever supports floating points
323 if (QQuickWindow *quickWindow = parentItem->window()) {
324 QPoint offset;
325 if (auto renderWindow = QQuickRenderControl::renderWindowFor(quickWindow, &offset)) {
326 QPoint relativePos = pos + offset;
327 return renderWindow->mapToGlobal(relativePos);
328 } else {
329 return quickWindow->mapToGlobal(pos);
330 }
331 } else {
332 return pos;
333 }
334}
335
336void QMenuProxy::open(int x, int y)
337{
338 QQuickItem *parentItem = nullptr;
339
340 if (m_visualParent) {
341 parentItem = qobject_cast<QQuickItem *>(m_visualParent.data());
342 } else {
343 parentItem = qobject_cast<QQuickItem *>(parent());
344 }
345
346 if (!parentItem) {
347 return;
348 }
349
350 rebuildMenu();
351
352 QPointF posLocal = parentItem->mapToScene(QPointF(x, y));
353
354 QPoint posGlobal = mapToGlobalUsingRenderWindowOfItem(parentItem, posLocal);
355
356 setupSeamlessEdges(std::nullopt);
357
358 openInternal(posGlobal);
359}
360
361void QMenuProxy::openRelative()
362{
363 QQuickItem *parentItem = nullptr;
364
365 if (m_visualParent) {
366 parentItem = qobject_cast<QQuickItem *>(m_visualParent.data());
367 } else {
368 parentItem = qobject_cast<QQuickItem *>(parent());
369 }
370
371 if (!parentItem) {
372 return;
373 }
374
375 rebuildMenu();
376
377 QPointF posLocal;
378 QPoint posGlobal;
379
380 auto boundaryCorrection = [this, &posLocal, &posGlobal, parentItem](int hDelta, int vDelta) {
381 if (auto window = parentItem->window(); //
382 QScreen *screen = window->screen()) {
383 QRect geo = screen->geometry();
384
385 QPoint pos = mapToGlobalUsingRenderWindowOfItem(parentItem, posLocal);
386
387 if (pos.x() < geo.x()) {
388 pos.setX(pos.x() + hDelta);
389 }
390 if (pos.y() < geo.y()) {
391 pos.setY(pos.y() + vDelta);
392 }
393
394 if (geo.x() + geo.width() < pos.x() + this->m_menu->width()) {
395 pos.setX(pos.x() + hDelta);
396 }
397 if (geo.y() + geo.height() < pos.y() + this->m_menu->height()) {
398 pos.setY(pos.y() + vDelta);
399 }
400 posGlobal = pos;
401 } else {
402 posGlobal = posLocal.toPoint();
403 }
404 };
405
406 const QQmlProperty enabledProp(parentItem, QStringLiteral("LayoutMirroring.enabled"), qmlContext(parentItem));
407 const bool mirrored(enabledProp.read().toBool());
408 const auto placement = visualPopupPlacement(m_placement, mirrored ? Qt::RightToLeft : Qt::LeftToRight);
409
410 using namespace Plasma;
411
412 switch (placement) {
413 case TopPosedLeftAlignedPopup: {
414 posLocal = parentItem->mapToScene(QPointF(0, -m_menu->height()));
415 boundaryCorrection(-m_menu->width() + parentItem->width(), m_menu->height() + parentItem->height());
416 break;
417 }
418 case LeftPosedTopAlignedPopup: {
419 posLocal = parentItem->mapToScene(QPointF(-m_menu->width(), 0));
420 boundaryCorrection(m_menu->width() + parentItem->width(), -m_menu->height() + parentItem->height());
421 break;
422 }
423 case TopPosedRightAlignedPopup:
424 posLocal = parentItem->mapToScene(QPointF(parentItem->width() - m_menu->width(), -m_menu->height()));
425 boundaryCorrection(m_menu->width() - parentItem->width(), m_menu->height() + parentItem->height());
426 break;
427 case RightPosedTopAlignedPopup: {
428 posLocal = parentItem->mapToScene(QPointF(parentItem->width(), 0));
429 boundaryCorrection(-m_menu->width() - parentItem->width(), -m_menu->height() + parentItem->height());
430 break;
431 }
432 case LeftPosedBottomAlignedPopup:
433 posLocal = parentItem->mapToScene(QPointF(-m_menu->width(), -m_menu->height() + parentItem->height()));
434 boundaryCorrection(m_menu->width() + parentItem->width(), m_menu->height() - parentItem->height());
435 break;
436 case BottomPosedLeftAlignedPopup: {
437 posLocal = parentItem->mapToScene(QPointF(0, parentItem->height()));
438 boundaryCorrection(-m_menu->width() + parentItem->width(), -m_menu->height() - parentItem->height());
439 break;
440 }
441 case BottomPosedRightAlignedPopup: {
442 posLocal = parentItem->mapToScene(QPointF(parentItem->width() - m_menu->width(), parentItem->height()));
443 boundaryCorrection(m_menu->width() - parentItem->width(), -m_menu->height() - parentItem->height());
444 break;
445 }
446 case RightPosedBottomAlignedPopup: {
447 posLocal = parentItem->mapToScene(QPointF(parentItem->width(), -m_menu->height() + parentItem->height()));
448 boundaryCorrection(-m_menu->width() - parentItem->width(), m_menu->height() - parentItem->height());
449 break;
450 }
451 default:
452 open();
453 return;
454 }
455
456 setupSeamlessEdges(std::optional(placement));
457
458 openInternal(posGlobal);
459}
460
461void QMenuProxy::openInternal(QPoint pos)
462{
463 QQuickItem *parentItem = this->parentItem();
464
465 if (parentItem && parentItem->window()) {
466 // create the QWindow
467 m_menu->winId();
468 m_menu->windowHandle()->setTransientParent(parentItem->window());
469
470 // Workaround for QTBUG-59044
471 // pre 5.8.0 QQuickWindow code is "item->grabMouse(); sendEvent(item, mouseEvent)"
472 // post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse()
474 this,
475 [this]() {
476 QQuickItem *parentItem = this->parentItem();
477 if (parentItem && parentItem->window() && parentItem->window()->mouseGrabberItem()) {
478 parentItem->window()->mouseGrabberItem()->ungrabMouse();
479 }
480 },
482 // end workaround
483 }
484
485 m_menu->popup(pos);
486 m_status = Open;
487 Q_EMIT statusChanged();
488}
489
490QQuickItem *QMenuProxy::parentItem() const
491{
492 if (m_visualParent) {
493 return qobject_cast<QQuickItem *>(m_visualParent.data());
494 }
495
496 return qobject_cast<QQuickItem *>(parent());
497}
498
499void QMenuProxy::close()
500{
501 m_menu->hide();
502}
503
504QMenuProxy::PopupPlacement QMenuProxy::visualPopupPlacement(PopupPlacement placement, Qt::LayoutDirection layoutDirection)
505{
506 const bool mirrored = (layoutDirection == Qt::LayoutDirectionAuto) ? qApp->isRightToLeft() : (layoutDirection == Qt::RightToLeft);
507
508 if (!mirrored) {
509 return placement;
510 }
511
512 switch (placement) {
513 case TopPosedLeftAlignedPopup:
514 return TopPosedRightAlignedPopup;
515 case TopPosedRightAlignedPopup:
516 return TopPosedLeftAlignedPopup;
517 case LeftPosedTopAlignedPopup:
518 return RightPosedTopAlignedPopup;
519 case LeftPosedBottomAlignedPopup:
520 return RightPosedBottomAlignedPopup;
521 case BottomPosedLeftAlignedPopup:
522 return BottomPosedRightAlignedPopup;
523 case BottomPosedRightAlignedPopup:
524 return BottomPosedLeftAlignedPopup;
525 case RightPosedTopAlignedPopup:
526 return LeftPosedTopAlignedPopup;
527 case RightPosedBottomAlignedPopup:
528 return LeftPosedBottomAlignedPopup;
529 case FloatingPopup:
530 default:
531 return placement;
532 }
533}
534
535Qt::Edges QMenuProxy::seamlessEdgesForPlacement(std::optional<PopupPlacement> placement)
536{
537 if (m_preferSeamlessEdges && placement.has_value()) {
538 switch (placement.value()) {
539 case TopPosedLeftAlignedPopup:
540 case TopPosedRightAlignedPopup:
541 return Qt::BottomEdge;
542
543 case LeftPosedTopAlignedPopup:
544 case LeftPosedBottomAlignedPopup:
545 return Qt::RightEdge;
546
547 case BottomPosedLeftAlignedPopup:
548 case BottomPosedRightAlignedPopup:
549 return Qt::TopEdge;
550
551 case RightPosedTopAlignedPopup:
552 case RightPosedBottomAlignedPopup:
553 return Qt::LeftEdge;
554
555 case FloatingPopup:
556 default:
557 break;
558 }
559 }
560
561 return Qt::Edges();
562}
563
564void QMenuProxy::setupSeamlessEdges(std::optional<PopupPlacement> placement)
565{
566 // Note: Assume that seamless edges don't affect size of the menu.
567 auto edges = seamlessEdgesForPlacement(placement);
568 m_menu->setProperty("_breeze_menu_seamless_edges", QVariant::fromValue(edges));
569}
570
571#include "moc_qmenu.cpp"
static void manage(QWidget *widget, bool programmers_mode=false)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
GeoCoordinates geo(const QVariant &location)
KGUIADDONS_EXPORT QWindow * window(QObject *job)
Namespace for everything in libplasma.
QMenu * menu() const const
void setMenu(QMenu *menu)
QObject * child() const const
QCoreApplication * instance()
const_reference at(qsizetype i) const const
void clear()
bool contains(const AT &value) const const
qsizetype count() const const
qsizetype indexOf(const AT &value, qsizetype from) const const
iterator insert(const_iterator before, parameter_type value)
qsizetype removeAll(const AT &t)
bool removeOne(const AT &t)
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
void aboutToHide()
QAction * addSection(const QIcon &icon, const QString &text)
void clear()
void popup(const QPoint &p, QAction *atAction)
void triggered(QAction *action)
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void destroyed(QObject *obj)
virtual bool event(QEvent *e)
QObject * parent() const const
bool setProperty(const char *name, QVariant &&value)
void setX(int x)
void setY(int y)
int x() const const
int y() const const
T * data() const const
QPoint toPoint() const const
void ungrabMouse()
QPointF mapToScene(const QPointF &point) const const
QQuickWindow * window() const const
QWindow * renderWindowFor(QQuickWindow *win, QPoint *offset)
QQuickItem * mouseGrabberItem() const const
QueuedConnection
typedef Edges
RightToLeft
WA_TranslucentBackground
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QVariant fromValue(T &&value)
void adjustSize()
void hide()
void insertAction(QAction *before, QAction *action)
void removeAction(QAction *action)
void updateGeometry()
WId winId() const const
QWindow * windowHandle() const const
QScreen * screen() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:54:11 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.