File indexing completed on 2024-03-24 05:33:43
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 }