/* SPDX-FileCopyrightText: 2016 Kai Uwe Broulik SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "appmenuapplet.h" #include "../plugin/appmenumodel.h" #include #include #include #include #include #include #include #include #include #include int AppMenuApplet::s_refs = 0; namespace { QString viewService() { return QStringLiteral("org.kde.kappmenuview"); } } AppMenuApplet::AppMenuApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) : Plasma::Applet(parent, data, args) { ++s_refs; // if we're the first, register the service if (s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } /*it registers or unregisters the service when the destroyed value of the applet change, and not in the dtor, because: when we "delete" an applet, it just hides it for about a minute setting its status to destroyed, in order to be able to do a clean undo: if we undo, there will be another destroyedchanged and destroyed will be false. When this happens, if we are the only appmenu applet existing, the dbus interface will have to be registered again*/ connect(this, &Applet::destroyedChanged, this, [](bool destroyed) { if (destroyed) { // if we were the last, unregister if (--s_refs == 0) { QDBusConnection::sessionBus().interface()->unregisterService(viewService()); } } else { // if we're the first, register the service if (++s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } } }); } AppMenuApplet::~AppMenuApplet() = default; void AppMenuApplet::init() { } QAbstractItemModel *AppMenuApplet::model() const { return m_model; } void AppMenuApplet::setModel(QAbstractItemModel *model) { if (m_model != model) { m_model = model; Q_EMIT modelChanged(); } } int AppMenuApplet::view() const { return m_viewType; } void AppMenuApplet::setView(int type) { if (m_viewType != type) { m_viewType = type; Q_EMIT viewChanged(); } } int AppMenuApplet::currentIndex() const { return m_currentIndex; } void AppMenuApplet::setCurrentIndex(int currentIndex) { if (m_currentIndex != currentIndex) { m_currentIndex = currentIndex; Q_EMIT currentIndexChanged(); } } QQuickItem *AppMenuApplet::buttonGrid() const { return m_buttonGrid; } void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) { if (m_buttonGrid != buttonGrid) { m_buttonGrid = buttonGrid; Q_EMIT buttonGridChanged(); } } QMenu *AppMenuApplet::createMenu(int idx) const { QMenu *menu = nullptr; if (view() == CompactView) { if (QAction *menuAction = m_model->data(QModelIndex(), AppMenuModel::ActionRole).value()) { menu = menuAction->menu(); } } else if (view() == FullView) { const QModelIndex index = m_model->index(idx, 0); if (QAction *action = m_model->data(index, AppMenuModel::ActionRole).value()) { menu = action->menu(); } } return menu; } void AppMenuApplet::onMenuAboutToHide() { setCurrentIndex(-1); } Qt::Edges edgeFromLocation(Plasma::Types::Location location) { switch (location) { case Plasma::Types::TopEdge: return Qt::TopEdge; case Plasma::Types::BottomEdge: return Qt::BottomEdge; case Plasma::Types::LeftEdge: return Qt::LeftEdge; case Plasma::Types::RightEdge: return Qt::RightEdge; case Plasma::Types::Floating: case Plasma::Types::Desktop: case Plasma::Types::FullScreen: break; } return Qt::Edges(); } void AppMenuApplet::trigger(QQuickItem *ctx, int idx) { if (m_currentIndex == idx) { return; } if (!ctx || !ctx->window() || !ctx->window()->screen()) { return; } QMenu *actionMenu = createMenu(idx); if (actionMenu) { // this is a workaround where Qt will fail to realize a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing // by releasing manually we avoid that situation auto ungrabMouseHack = [ctx]() { if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) { // FIXME event forge thing enters press and hold move mode :/ ctx->window()->mouseGrabberItem()->ungrabMouse(); } }; QTimer::singleShot(0, ctx, ungrabMouseHack); // end workaround const auto &geo = ctx->window()->screen()->availableVirtualGeometry(); QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint()); const Qt::Edges edges = edgeFromLocation(location()); actionMenu->setProperty("_breeze_menu_seamless_edges", QVariant::fromValue(edges)); if (location() == Plasma::Types::TopEdge) { pos.setY(pos.y() + ctx->height()); } actionMenu->adjustSize(); pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()), qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height())); if (view() == FullView) { actionMenu->installEventFilter(this); } actionMenu->winId(); // create window handle actionMenu->windowHandle()->setTransientParent(ctx->window()); // hide the old menu only after showing the new one to avoid brief focus flickering on X11. // on wayland, you can't have more than one grabbing popup at a time so we show it after // the menu has hidden. thankfully, wayland doesn't have this flickering. if (!KWindowSystem::isPlatformWayland()) { actionMenu->popup(pos); } if (view() == FullView) { QMenu *oldMenu = m_currentMenu; m_currentMenu = actionMenu; if (oldMenu && oldMenu != actionMenu) { // don't initialize the currentIndex when another menu is already shown disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide); oldMenu->hide(); } } if (KWindowSystem::isPlatformWayland()) { actionMenu->popup(pos); } setCurrentIndex(idx); // FIXME TODO connect only once connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); } else { // is it just an action without a menu? if (QAction *action = m_model->index(idx, 0).data(AppMenuModel::ActionRole).value()) { Q_ASSERT(!action->menu()); action->trigger(); } } } // FIXME TODO doesn't work on submenu bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event) { auto *menu = qobject_cast(watched); if (!menu) { return false; } if (event->type() == QEvent::KeyPress) { auto *e = static_cast(event); // TODO right to left languages if (e->key() == Qt::Key_Left) { int desiredIndex = m_currentIndex - 1; Q_EMIT requestActivateIndex(desiredIndex); return true; } else if (e->key() == Qt::Key_Right) { if (menu->activeAction() && menu->activeAction()->menu()) { return false; } int desiredIndex = m_currentIndex + 1; Q_EMIT requestActivateIndex(desiredIndex); return true; } } else if (event->type() == QEvent::MouseMove) { auto *e = static_cast(event); if (!m_buttonGrid || !m_buttonGrid->window()) { return false; } // FIXME the panel margin breaks Fitt's law :( const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPosition()); const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos); auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y()); if (!item) { return false; } bool ok; const int buttonIndex = item->property("buttonIndex").toInt(&ok); if (!ok) { return false; } Q_EMIT requestActivateIndex(buttonIndex); } return false; } K_PLUGIN_CLASS(AppMenuApplet) #include "appmenuapplet.moc" #include "moc_appmenuapplet.cpp"