File indexing completed on 2024-04-14 15:37:20

0001 /*
0002 *  Copyright 2018 Michail Vourlakos <mvourlakos@gmail.com>
0003 *
0004 *  This file is part of Latte-Dock
0005 *
0006 *  Latte-Dock is free software; you can redistribute it and/or
0007 *  modify it under the terms of the GNU General Public License as
0008 *  published by the Free Software Foundation; either version 2 of
0009 *  the License, or (at your option) any later version.
0010 *
0011 *  Latte-Dock is distributed in the hope that it will be useful,
0012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 *  GNU General Public License for more details.
0015 *
0016 *  You should have received a copy of the GNU General Public License
0017 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
0018 */
0019 
0020 #include "contextmenu.h"
0021 
0022 // local
0023 #include "view.h"
0024 #include "visibilitymanager.h"
0025 #include "../lattecorona.h"
0026 
0027 // Qt
0028 #include <QMouseEvent>
0029 #include <QVersionNumber>
0030 
0031 // KDE
0032 #include <KActionCollection>
0033 #include <KAuthorized>
0034 #include <KLocalizedString>
0035 
0036 // Plasma
0037 #include <Plasma/Applet>
0038 #include <Plasma/Containment>
0039 #include <Plasma/ContainmentActions>
0040 #include <Plasma/Corona>
0041 #include <PlasmaQuick/AppletQuickItem>
0042 
0043 namespace Latte {
0044 namespace ViewPart {
0045 
0046 ContextMenu::ContextMenu(Latte::View *view) :
0047     QObject(view),
0048     m_latteView(view)
0049 {
0050 }
0051 
0052 ContextMenu::~ContextMenu()
0053 {
0054 }
0055 
0056 QMenu *ContextMenu::menu()
0057 {
0058     return m_contextMenu;
0059 }
0060 
0061 void ContextMenu::menuAboutToHide()
0062 {
0063     if (!m_latteView) {
0064         return;
0065     }
0066 
0067     m_contextMenu = 0;
0068 
0069     if (!m_latteView->containment()->isUserConfiguring()) {
0070         m_latteView->visibility()->setBlockHiding(false);
0071     }
0072 
0073     emit menuChanged();
0074 }
0075 
0076 bool ContextMenu::mousePressEvent(QMouseEvent *event)
0077 {
0078     //qDebug() << "Step -1 ...";
0079 
0080     if (!event || !m_latteView->containment()) {
0081         return false;
0082     }
0083 
0084     //qDebug() << "Step 0...";
0085 
0086     //even if the menu is executed synchronously, other events may be processed
0087     //by the qml incubator when plasma is loading, so we need to guard there
0088     if (m_contextMenu) {
0089         //qDebug() << "Step 0.5 ...";
0090         m_contextMenu->close();
0091         m_contextMenu = 0;
0092         emit menuChanged();
0093         // PlasmaQuick::ContainmentView::mousePressEvent(event);
0094         return false;
0095     }
0096 
0097     //qDebug() << "1 ...";
0098     QString trigger = Plasma::ContainmentActions::eventToString(event);
0099 
0100     if (trigger == "RightButton;NoModifier") {
0101         Plasma::ContainmentActions *plugin = m_latteView->containment()->containmentActions().value(trigger);
0102 
0103         if (!plugin || plugin->contextualActions().isEmpty()) {
0104             event->setAccepted(false);
0105             return false;
0106         }
0107 
0108         //qDebug() << "2 ...";
0109         //the plugin can be a single action or a context menu
0110         //Don't have an action list? execute as single action
0111         //and set the event position as action data
0112         /*if (plugin->contextualActions().length() == 1) {
0113             QAction *action = plugin->contextualActions().at(0);
0114             action->setData(event->pos());
0115             action->trigger();
0116             event->accept();
0117             return;
0118         }*/
0119         //FIXME: very inefficient appletAt() implementation
0120         Plasma::Applet *applet = 0;
0121         bool inSystray = false;
0122 
0123         //! initialize the appletContainsMethod on the first right click
0124         if (!m_appletContainsMethod.isValid()) {
0125             updateAppletContainsMethod();
0126         }
0127 
0128         for (const Plasma::Applet *appletTemp : m_latteView->containment()->applets()) {
0129             PlasmaQuick::AppletQuickItem *ai = appletTemp->property("_plasma_graphicObject").value<PlasmaQuick::AppletQuickItem *>();
0130 
0131             bool appletContainsMouse = false;
0132 
0133             if (m_appletContainsMethod.isValid()) {
0134                 QVariant retVal;
0135                 m_appletContainsMethod.invoke(m_appletContainsMethodItem, Qt::DirectConnection, Q_RETURN_ARG(QVariant, retVal)
0136                                               , Q_ARG(QVariant, appletTemp->id()), Q_ARG(QVariant, event->pos()));
0137                 appletContainsMouse = retVal.toBool();
0138             } else {
0139                 appletContainsMouse = ai->contains(ai->mapFromItem(m_latteView->contentItem(), event->pos()));
0140             }
0141 
0142             if (ai && ai->isVisible() && appletContainsMouse) {
0143                 applet = ai->applet();
0144 
0145                 if (m_latteView && m_latteView->layout() && m_latteView->layout()->isInternalContainment(applet)) {
0146                     Plasma::Containment *internalC = m_latteView->layout()->internalContainmentOf(applet);
0147 
0148                     if (internalC) {
0149                         Plasma::Applet *internalApplet{nullptr};
0150 
0151                         for (const Plasma::Applet *appletCont : internalC->applets()) {
0152                             PlasmaQuick::AppletQuickItem *ai2 = appletCont->property("_plasma_graphicObject").value<PlasmaQuick::AppletQuickItem *>();
0153 
0154                             if (ai2 && ai2->isVisible() && ai2->contains(ai2->mapFromItem(m_latteView->contentItem(), event->pos()))) {
0155                                 internalApplet = ai2->applet();
0156                                 break;
0157                             }
0158                         }
0159 
0160                         if (!internalApplet) {
0161                             return true;
0162                         } else {
0163                             applet = internalApplet;
0164                         }
0165                     }
0166 
0167                     break;
0168                 } else {
0169                     ai = 0;
0170                 }
0171             }
0172         }
0173 
0174         if (!applet && !inSystray) {
0175             applet = m_latteView->containment();
0176         }
0177 
0178         //qDebug() << "3 ...";
0179 
0180         if (applet) {
0181             const auto &provides = KPluginMetaData::readStringList(applet->pluginMetaData().rawData(), QStringLiteral("X-Plasma-Provides"));
0182 
0183             //qDebug() << "3.5 ...";
0184 
0185             if (!provides.contains(QLatin1String("org.kde.plasma.multitasking"))) {
0186                 //qDebug() << "4...";
0187                 QMenu *desktopMenu = new QMenu;
0188 
0189                 //this is a workaround where Qt now creates the menu widget
0190                 //in .exec before oxygen can polish it and set the following attribute
0191                 desktopMenu->setAttribute(Qt::WA_TranslucentBackground);
0192                 //end workaround
0193 
0194                 if (desktopMenu->winId()) {
0195                     desktopMenu->windowHandle()->setTransientParent(m_latteView);
0196                 }
0197 
0198                 desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
0199                 m_contextMenu = desktopMenu;
0200 
0201                 //! deprecated old code that can be removed if the following plasma approach doesn't
0202                 //! create any issues with context menu creation in Latte
0203                 /*if (m_latteView->mouseGrabberItem()) {
0204                     //workaround, this fixes for me most of the right click menu behavior
0205                     m_latteView->mouseGrabberItem()->ungrabMouse();
0206                     return;
0207                 }*/
0208 
0209                 //!plasma official code
0210                 //this is a workaround where Qt will fail to realize a mouse has been released
0211 
0212                 // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0213                 // whilst the mouse is depressed
0214                 // https://bugreports.qt.io/browse/QTBUG-59044
0215                 // this causes the next click to go missing
0216 
0217                 //by releasing manually we avoid that situation
0218                 auto ungrabMouseHack = [this]() {
0219                     if (m_latteView->mouseGrabberItem()) {
0220                         m_latteView->mouseGrabberItem()->ungrabMouse();
0221                     }
0222                 };
0223 
0224                 //pre 5.8.0 QQuickWindow code is "item->grabMouse(); sendEvent(item, mouseEvent)"
0225                 //post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse()
0226                 if (QVersionNumber::fromString(qVersion()) > QVersionNumber(5, 8, 0)) {
0227                     QTimer::singleShot(0, this, ungrabMouseHack);
0228                 } else {
0229                     ungrabMouseHack();
0230                 }
0231 
0232                 //end workaround
0233                 //!end of plasma official code(workaround)
0234 
0235                 //qDebug() << "5 ...";
0236 
0237                 if (applet && applet != m_latteView->containment()) {
0238                     //qDebug() << "5.3 ...";
0239                     emit applet->contextualActionsAboutToShow();
0240                     addAppletActions(desktopMenu, applet, event);
0241                 } else {
0242                     //qDebug() << "5.6 ...";
0243                     emit m_latteView->containment()->contextualActionsAboutToShow();
0244                     addContainmentActions(desktopMenu, event);
0245                 }
0246 
0247                 //this is a workaround where Qt now creates the menu widget
0248                 //in .exec before oxygen can polish it and set the following attribute
0249                 desktopMenu->setAttribute(Qt::WA_TranslucentBackground);
0250                 //end workaround
0251                 QPoint pos = event->globalPos();
0252 
0253                 if (applet) {
0254                     //qDebug() << "6 ...";
0255                     desktopMenu->adjustSize();
0256 
0257                     if (m_latteView->screen()) {
0258                         const QRect scr = m_latteView->screen()->geometry();
0259                         int smallStep = 3;
0260                         int x = event->globalPos().x() + smallStep;
0261                         int y = event->globalPos().y() + smallStep;
0262 
0263                         //qDebug()<<x << " - "<<y;
0264 
0265                         if (event->globalPos().x() > scr.center().x()) {
0266                             x = event->globalPos().x() - desktopMenu->width() - smallStep;
0267                         }
0268 
0269                         if (event->globalPos().y() > scr.center().y()) {
0270                             y = event->globalPos().y() - desktopMenu->height() - smallStep;
0271                         }
0272 
0273                         pos = QPoint(x, y);
0274                     }
0275                 }
0276 
0277                 //qDebug() << "7...";
0278 
0279                 if (desktopMenu->isEmpty()) {
0280                     //qDebug() << "7.5 ...";
0281                     delete desktopMenu;
0282                     event->accept();
0283                     return false;
0284                 }
0285 
0286                 connect(desktopMenu, SIGNAL(aboutToHide()), this, SLOT(menuAboutToHide()));
0287                 m_latteView->visibility()->setBlockHiding(true);
0288                 desktopMenu->popup(pos);
0289                 event->setAccepted(true);
0290                 emit menuChanged();
0291                 return false;
0292             }
0293 
0294             //qDebug() << "8 ...";
0295         }
0296 
0297         //qDebug() << "9 ...";
0298     }
0299 
0300     //qDebug() << "10 ...";
0301     emit menuChanged();
0302     return true;
0303     //  PlasmaQuick::ContainmentView::mousePressEvent(event);
0304 }
0305 
0306 //! update the appletContainsPos method from Panel view
0307 void ContextMenu::updateAppletContainsMethod()
0308 {
0309     for (QQuickItem *item : m_latteView->contentItem()->childItems()) {
0310         if (auto *metaObject = item->metaObject()) {
0311             // not using QMetaObject::invokeMethod to avoid warnings when calling
0312             // this on applets that don't have it or other child items since this
0313             // is pretty much trial and error.
0314             // Also, "var" arguments are treated as QVariant in QMetaObject
0315 
0316             int methodIndex = metaObject->indexOfMethod("appletContainsPos(QVariant,QVariant)");
0317 
0318             if (methodIndex == -1) {
0319                 continue;
0320             }
0321 
0322             m_appletContainsMethod = metaObject->method(methodIndex);
0323             m_appletContainsMethodItem = item;
0324         }
0325     }
0326 }
0327 
0328 void ContextMenu::addAppletActions(QMenu *desktopMenu, Plasma::Applet *applet, QEvent *event)
0329 {
0330     if (!m_latteView->containment()) {
0331         return;
0332     }
0333 
0334     for (QAction *action : applet->contextualActions()) {
0335         if (action) {
0336             desktopMenu->addAction(action);
0337         }
0338     }
0339 
0340     if (!applet->failedToLaunch()) {
0341         QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
0342 
0343         if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
0344             desktopMenu->addAction(runAssociatedApplication);
0345         }
0346 
0347         QAction *configureApplet = applet->actions()->action(QStringLiteral("configure"));
0348 
0349         if (configureApplet && configureApplet->isEnabled()) {
0350             desktopMenu->addAction(configureApplet);
0351         }
0352 
0353         QAction *appletAlternatives = applet->actions()->action(QStringLiteral("alternatives"));
0354 
0355         if (appletAlternatives && appletAlternatives->isEnabled() && m_latteView->containment()->isUserConfiguring()) {
0356             desktopMenu->addAction(appletAlternatives);
0357         }
0358     }
0359 
0360     QAction *containmentAction = desktopMenu->menuAction();
0361     containmentAction->setText(i18nc("%1 is the name of the containment", "%1 Options", m_latteView->containment()->title()));
0362 
0363     addContainmentActions(containmentAction->menu(), event);
0364 
0365     if (!containmentAction->menu()->isEmpty()) {
0366         int enabled = 0;
0367         //count number of real actions
0368         QListIterator<QAction *> actionsIt(containmentAction->menu()->actions());
0369 
0370         while (enabled < 3 && actionsIt.hasNext()) {
0371             QAction *action = actionsIt.next();
0372 
0373             if (action->isVisible() && !action->isSeparator()) {
0374                 ++enabled;
0375             }
0376         }
0377 
0378         desktopMenu->addSeparator();
0379 
0380         if (enabled) {
0381             //if there is only one, don't create a submenu
0382             // if (enabled < 2) {
0383             for (QAction *action : containmentAction->menu()->actions()) {
0384                 if (action && action->isVisible()) {
0385                     desktopMenu->addAction(action);
0386                 }
0387             }
0388 
0389             // } else {
0390             //     desktopMenu->addMenu(containmentMenu);
0391             // }
0392         }
0393     }
0394 
0395     if (m_latteView->containment()->immutability() == Plasma::Types::Mutable &&
0396         (m_latteView->containment()->containmentType() != Plasma::Types::PanelContainment || m_latteView->containment()->isUserConfiguring())) {
0397         QAction *closeApplet = applet->actions()->action(QStringLiteral("remove"));
0398 
0399         //qDebug() << "checking for removal" << closeApplet;
0400         if (closeApplet) {
0401             if (!desktopMenu->isEmpty()) {
0402                 desktopMenu->addSeparator();
0403             }
0404 
0405             //qDebug() << "adding close action" << closeApplet->isEnabled() << closeApplet->isVisible();
0406             desktopMenu->addAction(closeApplet);
0407         }
0408     }
0409 }
0410 
0411 void ContextMenu::addContainmentActions(QMenu *desktopMenu, QEvent *event)
0412 {
0413     if (!m_latteView->containment()) {
0414         return;
0415     }
0416 
0417     if (m_latteView->containment()->corona()->immutability() != Plasma::Types::Mutable &&
0418         !KAuthorized::authorizeAction(QStringLiteral("plasma/containment_actions"))) {
0419         //qDebug() << "immutability";
0420         return;
0421     }
0422 
0423     //this is what ContainmentPrivate::prepareContainmentActions was
0424     const QString trigger = Plasma::ContainmentActions::eventToString(event);
0425     //"RightButton;NoModifier"
0426     Plasma::ContainmentActions *plugin = m_latteView->containment()->containmentActions().value(trigger);
0427 
0428     if (!plugin) {
0429         return;
0430     }
0431 
0432     if (plugin->containment() != m_latteView->containment()) {
0433         plugin->setContainment(m_latteView->containment());
0434         // now configure it
0435         KConfigGroup cfg(m_latteView->containment()->corona()->config(), "ActionPlugins");
0436         cfg = KConfigGroup(&cfg, QString::number(m_latteView->containment()->containmentType()));
0437         KConfigGroup pluginConfig = KConfigGroup(&cfg, trigger);
0438         plugin->restore(pluginConfig);
0439     }
0440 
0441     QList<QAction *> actions = plugin->contextualActions();
0442 
0443     for (const QAction *act : actions) {
0444         if (act->menu()) {
0445             //this is a workaround where Qt now creates the menu widget
0446             //in .exec before oxygen can polish it and set the following attribute
0447             act->menu()->setAttribute(Qt::WA_TranslucentBackground);
0448             //end workaround
0449 
0450             if (act->menu()->winId()) {
0451                 act->menu()->windowHandle()->setTransientParent(m_latteView);
0452             }
0453         }
0454     }
0455 
0456     desktopMenu->addActions(actions);
0457 
0458     return;
0459 }
0460 
0461 Plasma::Containment *ContextMenu::containmentById(uint id)
0462 {
0463     for (const auto containment : m_latteView->corona()->containments()) {
0464         if (id == containment->id()) {
0465             return containment;
0466         }
0467     }
0468 
0469     return 0;
0470 }
0471 
0472 }
0473 }