File indexing completed on 2024-05-26 05:38:20

0001 /*
0002     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "appmenuapplet.h"
0008 #include "../plugin/appmenumodel.h"
0009 
0010 #include <QAction>
0011 #include <QDBusConnection>
0012 #include <QDBusConnectionInterface>
0013 #include <QKeyEvent>
0014 #include <QMenu>
0015 #include <QMouseEvent>
0016 #include <QQuickItem>
0017 #include <QQuickWindow>
0018 #include <QScreen>
0019 #include <QTimer>
0020 
0021 int AppMenuApplet::s_refs = 0;
0022 namespace
0023 {
0024 QString viewService()
0025 {
0026     return QStringLiteral("org.kde.kappmenuview");
0027 }
0028 }
0029 
0030 AppMenuApplet::AppMenuApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0031     : Plasma::Applet(parent, data, args)
0032 {
0033     ++s_refs;
0034     // if we're the first, register the service
0035     if (s_refs == 1) {
0036         QDBusConnection::sessionBus().interface()->registerService(viewService(),
0037                                                                    QDBusConnectionInterface::QueueService,
0038                                                                    QDBusConnectionInterface::DontAllowReplacement);
0039     }
0040     /*it registers or unregisters the service when the destroyed value of the applet change,
0041       and not in the dtor, because:
0042       when we "delete" an applet, it just hides it for about a minute setting its status
0043       to destroyed, in order to be able to do a clean undo: if we undo, there will be
0044       another destroyedchanged and destroyed will be false.
0045       When this happens, if we are the only appmenu applet existing, the dbus interface
0046       will have to be registered again*/
0047     connect(this, &Applet::destroyedChanged, this, [](bool destroyed) {
0048         if (destroyed) {
0049             // if we were the last, unregister
0050             if (--s_refs == 0) {
0051                 QDBusConnection::sessionBus().interface()->unregisterService(viewService());
0052             }
0053         } else {
0054             // if we're the first, register the service
0055             if (++s_refs == 1) {
0056                 QDBusConnection::sessionBus().interface()->registerService(viewService(),
0057                                                                            QDBusConnectionInterface::QueueService,
0058                                                                            QDBusConnectionInterface::DontAllowReplacement);
0059             }
0060         }
0061     });
0062 }
0063 
0064 AppMenuApplet::~AppMenuApplet() = default;
0065 
0066 void AppMenuApplet::init()
0067 {
0068 }
0069 
0070 QAbstractItemModel *AppMenuApplet::model() const
0071 {
0072     return m_model;
0073 }
0074 
0075 void AppMenuApplet::setModel(QAbstractItemModel *model)
0076 {
0077     if (m_model != model) {
0078         m_model = model;
0079         Q_EMIT modelChanged();
0080     }
0081 }
0082 
0083 int AppMenuApplet::view() const
0084 {
0085     return m_viewType;
0086 }
0087 
0088 void AppMenuApplet::setView(int type)
0089 {
0090     if (m_viewType != type) {
0091         m_viewType = type;
0092         Q_EMIT viewChanged();
0093     }
0094 }
0095 
0096 int AppMenuApplet::currentIndex() const
0097 {
0098     return m_currentIndex;
0099 }
0100 
0101 void AppMenuApplet::setCurrentIndex(int currentIndex)
0102 {
0103     if (m_currentIndex != currentIndex) {
0104         m_currentIndex = currentIndex;
0105         Q_EMIT currentIndexChanged();
0106     }
0107 }
0108 
0109 QQuickItem *AppMenuApplet::buttonGrid() const
0110 {
0111     return m_buttonGrid;
0112 }
0113 
0114 void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid)
0115 {
0116     if (m_buttonGrid != buttonGrid) {
0117         m_buttonGrid = buttonGrid;
0118         Q_EMIT buttonGridChanged();
0119     }
0120 }
0121 
0122 QMenu *AppMenuApplet::createMenu(int idx) const
0123 {
0124     QMenu *menu = nullptr;
0125 
0126     if (view() == CompactView) {
0127         if (QAction *menuAction = m_model->data(QModelIndex(), AppMenuModel::ActionRole).value<QAction *>()) {
0128             menu = menuAction->menu();
0129         }
0130     } else if (view() == FullView) {
0131         const QModelIndex index = m_model->index(idx, 0);
0132         if (QAction *action = m_model->data(index, AppMenuModel::ActionRole).value<QAction *>()) {
0133             menu = action->menu();
0134         }
0135     }
0136 
0137     return menu;
0138 }
0139 
0140 void AppMenuApplet::onMenuAboutToHide()
0141 {
0142     setCurrentIndex(-1);
0143 }
0144 
0145 Qt::Edges edgeFromLocation(Plasma::Types::Location location)
0146 {
0147     switch (location) {
0148     case Plasma::Types::TopEdge:
0149         return Qt::TopEdge;
0150     case Plasma::Types::BottomEdge:
0151         return Qt::BottomEdge;
0152     case Plasma::Types::LeftEdge:
0153         return Qt::LeftEdge;
0154     case Plasma::Types::RightEdge:
0155         return Qt::RightEdge;
0156     case Plasma::Types::Floating:
0157     case Plasma::Types::Desktop:
0158     case Plasma::Types::FullScreen:
0159         break;
0160     }
0161     return Qt::Edges();
0162 }
0163 
0164 void AppMenuApplet::trigger(QQuickItem *ctx, int idx)
0165 {
0166     if (m_currentIndex == idx) {
0167         return;
0168     }
0169 
0170     if (!ctx || !ctx->window() || !ctx->window()->screen()) {
0171         return;
0172     }
0173 
0174     QMenu *actionMenu = createMenu(idx);
0175     if (actionMenu) {
0176         // this is a workaround where Qt will fail to realize a mouse has been released
0177         // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0178         // whilst the mouse is depressed
0179         // https://bugreports.qt.io/browse/QTBUG-59044
0180         // this causes the next click to go missing
0181 
0182         // by releasing manually we avoid that situation
0183         auto ungrabMouseHack = [ctx]() {
0184             if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) {
0185                 // FIXME event forge thing enters press and hold move mode :/
0186                 ctx->window()->mouseGrabberItem()->ungrabMouse();
0187             }
0188         };
0189 
0190         QTimer::singleShot(0, ctx, ungrabMouseHack);
0191         // end workaround
0192 
0193         const auto &geo = ctx->window()->screen()->availableVirtualGeometry();
0194 
0195         QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint());
0196 
0197         const Qt::Edges edges = edgeFromLocation(location());
0198         actionMenu->setProperty("_breeze_menu_seamless_edges", QVariant::fromValue(edges));
0199 
0200         if (location() == Plasma::Types::TopEdge) {
0201             pos.setY(pos.y() + ctx->height());
0202         }
0203 
0204         actionMenu->adjustSize();
0205 
0206         pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()),
0207                      qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height()));
0208 
0209         if (view() == FullView) {
0210             actionMenu->installEventFilter(this);
0211         }
0212 
0213         actionMenu->winId(); // create window handle
0214         actionMenu->windowHandle()->setTransientParent(ctx->window());
0215 
0216         // hide the old menu only after showing the new one to avoid brief focus flickering on X11.
0217         // on wayland, you can't have more than one grabbing popup at a time so we show it after
0218         // the menu has hidden. thankfully, wayland doesn't have this flickering.
0219         if (!KWindowSystem::isPlatformWayland()) {
0220             actionMenu->popup(pos);
0221         }
0222 
0223         if (view() == FullView) {
0224             QMenu *oldMenu = m_currentMenu;
0225             m_currentMenu = actionMenu;
0226             if (oldMenu && oldMenu != actionMenu) {
0227                 // don't initialize the currentIndex when another menu is already shown
0228                 disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide);
0229                 oldMenu->hide();
0230             }
0231         }
0232 
0233         if (KWindowSystem::isPlatformWayland()) {
0234             actionMenu->popup(pos);
0235         }
0236 
0237         setCurrentIndex(idx);
0238 
0239         // FIXME TODO connect only once
0240         connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection);
0241     } else { // is it just an action without a menu?
0242         if (QAction *action = m_model->index(idx, 0).data(AppMenuModel::ActionRole).value<QAction *>()) {
0243             Q_ASSERT(!action->menu());
0244             action->trigger();
0245         }
0246     }
0247 }
0248 
0249 // FIXME TODO doesn't work on submenu
0250 bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event)
0251 {
0252     auto *menu = qobject_cast<QMenu *>(watched);
0253     if (!menu) {
0254         return false;
0255     }
0256 
0257     if (event->type() == QEvent::KeyPress) {
0258         auto *e = static_cast<QKeyEvent *>(event);
0259 
0260         // TODO right to left languages
0261         if (e->key() == Qt::Key_Left) {
0262             int desiredIndex = m_currentIndex - 1;
0263             Q_EMIT requestActivateIndex(desiredIndex);
0264             return true;
0265         } else if (e->key() == Qt::Key_Right) {
0266             if (menu->activeAction() && menu->activeAction()->menu()) {
0267                 return false;
0268             }
0269 
0270             int desiredIndex = m_currentIndex + 1;
0271             Q_EMIT requestActivateIndex(desiredIndex);
0272             return true;
0273         }
0274 
0275     } else if (event->type() == QEvent::MouseMove) {
0276         auto *e = static_cast<QMouseEvent *>(event);
0277 
0278         if (!m_buttonGrid || !m_buttonGrid->window()) {
0279             return false;
0280         }
0281 
0282         // FIXME the panel margin breaks Fitt's law :(
0283         const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPosition());
0284         const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos);
0285         auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y());
0286         if (!item) {
0287             return false;
0288         }
0289 
0290         bool ok;
0291         const int buttonIndex = item->property("buttonIndex").toInt(&ok);
0292         if (!ok) {
0293             return false;
0294         }
0295 
0296         Q_EMIT requestActivateIndex(buttonIndex);
0297     }
0298 
0299     return false;
0300 }
0301 
0302 K_PLUGIN_CLASS(AppMenuApplet)
0303 
0304 #include "appmenuapplet.moc"
0305 #include "moc_appmenuapplet.cpp"