diff --git a/applets/CMakeLists.txt b/applets/CMakeLists.txt --- a/applets/CMakeLists.txt +++ b/applets/CMakeLists.txt @@ -6,6 +6,7 @@ plasma_install_package(panelspacer org.kde.plasma.panelspacer) plasma_install_package(lock_logout org.kde.plasma.lock_logout) +add_subdirectory(appmenu) add_subdirectory(systemmonitor) add_subdirectory(batterymonitor) add_subdirectory(calendar) diff --git a/applets/appmenu/CMakeLists.txt b/applets/appmenu/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/applets/appmenu/CMakeLists.txt @@ -0,0 +1,22 @@ +set(appmenuapplet_SRCS + appmenuapplet.cpp +) + +add_library(plasma_applet_appmenu MODULE ${appmenuapplet_SRCS}) + +kcoreaddons_desktop_to_json(plasma_applet_appmenu package/metadata.desktop) + +target_link_libraries(plasma_applet_appmenu + Qt5::Widgets + Qt5::Quick + KF5::Plasma + KF5::WindowSystem + dbusmenu-qt5) + +if(HAVE_X11) + target_link_libraries(plasma_applet_appmenu Qt5::X11Extras XCB::XCB) +endif() + +install(TARGETS plasma_applet_appmenu DESTINATION ${PLUGIN_INSTALL_DIR}/plasma/applets) + +plasma_install_package(package org.kde.plasma.appmenu) diff --git a/applets/appmenu/Messages.sh b/applets/appmenu/Messages.sh new file mode 100755 --- /dev/null +++ b/applets/appmenu/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.appmenu.pot +rm -f rc.cpp diff --git a/applets/appmenu/appmenuapplet.h b/applets/appmenu/appmenuapplet.h new file mode 100644 --- /dev/null +++ b/applets/appmenu/appmenuapplet.h @@ -0,0 +1,88 @@ +/* + * Copyright 2016 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +class KDBusMenuImporter; +class QQuickItem; +class QMenu; + +class AppMenuApplet : public Plasma::Applet +{ + Q_OBJECT + + // FIXME HACK just for testing + Q_PROPERTY(QStringList menu READ menu NOTIFY menuChanged) + + Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) + + Q_PROPERTY(QQuickItem *buttonGrid READ buttonGrid WRITE setButtonGrid NOTIFY buttonGridChanged) + +public: + explicit AppMenuApplet(QObject *parent, const QVariantList &data); + ~AppMenuApplet() override; + + void init() override; + + QStringList menu() const; + + int currentIndex() const; + + QQuickItem *buttonGrid() const; + void setButtonGrid(QQuickItem *buttonGrid); + + Q_INVOKABLE void trigger(QQuickItem *ctx, int index); + +signals: + void menuChanged(); + void currentIndexChanged(); + void buttonGridChanged(); + + void requestActivateIndex(int index); + +protected: + bool eventFilter(QObject *watched, QEvent *event); + +private: + void onActiveWindowChanged(WId id); + + void setCurrentIndex(int currentIndex); + + void updateMenu(const QString &serviceName, const QString &menuObjectPath); + + void onMenuAboutToHide(); + + KDBusMenuImporter *m_importer = nullptr; + + QString m_serviceName; + QString m_menuObjectPath; + + QStringList m_menu; + + QPointer m_currentMenu; + int m_currentIndex = -1; + + QQuickItem *m_buttonGrid = nullptr; // QPointer? + +}; diff --git a/applets/appmenu/appmenuapplet.cpp b/applets/appmenu/appmenuapplet.cpp new file mode 100644 --- /dev/null +++ b/applets/appmenu/appmenuapplet.cpp @@ -0,0 +1,371 @@ +/* + * Copyright 2016 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "appmenuapplet.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#if HAVE_X11 +#include +#include +#endif + +#include + +#include + +static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); +static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); + +class KDBusMenuImporter : public DBusMenuImporter +{ + +public: + KDBusMenuImporter(const QString &service, const QString &path, QObject *parent) + : DBusMenuImporter(service, path, ASYNCHRONOUS, parent) + { + + } + +protected: + QIcon iconForName(const QString &name) override + { + return QIcon::fromTheme(name); + } + +}; + +AppMenuApplet::AppMenuApplet(QObject *parent, const QVariantList &data) + : Plasma::Applet(parent, data) +{ + +} + +AppMenuApplet::~AppMenuApplet() = default; + +QStringList AppMenuApplet::menu() const +{ + return m_menu; +} + +int AppMenuApplet::currentIndex() const +{ + return m_currentIndex; +} + +void AppMenuApplet::setCurrentIndex(int currentIndex) +{ + if (m_currentIndex != currentIndex) { + m_currentIndex = currentIndex; + emit currentIndexChanged(); + } +} + +QQuickItem *AppMenuApplet::buttonGrid() const +{ + return m_buttonGrid; +} + +void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) +{ + if (m_buttonGrid != buttonGrid) { + m_buttonGrid = buttonGrid; + emit buttonGridChanged(); + } +} + +void AppMenuApplet::init() +{ + // TODO Wayland PlasmaShellSurface stuff + connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &AppMenuApplet::onActiveWindowChanged); + + onActiveWindowChanged(KWindowSystem::activeWindow()); +} + +void AppMenuApplet::onActiveWindowChanged(WId id) +{ + qDebug() << "active window changed" << id; + +#if HAVE_X11 + if (KWindowSystem::isPlatformX11()) { + auto *c = QX11Info::connection(); + + auto getWindowPropertyString = [c](WId id, const QByteArray &name) -> QByteArray { + QByteArray value; + + const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(c, false, name.length(), name.constData()); + QScopedPointer atomReply(xcb_intern_atom_reply(c, atomCookie, Q_NULLPTR)); + if (atomReply.isNull()) { + return value; + } + + static const long MAX_PROP_SIZE = 10000; + auto propertyCookie = xcb_get_property(c, false, id, atomReply->atom, XCB_ATOM_STRING, 0, MAX_PROP_SIZE); + QScopedPointer propertyReply(xcb_get_property_reply(c, propertyCookie, NULL)); + if (propertyReply.isNull()) { + return value; + } + + if (propertyReply->type == XCB_ATOM_STRING && propertyReply->format == 8 && propertyReply->value_len > 0) { + const char *data = (const char *) xcb_get_property_value(propertyReply.data()); + int len = propertyReply->value_len; + if (data) { + value = QByteArray(data, data[len - 1] ? len : len - 1); + } + } + + return value; + }; + + auto updateMenuFromWindowIfHasMenu = [this, &getWindowPropertyString](WId id) { + const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuServiceNamePropertyName)); + const QString menuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuObjectPathPropertyName)); + + if (!serviceName.isEmpty() && !menuObjectPath.isEmpty()) { + updateMenu(serviceName, menuObjectPath); + return true; + } + + return false; + }; + + KWindowInfo info(id, NET::WMState, NET::WM2TransientFor); + if (info.hasState(NET::SkipTaskbar) || info.windowType(NET::UtilityMask) == NET::Utility) { + return; + } + + WId transientId = info.transientFor(); + // lok at transient windows first + while (transientId) { + if (updateMenuFromWindowIfHasMenu(transientId)) { + return; + } + transientId = KWindowInfo(transientId, 0, NET::WM2TransientFor).transientFor(); + } + + if (updateMenuFromWindowIfHasMenu(id)) { + return; + } + } +#endif + + // in doubt just clear the menu :/ + m_menu.clear(); + emit menuChanged(); + +} + +void AppMenuApplet::updateMenu(const QString &serviceName, const QString &menuObjectPath) +{ + if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) { + if (m_importer) { + QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); + } + return; + } + + setCurrentIndex(-1); + + m_serviceName = serviceName; + m_menuObjectPath = menuObjectPath; + + if (m_importer) { + m_importer->deleteLater(); + m_importer = nullptr; + } + + m_importer = new KDBusMenuImporter(serviceName, menuObjectPath, this); + QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); + + /*connect(m_menuImporter, &DBusMenuImporter::actionActivationRequested, this, [=](QAction *action) { + // else send request to kwin or others dbus interface registrars + m_waitingAction = action; + emit showRequest(serviceName, menuObjectPath); + });*/ + + connect(m_importer, &DBusMenuImporter::menuUpdated, this, [=] { + QMenu *menu = m_importer->menu(); + if (!menu) { + return; + } + + const auto &actions = menu->actions(); + if (actions.isEmpty()) { + return; + } + + const auto oldMenu = m_menu; + + m_menu.clear(); + + for (QAction *action : actions) { + m_menu.append(action->text()); + } + + if (m_menu != oldMenu) { + emit menuChanged(); + } + }); + + /*connect(m_importer, &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) { + qDebug() << "AKTION ACTI" << action->text(); + });*/ +} + +void AppMenuApplet::trigger(QQuickItem *ctx, int index) +{ + if (!m_importer) { + return; + } + + QMenu *menu = m_importer->menu(); + if (!menu) { + return; + } + + QAction *action = menu->actions().at(index); + if (!action) { + return; + } + + if (QMenu *actionMenu = action->menu()) { + if (m_currentIndex == index) { + return; + } + + if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) { + // FIXME event forge thing enters press and hold move mode :/ + ctx->window()->mouseGrabberItem()->ungrabMouse(); + } + + const auto &geo = ctx->window()->screen()->availableVirtualGeometry(); + + QPoint pos = ctx->mapToGlobal(QPointF(0, 0)).toPoint(); + 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())); + + actionMenu->installEventFilter(this); + + QMenu *oldMenu = m_currentMenu; + m_currentMenu = actionMenu; + + actionMenu->popup(pos); + + // hide the old menu only after showing the new one to avoid brief flickering + // in other windows as they briefly re-gain focus + if (oldMenu) { + oldMenu->hide(); + } + + setCurrentIndex(index); + + // FIXME TODO connect only once + connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); + return; + } + + action->trigger(); +} + +void AppMenuApplet::onMenuAboutToHide() +{ + setCurrentIndex(-1); +} + +// 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; + if (desiredIndex < 0) { + desiredIndex = m_menu.count() - 1; + } + menu->hide(); + emit requestActivateIndex(desiredIndex); + return true; + } else if (e->key() == Qt::Key_Right) { + // if it has a submenu let QMenu handle it + if (menu->activeAction() && menu->activeAction()->menu()) { + return false; + } + + int desiredIndex = m_currentIndex + 1; + if (desiredIndex >= m_menu.count()) { + desiredIndex = 0; + } + emit requestActivateIndex(desiredIndex); + return true; + } + + } else if (event->type() == QEvent::MouseMove) { + auto *e = static_cast(event); + + if (!m_buttonGrid) { + return false; + } + + // FIXME the panel margin breaks Fitt's law :( + const QPointF &localPos = m_buttonGrid->mapFromGlobal(e->globalPos()); + auto *item = m_buttonGrid->childAt(localPos.x(), localPos.y()); + if (!item) { + return false; + } + + bool ok; + const int buttonIndex = item->property("buttonIndex").toInt(&ok); + if (!ok) { + return false; + } + + emit requestActivateIndex(buttonIndex); + } + + return false; +} + +K_EXPORT_PLASMA_APPLET_WITH_JSON(appmenu, AppMenuApplet, "metadata.json") + +#include "appmenuapplet.moc" diff --git a/applets/appmenu/package/contents/ui/main.qml b/applets/appmenu/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/applets/appmenu/package/contents/ui/main.qml @@ -0,0 +1,92 @@ +/* + * Copyright 2013 Heena Mahour + * Copyright 2013 Sebastian Kügler + * Copyright 2016 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.0 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Item { + id: root + + readonly property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical + + Layout.fillWidth: !root.vertical + Layout.fillHeight: root.vertical + //Plasmoid.switchWidth: units.gridUnit * 12 + //Plasmoid.switchHeight: units.gridUnit * 12 + + Layout.minimumWidth: units.iconSizes.large + Layout.minimumHeight: units.iconSizes.large + + //Plasmoid.compactRepresentation: // TODO single "Menu" button? + + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + + Plasmoid.fullRepresentation: GridLayout { + id: buttonGrid + + Layout.fillWidth: !root.vertical + Layout.fillHeight: root.vertical + flow: root.vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight + rowSpacing: units.smallSpacing + columnSpacing: units.smallSpacing + + Component.onCompleted: { + plasmoid.nativeInterface.buttonGrid = buttonGrid + } + + Connections { + target: plasmoid.nativeInterface + onRequestActivateIndex: { + var button = buttonRepeater.itemAt(index) + if (button) { + plasmoid.nativeInterface.trigger(button, index) + } + } + } + + Repeater { + id: buttonRepeater + model: plasmoid.nativeInterface.menu + + PlasmaComponents.ToolButton { + readonly property int buttonIndex: index + + Layout.preferredWidth: minimumWidth + Layout.fillWidth: root.vertical + Layout.fillHeight: !root.vertical + text: modelData + // fake highlighted + checkable: plasmoid.nativeInterface.currentIndex === index + checked: checkable + onClicked: { + plasmoid.nativeInterface.trigger(this, index) + } + } + } + + Item { // compact layout + Layout.fillWidth: true + Layout.fillHeight: true + } + + } +} diff --git a/applets/appmenu/package/metadata.desktop b/applets/appmenu/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/applets/appmenu/package/metadata.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Application Menu +Icon=show-menu +Type=Service +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@privat.broulik.de +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=org.kde.plasma.appmenu +X-KDE-PluginInfo-Version=2.0 +X-KDE-PluginInfo-Website=plasma.kde.org +X-KDE-ServiceTypes=Plasma/Applet +X-Plasma-API=declarativeappletscript +X-KDE-Library=plasma_applet_appmenu + +X-Plasma-MainScript=ui/main.qml +X-KDE-PluginInfo-Category=Windows and Tasks