File indexing completed on 2024-04-28 13:25:27

0001 /*
0002     SPDX-FileCopyrightText: 2022 Michail Vourlakos <mvourlakos@gmail.com>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "contextmenulayerquickitem.h"
0007 
0008 // local
0009 #include "../lattecorona.h"
0010 #include "../layouts/storage.h"
0011 #include "../view/view.h"
0012 
0013 // Qt
0014 #include <QMouseEvent>
0015 #include <QVersionNumber>
0016 #include <QLatin1String>
0017 
0018 // KDE
0019 #include <KAcceleratorManager>
0020 #include <KActionCollection>
0021 #include <KAuthorized>
0022 #include <KLocalizedString>
0023 
0024 // Plasma
0025 #include <Plasma/Applet>
0026 #include <Plasma/Containment>
0027 #include <Plasma/ContainmentActions>
0028 #include <Plasma/Corona>
0029 #include <PlasmaQuick/AppletQuickItem>
0030 
0031 namespace Latte {
0032 
0033 ContextMenuLayerQuickItem::ContextMenuLayerQuickItem(QQuickItem *parent) :
0034     QQuickItem(parent)
0035 {
0036     setAcceptedMouseButtons(Qt::AllButtons);
0037 }
0038 
0039 ContextMenuLayerQuickItem::~ContextMenuLayerQuickItem()
0040 {
0041 }
0042 
0043 bool ContextMenuLayerQuickItem::menuIsShown() const
0044 {
0045     return m_contextMenu != nullptr;
0046 }
0047 
0048 QObject *ContextMenuLayerQuickItem::view() const
0049 {
0050     return m_latteView;
0051 }
0052 
0053 void ContextMenuLayerQuickItem::setView(QObject *view)
0054 {
0055     if (m_latteView == view) {
0056         return;
0057     }
0058 
0059     m_latteView = qobject_cast<Latte::View *>(view);
0060     emit viewChanged();
0061 }
0062 
0063 void ContextMenuLayerQuickItem::onMenuAboutToHide()
0064 {
0065     if (!m_latteView) {
0066         return;
0067     }
0068 
0069     m_latteView->containment()->setStatus(m_lastContainmentStatus);
0070     m_contextMenu = nullptr;
0071     emit menuChanged();
0072 }
0073 
0074 QPoint ContextMenuLayerQuickItem::popUpRelevantToParent(const QRect &parentItem, const QRect popUpRect)
0075 {
0076     QPoint resultPoint;
0077 
0078     if (!m_latteView) {
0079         return resultPoint;
0080     }
0081 
0082     if (m_latteView->location() == Plasma::Types::TopEdge) {
0083         resultPoint.setX(parentItem.left());
0084         resultPoint.setY(parentItem.bottom());
0085     } else if (m_latteView->location() == Plasma::Types::BottomEdge) {
0086         resultPoint.setX(parentItem.left());
0087         resultPoint.setY(parentItem.top() - popUpRect.height() - 1);
0088     } else if (m_latteView->location() == Plasma::Types::LeftEdge) {
0089         resultPoint.setX(parentItem.right());
0090         resultPoint.setY(parentItem.top());
0091     } else if (m_latteView->location() == Plasma::Types::RightEdge) {
0092         resultPoint.setX(parentItem.left() - popUpRect.width());
0093         resultPoint.setY(parentItem.top());
0094     }
0095 
0096     return resultPoint;
0097 }
0098 
0099 QPoint ContextMenuLayerQuickItem::popUpRelevantToGlobalPoint(const QRect &parentItem, const QRect popUpRect)
0100 {
0101     QPoint resultPoint;
0102 
0103     if (!m_latteView) {
0104         return resultPoint;
0105     }
0106 
0107     if (m_latteView->location() == Plasma::Types::TopEdge) {
0108         resultPoint.setX(popUpRect.x());
0109         resultPoint.setY(popUpRect.y() + 1);
0110     } else if (m_latteView->location() == Plasma::Types::BottomEdge) {
0111         resultPoint.setX(popUpRect.x());
0112         resultPoint.setY(popUpRect.y() - popUpRect.height() - 1);
0113     } else if (m_latteView->location() == Plasma::Types::LeftEdge) {
0114         resultPoint.setX(popUpRect.x() + 1);
0115         resultPoint.setY(popUpRect.y());
0116     } else if (m_latteView->location() == Plasma::Types::RightEdge) {
0117         resultPoint.setX(popUpRect.x() - popUpRect.width() - 1);
0118         resultPoint.setY(popUpRect.y());
0119     }
0120 
0121     return resultPoint;
0122 }
0123 
0124 QPoint ContextMenuLayerQuickItem::popUpTopLeft(Plasma::Applet *applet, const QRect popUpRect)
0125 {
0126     PlasmaQuick::AppletQuickItem *ai = applet->property("_plasma_graphicObject").value<PlasmaQuick::AppletQuickItem *>();
0127 
0128     QRect globalItemRect = m_latteView->absoluteGeometry();
0129 
0130     if (ai && applet != m_latteView->containment()) {
0131         QPointF appletGlobalTopLeft = ai->mapToGlobal(QPointF(ai->x(), ai->y()));
0132         globalItemRect = QRect(appletGlobalTopLeft.x(), appletGlobalTopLeft.y(), ai->width(), ai->height());
0133     }
0134 
0135     int itemLength = (m_latteView->formFactor() == Plasma::Types::Horizontal ? globalItemRect.width() : globalItemRect.height());
0136     int menuLength = (m_latteView->formFactor() == Plasma::Types::Horizontal ? popUpRect.width() : popUpRect.height());
0137 
0138     if ((itemLength > menuLength)
0139             || (applet == m_latteView->containment())
0140             || (m_latteView && Layouts::Storage::self()->isSubContainment(m_latteView->corona(), applet)) ) {
0141         return popUpRelevantToGlobalPoint(globalItemRect, popUpRect);
0142     } else {
0143         return popUpRelevantToParent(globalItemRect, popUpRect);
0144     }
0145 }
0146 
0147 
0148 void ContextMenuLayerQuickItem::mouseReleaseEvent(QMouseEvent *event)
0149 {
0150     if (!event || !m_latteView) {
0151         return;
0152     }
0153 
0154     event->setAccepted(m_latteView->containment()->containmentActions().contains(Plasma::ContainmentActions::eventToString(event)));
0155     emit menuChanged();
0156 }
0157 
0158 void ContextMenuLayerQuickItem::mousePressEvent(QMouseEvent *event)
0159 {
0160     //qDebug() << "Step -1 ...";
0161 
0162     if (!event || !m_latteView || !m_latteView->containment()) {
0163         return;
0164     }
0165 
0166     //qDebug() << "Step 0...";
0167 
0168     //even if the menu is executed synchronously, other events may be processed
0169     //by the qml incubator when plasma is loading, so we need to guard there
0170     if (m_contextMenu) {
0171         //qDebug() << "Step 0.5 ...";
0172         m_contextMenu->close();
0173         m_contextMenu = nullptr;
0174         return;
0175     }
0176 
0177     //qDebug() << "1 ...";
0178     const QString trigger = Plasma::ContainmentActions::eventToString(event);
0179     Plasma::ContainmentActions *plugin = m_latteView->containment()->containmentActions().value(trigger);
0180 
0181     if (!plugin || plugin->contextualActions().isEmpty()) {
0182         event->setAccepted(false);
0183         return;
0184     }
0185 
0186     // the plugin can be a single action or a context menu
0187     // Don't have an action list? execute as single action
0188     // and set the event position as action data
0189     if (plugin->contextualActions().length() == 1) {
0190         QAction *action = plugin->contextualActions().at(0);
0191         action->setData(event->pos());
0192         action->trigger();
0193         event->accept();
0194         return;
0195     }
0196 
0197     //qDebug() << "2 ...";
0198     //the plugin can be a single action or a context menu
0199     //Don't have an action list? execute as single action
0200     //and set the event position as action data
0201     /*if (plugin->contextualActions().length() == 1) {
0202             QAction *action = plugin->contextualActions().at(0);
0203             action->setData(event->pos());
0204             action->trigger();
0205             event->accept();
0206             return;
0207         }*/
0208     //FIXME: very inefficient appletAt() implementation
0209     Plasma::Applet *applet = 0;
0210 
0211     //! initialize the appletContainsMethod on the first right click
0212     if (!m_appletContainsMethod.isValid()) {
0213         updateAppletContainsMethod();
0214     }
0215 
0216     for (const Plasma::Applet *appletTemp : m_latteView->containment()->applets()) {
0217         PlasmaQuick::AppletQuickItem *ai = appletTemp->property("_plasma_graphicObject").value<PlasmaQuick::AppletQuickItem *>();
0218 
0219         bool appletContainsMouse = false;
0220 
0221         if (m_appletContainsMethod.isValid()) {
0222             QVariant retVal;
0223             m_appletContainsMethod.invoke(m_appletContainsMethodItem, Qt::DirectConnection, Q_RETURN_ARG(QVariant, retVal)
0224                                           , Q_ARG(QVariant, appletTemp->id()), Q_ARG(QVariant, event->pos()));
0225             appletContainsMouse = retVal.toBool();
0226         } else {
0227             appletContainsMouse = ai->contains(ai->mapFromItem(this, event->pos()));
0228         }
0229 
0230         if (ai && ai->isVisible() && appletContainsMouse) {
0231             applet = ai->applet();
0232             break;
0233         }
0234     }
0235 
0236     if (!applet) {
0237         applet = m_latteView->containment();
0238     }
0239 
0240     //qDebug() << "3 ...";
0241 
0242     QMenu *desktopMenu = new QMenu;
0243 
0244     //this is a workaround where Qt now creates the menu widget
0245     //in .exec before oxygen can polish it and set the following attribute
0246     desktopMenu->setAttribute(Qt::WA_TranslucentBackground);
0247     //end workaround
0248 
0249     if (desktopMenu->winId()) {
0250         desktopMenu->windowHandle()->setTransientParent(window());
0251     }
0252 
0253     desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
0254     m_contextMenu = desktopMenu;
0255     emit menuChanged();
0256 
0257     //end workaround
0258     //!end of plasma official code(workaround)
0259 
0260     //qDebug() << "5 ...";
0261 
0262     emit m_latteView->containment()->contextualActionsAboutToShow();
0263 
0264     if (applet && applet != m_latteView->containment()) {
0265         //qDebug() << "5.3 ...";
0266         emit applet->contextualActionsAboutToShow();
0267         addAppletActions(desktopMenu, applet, event);
0268     } else {
0269         //qDebug() << "5.6 ...";
0270         addContainmentActions(desktopMenu, event);
0271     }
0272 
0273     //!plasma official code
0274     //this is a workaround where Qt will fail to realize a mouse has been released
0275 
0276     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0277     // whilst the mouse is depressed
0278     // https://bugreports.qt.io/browse/QTBUG-59044
0279     // this causes the next click to go missing
0280 
0281     //by releasing manually we avoid that situation
0282     auto ungrabMouseHack = [this]() {
0283         if (window() && window()->mouseGrabberItem()) {
0284             window()->mouseGrabberItem()->ungrabMouse();
0285         }
0286     };
0287 
0288     //post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse()
0289     QTimer::singleShot(0, this, ungrabMouseHack);
0290 
0291     //this is a workaround where Qt now creates the menu widget
0292     //in .exec before oxygen can polish it and set the following attribute
0293     desktopMenu->setAttribute(Qt::WA_TranslucentBackground);
0294     //end workaround
0295     QPoint globalPos = event->globalPos();
0296     desktopMenu->adjustSize();
0297 
0298     QRect popUpRect(globalPos.x(), globalPos.y(), desktopMenu->width(), desktopMenu->height());
0299 
0300     if (applet) {
0301         globalPos = popUpTopLeft(applet, popUpRect);
0302     } else {
0303         globalPos = popUpRelevantToGlobalPoint(QRect(0,0,0,0), popUpRect);
0304     }
0305 
0306     //qDebug() << "7...";
0307 
0308     if (desktopMenu->isEmpty()) {
0309         //qDebug() << "7.5 ...";
0310         delete desktopMenu;
0311         event->accept();
0312         return;
0313     }
0314 
0315     // Bug 344205 keep panel visible while menu is open
0316     m_lastContainmentStatus = m_latteView->containment()->status();
0317     m_latteView->containment()->setStatus(Plasma::Types::RequiresAttentionStatus);
0318 
0319     connect(desktopMenu, SIGNAL(aboutToHide()), this, SLOT(onMenuAboutToHide()));
0320 
0321     KAcceleratorManager::manage(desktopMenu);
0322 
0323     for (auto action : desktopMenu->actions()) {
0324         if (action->menu()) {
0325             connect(action->menu(), &QMenu::aboutToShow, desktopMenu, [action, desktopMenu] {
0326                 if (action->menu()->windowHandle()) {
0327                     // Need to add the transient parent otherwise Qt will create a new toplevel
0328                     action->menu()->windowHandle()->setTransientParent(desktopMenu->windowHandle());
0329                 }
0330             });
0331         }
0332     }
0333 
0334     //qDebug() << "8 ...";
0335     desktopMenu->popup(globalPos);
0336     event->setAccepted(true);
0337 }
0338 
0339 //! update the appletContainsPos method from Panel view
0340 void ContextMenuLayerQuickItem::updateAppletContainsMethod()
0341 {
0342     if (!m_latteView) {
0343         return;
0344     }
0345 
0346     for (QQuickItem *item : m_latteView->contentItem()->childItems()) {
0347         if (auto *metaObject = item->metaObject()) {
0348             // not using QMetaObject::invokeMethod to avoid warnings when calling
0349             // this on applets that don't have it or other child items since this
0350             // is pretty much trial and error.
0351             // Also, "var" arguments are treated as QVariant in QMetaObject
0352 
0353             int methodIndex = metaObject->indexOfMethod("appletContainsPos(QVariant,QVariant)");
0354 
0355             if (methodIndex == -1) {
0356                 continue;
0357             }
0358 
0359             m_appletContainsMethod = metaObject->method(methodIndex);
0360             m_appletContainsMethodItem = item;
0361         }
0362     }
0363 }
0364 
0365 void ContextMenuLayerQuickItem::addAppletActions(QMenu *desktopMenu, Plasma::Applet *applet, QEvent *event)
0366 {
0367     if (!m_latteView || !m_latteView->containment()) {
0368         return;
0369     }
0370 
0371     desktopMenu->addSection(applet->pluginMetaData().name());
0372 
0373     for (QAction *action : applet->contextualActions()) {
0374         if (action) {
0375             desktopMenu->addAction(action);
0376         }
0377     }
0378 
0379     if (!applet->failedToLaunch()) {
0380         QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
0381 
0382         if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
0383             desktopMenu->addAction(runAssociatedApplication);
0384         }
0385 
0386         QAction *configureApplet = applet->actions()->action(QStringLiteral("configure"));
0387 
0388         if (configureApplet && configureApplet->isEnabled()) {
0389             desktopMenu->addAction(configureApplet);
0390         }
0391 
0392         QAction *appletAlternatives = applet->actions()->action(QStringLiteral("alternatives"));
0393 
0394         if (appletAlternatives && appletAlternatives->isEnabled() && m_latteView->containment()->isUserConfiguring()) {
0395             desktopMenu->addAction(appletAlternatives);
0396         }
0397     }
0398 
0399     QAction *containmentAction = desktopMenu->menuAction();
0400     containmentAction->setText(i18nc("%1 is the name of the containment", "%1 Options", m_latteView->containment()->title()));
0401 
0402     if (desktopMenu->actions().count()>1) { /*take into account the Applet Name Section*/
0403         addContainmentActions(containmentAction->menu(), event);
0404     }
0405 
0406     if (!containmentAction->menu()->isEmpty()) {
0407         int enabled = 0;
0408         //count number of real actions
0409         QListIterator<QAction *> actionsIt(containmentAction->menu()->actions());
0410 
0411         while (enabled < 3 && actionsIt.hasNext()) {
0412             QAction *action = actionsIt.next();
0413 
0414             if (action->isVisible() && !action->isSeparator()) {
0415                 ++enabled;
0416             }
0417         }
0418 
0419         desktopMenu->addSeparator();
0420 
0421         if (enabled) {
0422             //if there is only one, don't create a submenu
0423             // if (enabled < 2) {
0424             for (QAction *action : containmentAction->menu()->actions()) {
0425                 if (action && action->isVisible()) {
0426                     desktopMenu->addAction(action);
0427                 }
0428             }
0429 
0430             // } else {
0431             //     desktopMenu->addMenu(containmentMenu);
0432             // }
0433         }
0434     }
0435 
0436     if (m_latteView->containment()->immutability() == Plasma::Types::Mutable &&
0437             (m_latteView->containment()->containmentType() != Plasma::Types::PanelContainment || m_latteView->containment()->isUserConfiguring())) {
0438         QAction *closeApplet = applet->actions()->action(QStringLiteral("remove"));
0439 
0440         //qDebug() << "checking for removal" << closeApplet;
0441         if (closeApplet) {
0442             if (!desktopMenu->isEmpty()) {
0443                 desktopMenu->addSeparator();
0444             }
0445 
0446             //qDebug() << "adding close action" << closeApplet->isEnabled() << closeApplet->isVisible();
0447             desktopMenu->addAction(closeApplet);
0448         }
0449     }
0450 }
0451 
0452 void ContextMenuLayerQuickItem::addContainmentActions(QMenu *desktopMenu, QEvent *event)
0453 {
0454     if (!m_latteView || !m_latteView->containment()) {
0455         return;
0456     }
0457 
0458     if (m_latteView->containment()->corona()->immutability() != Plasma::Types::Mutable &&
0459             !KAuthorized::authorizeAction(QStringLiteral("plasma/containment_actions"))) {
0460         //qDebug() << "immutability";
0461         return;
0462     }
0463 
0464     //this is what ContainmentPrivate::prepareContainmentActions was
0465     const QString trigger = Plasma::ContainmentActions::eventToString(event);
0466     //"RightButton;NoModifier"
0467     Plasma::ContainmentActions *plugin = m_latteView->containment()->containmentActions().value(trigger);
0468 
0469     if (!plugin) {
0470         return;
0471     }
0472 
0473     if (plugin->containment() != m_latteView->containment()) {
0474         plugin->setContainment(m_latteView->containment());
0475         // now configure it
0476         KConfigGroup cfg(m_latteView->containment()->corona()->config(), "ActionPlugins");
0477         cfg = KConfigGroup(&cfg, QString::number(m_latteView->containment()->containmentType()));
0478         KConfigGroup pluginConfig = KConfigGroup(&cfg, trigger);
0479         plugin->restore(pluginConfig);
0480     }
0481 
0482     QList<QAction *> actions = plugin->contextualActions();
0483 
0484     /*   for (const QAction *act : actions) {
0485         if (act->menu()) {
0486             //this is a workaround where Qt now creates the menu widget
0487             //in .exec before oxygen can polish it and set the following attribute
0488             act->menu()->setAttribute(Qt::WA_TranslucentBackground);
0489             //end workaround
0490 
0491             if (act->menu()->winId()) {
0492                 act->menu()->windowHandle()->setTransientParent(m_latteView);
0493             }
0494         }
0495     }*/
0496 
0497     desktopMenu->addActions(actions);
0498 }
0499 
0500 Plasma::Containment *ContextMenuLayerQuickItem::containmentById(uint id)
0501 {
0502     if (!m_latteView) {
0503         return nullptr;
0504     }
0505 
0506     for (const auto containment : m_latteView->corona()->containments()) {
0507         if (id == containment->id()) {
0508             return containment;
0509         }
0510     }
0511 
0512     return nullptr;
0513 }
0514 
0515 }