Add the global menu

pull/7/head
reionwong 5 years ago
parent 4a635c6acc
commit bb2e092579

@ -14,6 +14,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 CONFIG REQUIRED Widgets DBus X11Extras Concurrent Svg LinguistTools QuickControls2) find_package(Qt5 CONFIG REQUIRED Widgets DBus X11Extras Concurrent Svg LinguistTools QuickControls2)
find_package(KF5WindowSystem REQUIRED) find_package(KF5WindowSystem REQUIRED)
find_package(FishUI REQUIRED) find_package(FishUI REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(XCB_LIBS REQUIRED xcb)
set(SRCS set(SRCS
src/main.cpp src/main.cpp
@ -38,6 +41,20 @@ set(SRCS
src/libdbusmenuqt/dbusmenutypes_p.cpp src/libdbusmenuqt/dbusmenutypes_p.cpp
src/libdbusmenuqt/utils.cpp src/libdbusmenuqt/utils.cpp
src/appmenu/kdbusimporter.h
src/appmenu/appmenu.h
src/appmenu/appmenu.cpp
src/appmenu/appmenudbus.h
src/appmenu/appmenudbus.cpp
src/appmenu/verticalmenu.h
src/appmenu/verticalmenu.cpp
src/appmenu/menuimporter.h
src/appmenu/menuimporter.cpp
src/appmenu/appmenumodel.h
src/appmenu/appmenumodel.cpp
src/appmenu/appmenuapplet.h
src/appmenu/appmenuapplet.cpp
qml.qrc qml.qrc
) )
@ -64,8 +81,15 @@ set_source_files_properties(src/libdbusmenuqt/com.canonical.dbusmenu.xml PROPERT
) )
qt_add_dbus_interface(libdbusmenu_SRCS src/libdbusmenuqt/com.canonical.dbusmenu.xml dbusmenu_interface) qt_add_dbus_interface(libdbusmenu_SRCS src/libdbusmenuqt/com.canonical.dbusmenu.xml dbusmenu_interface)
add_executable(cutefish-statusbar ${SRCS} ${libdbusmenu_SRCS}) ## appmenu
qt_add_dbus_adaptor(appmenu_SRCS src/appmenu/com.canonical.AppMenu.Registrar.xml
src/appmenu/menuimporter.h MenuImporter menuimporteradaptor MenuImporterAdaptor)
qt_add_dbus_adaptor(appmenu_SRCS src/appmenu/org.cutefish.cappmenu.xml
src/appmenu/appmenudbus.h AppmenuDBus appmenuadaptor AppmenuAdaptor)
add_executable(cutefish-statusbar ${SRCS} ${libdbusmenu_SRCS} ${appmenu_SRCS})
target_include_directories(cutefish-statusbar PUBLIC ${XCB_LIBS_INCLUDE_DIRS})
target_link_libraries(cutefish-statusbar target_link_libraries(cutefish-statusbar
PRIVATE PRIVATE
Qt5::Core Qt5::Core
@ -80,6 +104,7 @@ target_link_libraries(cutefish-statusbar
FishUI FishUI
KF5::WindowSystem KF5::WindowSystem
${XCB_LIBS_LIBRARIES}
) )
file(GLOB TS_FILES translations/*.ts) file(GLOB TS_FILES translations/*.ts)

@ -1,3 +1,22 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <aj@cutefishos.com>
*
* 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 3 of the License, or
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12

@ -1,3 +1,22 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <aj@cutefishos.com>
*
* 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 3 of the License, or
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
@ -92,9 +111,9 @@ ControlCenterDialog {
Image { Image {
id: userIcon id: userIcon
Layout.fillHeight: true height: 48
Layout.preferredWidth: height width: height
sourceSize: Qt.size(width, height) sourceSize: Qt.size(width, width)
source: currentUser.iconFileName ? "file:///" + currentUser.iconFileName : "image://icontheme/default-user" source: currentUser.iconFileName ? "file:///" + currentUser.iconFileName : "image://icontheme/default-user"
layer.enabled: true layer.enabled: true

@ -1,3 +1,22 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <aj@cutefishos.com>
*
* 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 3 of the License, or
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12

@ -1,3 +1,22 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <aj@cutefishos.com>
*
* 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 3 of the License, or
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
@ -9,7 +28,7 @@ import Cutefish.StatusBar 1.0
Item { Item {
id: control id: control
property var popupText: "" property string popupText: ""
signal clicked signal clicked
signal rightClicked signal rightClicked

@ -1,3 +1,22 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <aj@cutefishos.com>
*
* 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 3 of the License, or
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -47,6 +66,7 @@ Item {
anchors.rightMargin: FishUI.Units.smallSpacing anchors.rightMargin: FishUI.Units.smallSpacing
spacing: FishUI.Units.smallSpacing spacing: FishUI.Units.smallSpacing
// App name
StandardItem { StandardItem {
id: acticityItem id: acticityItem
Layout.fillHeight: true Layout.fillHeight: true
@ -85,8 +105,67 @@ Item {
} }
} }
// App menu
Item { Item {
id: appMenuItem
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
ListView {
id: appMenuView
anchors.fill: parent
orientation: Qt.Horizontal
spacing: FishUI.Units.smallSpacing
visible: appMenuModel.visible
interactive: false
clip: true
model: appMenuModel
delegate: StandardItem {
id: _menuItem
width: _actionText.width + FishUI.Units.largeSpacing
height: ListView.view.height
onClicked: {
appMenuApplet.trigger(_menuItem, index)
}
Text {
id: _actionText
anchors.centerIn: parent
color: FishUI.Theme.textColor
text: {
var text = activeMenu
text = text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) {
return p1.concat(p2, p3)
})
return text
}
}
// QMenu opens on press, so we'll replicate that here
MouseArea {
anchors.fill: parent
hoverEnabled: appMenuApplet.currentIndex !== -1
onPressed: parent.clicked()
onEntered: parent.clicked()
}
}
AppMenuModel {
id: appMenuModel
onRequestActivateIndex: appMenuApplet.requestActivateIndex(appMenuView.currentIndex)
Component.onCompleted: {
appMenuView.model = appMenuModel
}
}
AppMenuApplet {
id: appMenuApplet
model: appMenuModel
}
}
} }
ListView { ListView {
@ -98,8 +177,8 @@ Item {
clip: true clip: true
spacing: FishUI.Units.smallSpacing spacing: FishUI.Units.smallSpacing
property var itemSize: rootItem.height * 0.8 property real itemSize: rootItem.height * 0.8
property var itemWidth: itemSize + FishUI.Units.smallSpacing property real itemWidth: itemSize + FishUI.Units.smallSpacing
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: (itemWidth + (count - 1) * FishUI.Units.smallSpacing) * count Layout.preferredWidth: (itemWidth + (count - 1) * FishUI.Units.smallSpacing) * count

@ -0,0 +1,244 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Copyright (c) 2016 Kai Uwe Broulik <kde@privat.broulik.de>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "appmenu.h"
#include "appmenudbus.h"
#include "appmenuadaptor.h"
#include "kdbusimporter.h"
#include "menuimporteradaptor.h"
#include "verticalmenu.h"
// Qt
#include <QApplication>
#include <QDBusInterface>
#include <QMenu>
// X11
#include <QX11Info>
#include <xcb/xcb.h>
static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME");
static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH");
AppMenu::AppMenu(QObject *parent)
: QObject(parent)
, m_appmenuDBus(new AppmenuDBus(this))
{
reconfigure();
m_appmenuDBus->connectToBus();
connect(m_appmenuDBus, &AppmenuDBus::appShowMenu, this, &AppMenu::slotShowMenu);
connect(m_appmenuDBus, &AppmenuDBus::reconfigured, this, &AppMenu::reconfigure);
// transfer our signals to dbus
connect(this, &AppMenu::showRequest, m_appmenuDBus, &AppmenuDBus::showRequest);
connect(this, &AppMenu::menuHidden, m_appmenuDBus, &AppmenuDBus::menuHidden);
connect(this, &AppMenu::menuShown, m_appmenuDBus, &AppmenuDBus::menuShown);
m_menuViewWatcher = new QDBusServiceWatcher(QStringLiteral("org.cutefish.cappmenuview"),
QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
this);
auto setupMenuImporter = [this]() {
QDBusConnection::sessionBus().connect({},
{},
QStringLiteral("com.canonical.dbusmenu"),
QStringLiteral("ItemActivationRequested"),
this,
SLOT(itemActivationRequested(int, uint)));
// Setup a menu importer if needed
if (!m_menuImporter) {
m_menuImporter = new MenuImporter(this);
connect(m_menuImporter, &MenuImporter::WindowRegistered, this, &AppMenu::slotWindowRegistered);
m_menuImporter->connectToBus();
}
};
connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceRegistered, this, setupMenuImporter);
connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) {
Q_UNUSED(service)
QDBusConnection::sessionBus().disconnect({},
{},
QStringLiteral("com.canonical.dbusmenu"),
QStringLiteral("ItemActivationRequested"),
this,
SLOT(itemActivationRequested(int, uint)));
delete m_menuImporter;
m_menuImporter = nullptr;
});
// if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kappmenuview"))) {
setupMenuImporter();
// }
if (!QX11Info::connection()) {
m_xcbConn = xcb_connect(nullptr, nullptr);
}
}
AppMenu::~AppMenu()
{
if (m_xcbConn) {
xcb_disconnect(m_xcbConn);
}
}
bool AppMenu::eventFilter(QObject *object, QEvent *event)
{
// HACK we need an input serial to create popups but Qt only sets them on click
// if (object == m_menu && event->type() == QEvent::Enter && m_plasmashell) {
// auto waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow *>(m_menu->windowHandle()->handle());
// if (waylandWindow) {
// const auto device = waylandWindow->display()->currentInputDevice();
// waylandWindow->display()->setLastInputDevice(device, device->pointer()->mEnterSerial, waylandWindow);
// }
// }
return AppMenu::eventFilter(object, event);
}
void AppMenu::slotWindowRegistered(WId id, const QString &serviceName, const QDBusObjectPath &menuObjectPath)
{
auto *c = QX11Info::connection();
if (!c) {
c = m_xcbConn;
}
if (c) {
static xcb_atom_t s_serviceNameAtom = XCB_ATOM_NONE;
static xcb_atom_t s_objectPathAtom = XCB_ATOM_NONE;
auto setWindowProperty = [c](WId id, xcb_atom_t &atom, const QByteArray &name, const QByteArray &value) {
if (atom == XCB_ATOM_NONE) {
const xcb_intern_atom_cookie_t cookie = xcb_intern_atom(c, false, name.length(), name.constData());
QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> reply(xcb_intern_atom_reply(c, cookie, nullptr));
if (reply.isNull()) {
return;
}
atom = reply->atom;
if (atom == XCB_ATOM_NONE) {
return;
}
}
auto cookie = xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData());
xcb_generic_error_t *error;
if ((error = xcb_request_check(c, cookie))) {
qWarning() << "Got an error";
free(error);
return;
}
};
// TODO only set the property if it doesn't already exist
setWindowProperty(id, s_serviceNameAtom, s_x11AppMenuServiceNamePropertyName, serviceName.toUtf8());
setWindowProperty(id, s_objectPathAtom, s_x11AppMenuObjectPathPropertyName, menuObjectPath.path().toUtf8());
}
}
void AppMenu::slotShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId)
{
if (!m_menuImporter) {
return;
}
// If menu visible, hide it
if (m_menu && m_menu.data()->isVisible()) {
m_menu.data()->hide();
return;
}
// dbus call by user (for khotkey shortcut)
if (x == -1 || y == -1) {
// We do not know kwin button position, so tell kwin to show menu
emit showRequest(serviceName, menuObjectPath, actionId);
return;
}
auto *importer = new KDBusMenuImporter(serviceName, menuObjectPath.path(), this);
QMetaObject::invokeMethod(importer, "updateMenu", Qt::QueuedConnection);
disconnect(importer, nullptr, this, nullptr); // ensure we don't popup multiple times in case the menu updates again later
connect(importer, &KDBusMenuImporter::menuUpdated, this, [=](QMenu *m) {
QMenu *menu = importer->menu();
if (!menu || menu != m) {
return;
}
m_menu = qobject_cast<VerticalMenu *>(menu);
m_menu.data()->setServiceName(serviceName);
m_menu.data()->setMenuObjectPath(menuObjectPath);
connect(m_menu.data(), &QMenu::aboutToHide, this, [this, importer] {
hideMenu();
importer->deleteLater();
});
// if (m_plasmashell) {
// connect(m_menu.data(), &QMenu::aboutToShow, this, &AppMenuModule::initMenuWayland, Qt::UniqueConnection);
// m_menu.data()->popup(QPoint(x, y));
// } else {
m_menu.data()->popup(QPoint(x, y) / qApp->devicePixelRatio());
// }
QAction *actiontoActivate = importer->actionForId(actionId);
emit menuShown(serviceName, menuObjectPath);
if (actiontoActivate) {
m_menu.data()->setActiveAction(actiontoActivate);
}
});
}
void AppMenu::reconfigure()
{
}
void AppMenu::itemActivationRequested(int actionId, uint timeStamp)
{
Q_UNUSED(timeStamp);
emit showRequest(message().service(), QDBusObjectPath(message().path()), actionId);
}
void AppMenu::hideMenu()
{
if (m_menu) {
emit menuHidden(m_menu.data()->serviceName(), m_menu->menuObjectPath());
}
}
void AppMenu::fakeUnityAboutToShow(const QString &service, const QDBusObjectPath &menuObjectPath)
{
}
KDBusMenuImporter *AppMenu::getImporter(const QString &service, const QString &path)
{
}

@ -0,0 +1,100 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Copyright (c) 2016 Kai Uwe Broulik <kde@privat.broulik.de>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#ifndef APPMENU_H
#define APPMENU_H
#include <QObject>
#include <xcb/xcb.h>
#include "menuimporter.h"
#include <QPointer>
class QDBusServiceWatcher;
class KDBusMenuImporter;
class AppmenuDBus;
class VerticalMenu;
class AppMenu : public QObject, protected QDBusContext
{
Q_OBJECT
public:
explicit AppMenu(QObject *parent = nullptr);
~AppMenu() override;
bool eventFilter(QObject *object, QEvent *event) override;
Q_SIGNALS:
/**
* We do not know where is menu decoration button, so tell kwin to show menu
*/
void showRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId);
/**
* This signal is emitted whenever popup menu/menubar is shown
* Useful for decorations to know if menu button should look pressed
*/
void menuShown(const QString &service, const QDBusObjectPath &objectPath);
/**
* This signal is emitted whenever popup menu/menubar is hidden
* Useful for decorations to know if menu button should be release
*/
void menuHidden(const QString &service, const QDBusObjectPath &objectPath);
private Q_SLOTS:
/**
* A new window was registered to AppMenu
*
* For compatibility this will set the DBus service name and menu object path as properties
* on the window so we keep working with clients that use the DBusMenu "properly".
*/
void slotWindowRegistered(WId id, const QString &serviceName, const QDBusObjectPath &menuObjectPath);
/**
* Show menu at QPoint(x,y) for DBus serviceName and menuObjectPath
* if x or y == -1, show in application window
*/
void slotShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId);
/**
* Reconfigure module
*/
void reconfigure();
void itemActivationRequested(int actionId, uint timeStamp);
private:
void hideMenu();
void fakeUnityAboutToShow(const QString &service, const QDBusObjectPath &menuObjectPath);
KDBusMenuImporter *getImporter(const QString &service, const QString &path);
MenuImporter *m_menuImporter = nullptr;
AppmenuDBus *m_appmenuDBus;
QDBusServiceWatcher *m_menuViewWatcher;
QPointer<VerticalMenu> m_menu;
xcb_connection_t *m_xcbConn = nullptr;
};
#endif // APPMENU_H

@ -0,0 +1,263 @@
/*
* Copyright 2021 Reion Wong <aj@cutefishos.com>
* Copyright 2016 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "appmenuapplet.h"
#include <QAction>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QKeyEvent>
#include <QMenu>
#include <QMouseEvent>
#include <QQuickItem>
#include <QQuickWindow>
#include <QScreen>
#include <QTimer>
int AppMenuApplet::s_refs = 0;
static QString viewService()
{
return QStringLiteral("org.cutefish.cappmenuview");
}
AppMenuApplet::AppMenuApplet(QObject *parent)
: QObject(parent)
{
++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);
// }
// }
// });
}
int AppMenuApplet::currentIndex() const
{
return m_currentIndex;
}
QQuickItem *AppMenuApplet::buttonGrid() const
{
return m_buttonGrid;
}
void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid)
{
if (m_buttonGrid != buttonGrid) {
m_buttonGrid = buttonGrid;
emit buttonGridChanged();
}
}
AppMenuModel *AppMenuApplet::model() const
{
return m_model;
}
void AppMenuApplet::setModel(AppMenuModel *model)
{
if (m_model != model) {
m_model = model;
emit modelChanged();
}
}
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());
// Top
pos.setY(pos.y() + ctx->height() + 6);
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);
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);
}
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?
const QVariant data = m_model->index(idx, 0).data(AppMenuModel::ActionRole);
QAction *action = static_cast<QAction *>(data.value<void *>());
if (action) {
Q_ASSERT(!action->menu());
action->trigger();
}
}
}
bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event)
{
auto *menu = qobject_cast<QMenu *>(watched);
if (!menu) {
return false;
}
if (event->type() == QEvent::KeyPress) {
auto *e = static_cast<QKeyEvent *>(event);
// TODO right to left languages
if (e->key() == Qt::Key_Left) {
int desiredIndex = m_currentIndex - 1;
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;
emit requestActivateIndex(desiredIndex);
return true;
}
} else if (event->type() == QEvent::MouseMove) {
auto *e = static_cast<QMouseEvent *>(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->globalPos());
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;
}
emit requestActivateIndex(buttonIndex);
}
return false;
}
QMenu *AppMenuApplet::createMenu(int idx) const
{
QMenu *menu = nullptr;
QAction *action = nullptr;
const QModelIndex index = m_model->index(idx, 0);
const QVariant data = m_model->data(index, AppMenuModel::ActionRole);
action = (QAction *)data.value<void *>();
if (action) {
menu = action->menu();
}
return menu;
}
void AppMenuApplet::setCurrentIndex(int currentIndex)
{
if (m_currentIndex != currentIndex) {
m_currentIndex = currentIndex;
emit currentIndexChanged();
}
}
void AppMenuApplet::onMenuAboutToHide()
{
setCurrentIndex(-1);
}

@ -0,0 +1,76 @@
/*
* Copyright 2021 Reion Wong <aj@cutefishos.com>
* Copyright 2016 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef APPMENUAPPLET_H
#define APPMENUAPPLET_H
#include <QObject>
#include <QQuickItem>
#include <QPointer>
#include <QMenu>
#include "appmenumodel.h"
class AppMenuApplet : public QObject
{
Q_OBJECT
Q_PROPERTY(AppMenuModel *model READ model WRITE setModel NOTIFY modelChanged)
Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(QQuickItem *buttonGrid READ buttonGrid WRITE setButtonGrid NOTIFY buttonGridChanged)
public:
explicit AppMenuApplet(QObject *parent = nullptr);
int currentIndex() const;
QQuickItem *buttonGrid() const;
void setButtonGrid(QQuickItem *buttonGrid);
AppMenuModel *model() const;
void setModel(AppMenuModel *model);
Q_SIGNALS:
void modelChanged();
void viewChanged();
void currentIndexChanged();
void buttonGridChanged();
void requestActivateIndex(int index);
public slots:
Q_INVOKABLE void trigger(QQuickItem *ctx, int idx);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
QMenu *createMenu(int idx) const;
void setCurrentIndex(int currentIndex);
void onMenuAboutToHide();
int m_currentIndex = -1;
QPointer<QMenu> m_currentMenu;
QPointer<QQuickItem> m_buttonGrid;
QPointer<AppMenuModel> m_model;
static int s_refs;
};
#endif // APPMENUAPPLET_H

@ -0,0 +1,66 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "appmenudbus.h"
#include "appmenuadaptor.h"
#include "kdbusimporter.h"
#include <QApplication>
#include <QDBusMessage>
#include <QDBusServiceWatcher>
static const char *DBUS_SERVICE = "org.cutefish.cappmenu";
static const char *DBUS_OBJECT_PATH = "/CAppMenu";
AppmenuDBus::AppmenuDBus(QObject *parent)
: QObject(parent)
{
}
AppmenuDBus::~AppmenuDBus()
{
}
bool AppmenuDBus::connectToBus(const QString &service, const QString &path)
{
m_service = service.isEmpty() ? DBUS_SERVICE : service;
QString newPath = path.isEmpty() ? DBUS_OBJECT_PATH : path;
if (!QDBusConnection::sessionBus().registerService(m_service)) {
return false;
}
new AppmenuAdaptor(this);
QDBusConnection::sessionBus().registerObject(newPath, this);
return true;
}
void AppmenuDBus::showMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId)
{
emit appShowMenu(x, y, serviceName, menuObjectPath, actionId);
}
void AppmenuDBus::reconfigure()
{
emit reconfigured();
}

@ -0,0 +1,85 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#ifndef APPMENU_DBUS_H
#define APPMENU_DBUS_H
// Qt
#include <QDBusContext>
#include <QDBusObjectPath>
#include <QDebug>
#include <QObject>
#include <qwindowdefs.h>
class AppmenuDBus : public QObject, protected QDBusContext
{
Q_OBJECT
public:
explicit AppmenuDBus(QObject *);
~AppmenuDBus() override;
bool connectToBus(const QString &service = QString(), const QString &path = QString());
/**
* DBus method showing menu at QPoint(x,y) for given DBus service name and menuObjectPath
* if x or y == -1, show in application window
*/
void showMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId);
/**
* DBus method reconfiguring kded module
*/
void reconfigure();
Q_SIGNALS:
/**
* This signal is emitted on showMenu() request
*/
void appShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId);
/**
* This signal is emitted on reconfigure() request
*/
void reconfigured();
// Dbus signals
/**
* This signal is emitted whenever kded want to show menu
* We do not know where is menu decoration button, so tell kwin to show menu
*/
void showRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId);
/**
* This signal is emitted whenever popup menu/menubar is shown
* Useful for decorations to know if menu button should look pressed
*/
void menuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath);
/**
* This signal is emitted whenever popup menu/menubar is hidden
* Useful for decorations to know if menu button should be release
*/
void menuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath);
private:
QString m_service;
};
#endif // APPMENU_DBUS_H

@ -0,0 +1,249 @@
/******************************************************************
* Copyright 2021 Reion Wong <aj@cutefishos.com>
* Copyright 2016 Kai Uwe Broulik <kde@privat.broulik.de>
* Copyright 2016 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
*
*
* 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 <http://www.gnu.org/licenses/>.
*
******************************************************************/
#include "appmenumodel.h"
#include <QAction>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <QGuiApplication>
#include <QMenu>
#include <KWindowSystem>
#include "../libdbusmenuqt/dbusmenuimporter.h"
#include <QDebug>
class CDBusMenuImporter : public DBusMenuImporter
{
public:
CDBusMenuImporter(const QString &service, const QString &path, QObject *parent)
: DBusMenuImporter(service, path, parent)
{
}
protected:
QIcon iconForName(const QString &name) override
{
return QIcon::fromTheme(name);
}
};
AppMenuModel::AppMenuModel(QObject *parent)
: QAbstractListModel(parent)
, m_serviceWatcher(new QDBusServiceWatcher(this))
{
connect(this, &AppMenuModel::modelNeedsUpdate, this, [this] {
if (!m_updatePending) {
m_updatePending = true;
QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
}
});
// Active window
connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &AppMenuModel::onActiveWindowChanged, Qt::QueuedConnection);
connect(KWindowSystem::self(), static_cast<void (KWindowSystem::*)(WId)>(&KWindowSystem::windowChanged), this, &AppMenuModel::onActiveWindowChanged, Qt::QueuedConnection);
onActiveWindowChanged();
m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
// if our current DBus connection gets lost, close the menu
// we'll select the new menu when the focus changes
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) {
if (serviceName == m_serviceName) {
setMenuAvailable(false);
emit modelNeedsUpdate();
}
});
}
AppMenuModel::~AppMenuModel() = default;
bool AppMenuModel::menuAvailable() const
{
return m_menuAvailable;
}
void AppMenuModel::setMenuAvailable(bool set)
{
if (m_menuAvailable != set) {
m_menuAvailable = set;
setVisible(true);
emit menuAvailableChanged();
}
}
bool AppMenuModel::visible() const
{
return m_visible;
}
void AppMenuModel::setVisible(bool visible)
{
if (m_visible != visible) {
m_visible = visible;
emit visibleChanged();
}
}
QRect AppMenuModel::screenGeometry() const
{
// return m_tasksModel->screenGeometry();
}
void AppMenuModel::setScreenGeometry(QRect geometry)
{
// m_tasksModel->setScreenGeometry(geometry);
}
int AppMenuModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if (!m_menuAvailable || !m_menu) {
return 0;
}
return m_menu->actions().count();
}
void AppMenuModel::update()
{
beginResetModel();
endResetModel();
m_updatePending = false;
}
void AppMenuModel::onActiveWindowChanged()
{
KWindowInfo info(KWindowSystem::activeWindow(),
NET::WMState | NET::WMVisibleName,
NET::WM2AppMenuObjectPath | NET::WM2AppMenuServiceName);
const QString objectPath = info.applicationMenuObjectPath();
const QString serviceName = info.applicationMenuServiceName();
if (!objectPath.isEmpty() && !serviceName.isEmpty()) {
setMenuAvailable(true);
updateApplicationMenu(serviceName, objectPath);
setVisible(true);
emit modelNeedsUpdate();
} else {
setMenuAvailable(false);
setVisible(false);
}
}
QHash<int, QByteArray> AppMenuModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[MenuRole] = QByteArrayLiteral("activeMenu");
roleNames[ActionRole] = QByteArrayLiteral("activeActions");
return roleNames;
}
QVariant AppMenuModel::data(const QModelIndex &index, int role) const
{
const int row = index.row();
if (row < 0 || !m_menuAvailable || !m_menu) {
return QVariant();
}
const auto actions = m_menu->actions();
if (row >= actions.count()) {
return QVariant();
}
if (role == MenuRole) { // TODO this should be Qt::DisplayRole
return actions.at(row)->text();
} else if (role == ActionRole) {
return QVariant::fromValue((void *)actions.at(row));
}
return QVariant();
}
void AppMenuModel::updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath)
{
if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) {
if (m_importer) {
QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection);
}
return;
}
m_serviceName = serviceName;
m_serviceWatcher->setWatchedServices(QStringList({m_serviceName}));
m_menuObjectPath = menuObjectPath;
if (m_importer) {
m_importer->deleteLater();
}
m_importer = new CDBusMenuImporter(serviceName, menuObjectPath, this);
QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection);
connect(m_importer.data(), &DBusMenuImporter::menuUpdated, this, [=](QMenu *menu) {
m_menu = m_importer->menu();
if (m_menu.isNull() || menu != m_menu) {
return;
}
// cache first layer of sub menus, which we'll be popping up
const auto actions = m_menu->actions();
for (QAction *a : actions) {
// signal dataChanged when the action changes
connect(a, &QAction::changed, this, [this, a] {
if (m_menuAvailable && m_menu) {
const int actionIdx = m_menu->actions().indexOf(a);
if (actionIdx > -1) {
const QModelIndex modelIdx = index(actionIdx, 0);
emit dataChanged(modelIdx, modelIdx);
}
}
});
connect(a, &QAction::destroyed, this, &AppMenuModel::modelNeedsUpdate);
if (a->menu()) {
m_importer->updateMenu(a->menu());
}
}
setMenuAvailable(true);
emit modelNeedsUpdate();
});
connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) {
// TODO submenus
if (!m_menuAvailable || !m_menu) {
return;
}
const auto actions = m_menu->actions();
auto it = std::find(actions.begin(), actions.end(), action);
if (it != actions.end()) {
Q_EMIT requestActivateIndex(it - actions.begin());
}
});
}

@ -0,0 +1,102 @@
/******************************************************************
* Copyright 2021 Reion Wong <aj@cutefishos.com>
* Copyright 2016 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
******************************************************************/
#ifndef APPMENUMODEL_H
#define APPMENUMODEL_H
#include <KWindowSystem>
#include <QAbstractListModel>
#include <QPointer>
#include <QRect>
#include <QStringList>
class QMenu;
class QModelIndex;
class QDBusServiceWatcher;
class CDBusMenuImporter;
class AppMenuModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool menuAvailable READ menuAvailable WRITE setMenuAvailable NOTIFY menuAvailableChanged)
Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged)
Q_PROPERTY(QRect screenGeometry READ screenGeometry WRITE setScreenGeometry NOTIFY screenGeometryChanged)
public:
explicit AppMenuModel(QObject *parent = nullptr);
~AppMenuModel() override;
enum AppMenuRole {
MenuRole = Qt::UserRole + 1, // TODO this should be Qt::DisplayRole
ActionRole,
};
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
void updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath);
bool menuAvailable() const;
void setMenuAvailable(bool set);
bool visible() const;
QRect screenGeometry() const;
void setScreenGeometry(QRect geometry);
Q_SIGNALS:
void requestActivateIndex(int index);
private Q_SLOTS:
void onActiveWindowChanged();
void setVisible(bool visible);
void update();
Q_SIGNALS:
void menuAvailableChanged();
void modelNeedsUpdate();
void screenGeometryChanged();
void visibleChanged();
private:
bool m_menuAvailable;
bool m_updatePending = false;
bool m_visible = true;
//! current active window used
WId m_currentWindowId = 0;
//! window that its menu initialization may be delayed
WId m_delayedMenuWindowId = 0;
QPointer<QMenu> m_menu;
QDBusServiceWatcher *m_serviceWatcher;
QString m_serviceName;
QString m_menuObjectPath;
QPointer<CDBusMenuImporter> m_importer;
};
#endif

@ -0,0 +1,56 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
<dox:d><![CDATA[
@mainpage
An interface to register menus that are associated with a window in an application. The
main interface is docuemented here: @ref com::canonical::AppMenu::Registrar.
The actual menus are transported using the dbusmenu protocol which is available
here: @ref com::canonical::dbusmenu.
]]></dox:d>
<interface name="com.canonical.AppMenu.Registrar" xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
<dox:d>
An interface to register a menu from an application's window to be displayed in another
window. This manages that association between XWindow Window IDs and the dbus
address and object that provides the menu using the dbusmenu dbus interface.
</dox:d>
<method name="RegisterWindow">
<dox:d><![CDATA[
Associates a dbusmenu with a window
/note this method assumes that the connection from the caller is the DBus connection
to use for the object. Applications that use multiple DBus connections will need to
ensure this method is called with the same connection that implmenets the object.
]]></dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window</dox:d>
</arg>
<arg name="menuObjectPath" type="o" direction="in">
<dox:d>The object on the dbus interface implementing the dbusmenu interface</dox:d>
</arg>
</method>
<method name="UnregisterWindow">
<dox:d>
A method to allow removing a window from the database. Windows will also be removed
when the client drops off DBus so this is not required. It is polite though. And
important for testing.
</dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window</dox:d>
</arg>
</method>
<method name="GetMenuForWindow">
<dox:d>Gets the registered menu for a given window ID.</dox:d>
<arg name="windowId" type="u" direction="in">
<dox:d>The XWindow ID of the window to get</dox:d>
</arg>
<arg name="service" type="s" direction="out">
<dox:d>The address of the connection on DBus (e.g. :1.23 or org.example.service)</dox:d>
</arg>
<arg name="menuObjectPath" type="o" direction="out">
<dox:d>The path to the object which implements the com.canonical.dbusmenu interface.</dox:d>
</arg>
</method>
</interface>
</node>

@ -0,0 +1,52 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#ifndef KDBUSIMPORTER_H
#define KDBUSIMPORTER_H
#include "../libdbusmenuqt/dbusmenuimporter.h"
#include "verticalmenu.h"
#include <QIcon>
class KDBusMenuImporter : public DBusMenuImporter
{
public:
KDBusMenuImporter(const QString &service, const QString &path, QObject *parent)
: DBusMenuImporter(service, path, parent)
{
}
protected:
QIcon iconForName(const QString &name) override
{
return QIcon::fromTheme(name);
}
QMenu *createMenu(QWidget *parent) override
{
return new VerticalMenu(parent);
}
};
#endif // KDBUSIMPORTER_H

@ -0,0 +1,115 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Copyright (c) 2016 Kai Uwe Broulik <kde@privat.broulik.de>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "menuimporter.h"
#include "../libdbusmenuqt/dbusmenutypes_p.h"
#include "menuimporteradaptor.h"
#include <QDBusMessage>
#include <QDBusServiceWatcher>
#include <KWindowInfo>
#include <KWindowSystem>
static const char *DBUS_SERVICE = "com.canonical.AppMenu.Registrar";
static const char *DBUS_OBJECT_PATH = "/com/canonical/AppMenu/Registrar";
MenuImporter::MenuImporter(QObject *parent)
: QObject(parent)
, m_serviceWatcher(new QDBusServiceWatcher(this))
{
qDBusRegisterMetaType<DBusMenuLayoutItem>();
m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MenuImporter::slotServiceUnregistered);
}
MenuImporter::~MenuImporter()
{
QDBusConnection::sessionBus().unregisterService(DBUS_SERVICE);
}
bool MenuImporter::connectToBus()
{
if (!QDBusConnection::sessionBus().registerService(DBUS_SERVICE)) {
return false;
}
new MenuImporterAdaptor(this);
QDBusConnection::sessionBus().registerObject(DBUS_OBJECT_PATH, this);
return true;
}
void MenuImporter::RegisterWindow(WId id, const QDBusObjectPath &path)
{
KWindowInfo info(id, NET::WMWindowType, NET::WM2WindowClass);
NET::WindowTypes mask = NET::AllTypesMask;
auto type = info.windowType(mask);
// Menu can try to register, right click in gimp for example
if (type != NET::Unknown && (type & (NET::Menu | NET::DropdownMenu | NET::PopupMenu))) {
return;
}
if (path.path().isEmpty()) // prevent bad dbusmenu usage
return;
QString service = message().service();
QString classClass = info.windowClassClass();
m_windowClasses.insert(id, classClass);
m_menuServices.insert(id, service);
m_menuPaths.insert(id, path);
if (!m_serviceWatcher->watchedServices().contains(service)) {
m_serviceWatcher->addWatchedService(service);
}
emit WindowRegistered(id, service, path);
}
void MenuImporter::UnregisterWindow(WId id)
{
m_menuServices.remove(id);
m_menuPaths.remove(id);
m_windowClasses.remove(id);
emit WindowUnregistered(id);
}
QString MenuImporter::GetMenuForWindow(WId id, QDBusObjectPath &path)
{
path = m_menuPaths.value(id);
return m_menuServices.value(id);
}
void MenuImporter::slotServiceUnregistered(const QString &service)
{
WId id = m_menuServices.key(service);
m_menuServices.remove(id);
m_menuPaths.remove(id);
m_windowClasses.remove(id);
emit WindowUnregistered(id);
m_serviceWatcher->removeWatchedService(service);
}

@ -0,0 +1,90 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Copyright (c) 2016 Kai Uwe Broulik <kde@privat.broulik.de>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#ifndef MENUIMPORTER_H
#define MENUIMPORTER_H
// Qt
#include <QDBusArgument>
#include <QDBusContext>
#include <QDBusObjectPath>
#include <QObject>
#include <QWidget> // For WId
class QDBusObjectPath;
class QDBusServiceWatcher;
class MenuImporter : public QObject, protected QDBusContext
{
Q_OBJECT
public:
explicit MenuImporter(QObject *);
~MenuImporter() override;
bool connectToBus();
bool serviceExist(WId id)
{
return m_menuServices.contains(id);
}
QString serviceForWindow(WId id)
{
return m_menuServices.value(id);
}
bool pathExist(WId id)
{
return m_menuPaths.contains(id);
}
QString pathForWindow(WId id)
{
return m_menuPaths.value(id).path();
}
QList<WId> ids()
{
return m_menuServices.keys();
}
Q_SIGNALS:
void WindowRegistered(WId id, const QString &service, const QDBusObjectPath &);
void WindowUnregistered(WId id);
public Q_SLOTS:
Q_NOREPLY void RegisterWindow(WId id, const QDBusObjectPath &path);
Q_NOREPLY void UnregisterWindow(WId id);
QString GetMenuForWindow(WId id, QDBusObjectPath &path);
private Q_SLOTS:
void slotServiceUnregistered(const QString &service);
private:
QDBusServiceWatcher *m_serviceWatcher;
QHash<WId, QString> m_menuServices;
QHash<WId, QDBusObjectPath> m_menuPaths;
QHash<WId, QString> m_windowClasses;
};
#endif /* MENUIMPORTER_H */

@ -0,0 +1,28 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.cappmenu">
<method name="showMenu">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
<arg name="service" type="s" direction="in"/>
<arg name="objectPath" type="o" direction="in"/>
<arg name="actionId" type="i" direction="in"/>
</method>
<method name="reconfigure">
</method>
<signal name="reconfigured" />
<signal name="showRequest">
<arg name="service" type="s" direction="out"/>
<arg name="objectPath" type="o" direction="out"/>
<arg name="actionId" type="i" direction="out"/>
</signal>
<signal name="menuShown">
<arg name="service" type="s" direction="out"/>
<arg name="objectPath" type="o" direction="out"/>
</signal>
<signal name="menuHidden">
<arg name="service" type="s" direction="out"/>
<arg name="objectPath" type="o" direction="out"/>
</signal>
</interface>
</node>

@ -0,0 +1,75 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "verticalmenu.h"
#include <QCoreApplication>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
VerticalMenu::VerticalMenu(QWidget *parent)
: QMenu(parent)
{
}
VerticalMenu::~VerticalMenu()
{
}
QMenu *VerticalMenu::leafMenu()
{
QMenu *leaf = this;
while (true) {
QAction *act = leaf->activeAction();
if (act && act->menu() && act->menu()->isVisible()) {
leaf = act->menu();
continue;
}
return leaf == this ? nullptr : leaf;
}
return nullptr; // make gcc happy
}
void VerticalMenu::paintEvent(QPaintEvent *pe)
{
QMenu::paintEvent(pe);
if (QWidget::mouseGrabber() == this)
return;
if (QWidget::mouseGrabber())
QWidget::mouseGrabber()->releaseMouse();
grabMouse();
grabKeyboard();
}
#define FORWARD(_EVENT_, _TYPE_) \
void VerticalMenu::_EVENT_##Event(Q##_TYPE_##Event *e) \
{ \
if (QMenu *leaf = leafMenu()) \
QCoreApplication::sendEvent(leaf, e); \
else \
QMenu::_EVENT_##Event(e); \
}
FORWARD(keyPress, Key)
FORWARD(keyRelease, Key)

@ -0,0 +1,65 @@
/*
Copyright (c) 2011 Lionel Chauvin <megabigbug@yahoo.fr>
Copyright (c) 2011,2012 Cédric Bellegarde <gnumdk@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#ifndef VERTICALMENU_H
#define VERTICALMENU_H
#include <QDBusObjectPath>
#include <QMenu>
class VerticalMenu : public QMenu
{
Q_OBJECT
public:
explicit VerticalMenu(QWidget *parent = nullptr);
~VerticalMenu() override;
QString serviceName() const
{
return m_serviceName;
}
void setServiceName(const QString &serviceName)
{
m_serviceName = serviceName;
}
QDBusObjectPath menuObjectPath() const
{
return m_menuObjectPath;
}
void setMenuObjectPath(const QDBusObjectPath &menuObjectPath)
{
m_menuObjectPath = menuObjectPath;
}
protected:
void keyPressEvent(QKeyEvent *) override;
void keyReleaseEvent(QKeyEvent *) override;
void paintEvent(QPaintEvent *) override;
private:
QMenu *leafMenu();
QString m_serviceName;
QDBusObjectPath m_menuObjectPath;
};
#endif // VERTICALMENU_H

@ -90,7 +90,10 @@ CAppItem *CApplications::matchItem(quint32 pid)
if (item->fullExec == command || if (item->fullExec == command ||
item->exec == command || item->exec == command ||
item->fullExec == commandName || item->fullExec == commandName ||
item->exec == commandName) { item->exec == commandName ||
// FileName
item->fileName == command ||
item->fileName == commandName) {
return item; return item;
} }
} }
@ -172,6 +175,7 @@ void CApplications::addApplication(const QString &filePath)
item->icon = desktop.value("Icon").toString(); item->icon = desktop.value("Icon").toString();
item->fullExec = desktop.value("Exec").toString(); item->fullExec = desktop.value("Exec").toString();
item->exec = simplifiedExec; item->exec = simplifiedExec;
item->fileName = QFileInfo(filePath).baseName();
m_items.append(item); m_items.append(item);
} }

@ -33,6 +33,7 @@ public:
QString icon; QString icon;
QString fullExec; QString fullExec;
QString exec; QString exec;
QString fileName;
}; };
class CApplications : public QObject class CApplications : public QObject

@ -24,6 +24,8 @@
#include "statusbar.h" #include "statusbar.h"
#include "controlcenterdialog.h" #include "controlcenterdialog.h"
#include "systemtray/systemtraymodel.h" #include "systemtray/systemtraymodel.h"
#include "appmenu/appmenumodel.h"
#include "appmenu/appmenuapplet.h"
#include "appearance.h" #include "appearance.h"
#include "brightness.h" #include "brightness.h"
@ -52,6 +54,8 @@ int main(int argc, char *argv[])
qmlRegisterType<Brightness>(uri, 1, 0, "Brightness"); qmlRegisterType<Brightness>(uri, 1, 0, "Brightness");
qmlRegisterType<Battery>(uri, 1, 0, "Battery"); qmlRegisterType<Battery>(uri, 1, 0, "Battery");
qmlRegisterType<VolumeManager>(uri, 1, 0, "Volume"); qmlRegisterType<VolumeManager>(uri, 1, 0, "Volume");
qmlRegisterType<AppMenuModel>(uri, 1, 0, "AppMenuModel");
qmlRegisterType<AppMenuApplet>(uri, 1, 0, "AppMenuApplet");
StatusBar bar; StatusBar bar;

@ -19,6 +19,7 @@
#include "statusbar.h" #include "statusbar.h"
#include "processprovider.h" #include "processprovider.h"
#include "appmenu/appmenu.h"
#include <QQmlEngine> #include <QQmlEngine>
#include <QQmlContext> #include <QQmlContext>
@ -41,6 +42,8 @@ StatusBar::StatusBar(QQuickView *parent)
KWindowSystem::setType(winId(), NET::Dock); KWindowSystem::setType(winId(), NET::Dock);
KWindowEffects::slideWindow(winId(), KWindowEffects::TopEdge); KWindowEffects::slideWindow(winId(), KWindowEffects::TopEdge);
new AppMenu(this);
engine()->rootContext()->setContextProperty("acticity", m_acticity); engine()->rootContext()->setContextProperty("acticity", m_acticity);
engine()->rootContext()->setContextProperty("process", new ProcessProvider); engine()->rootContext()->setContextProperty("process", new ProcessProvider);

@ -28,7 +28,7 @@
#include <QDebug> #include <QDebug>
#include <netinet/in.h> #include <netinet/in.h>
class MenuImporter : public DBusMenuImporter class TrayMenuImporter : public DBusMenuImporter
{ {
public: public:
using DBusMenuImporter::DBusMenuImporter; using DBusMenuImporter::DBusMenuImporter;
@ -157,10 +157,10 @@ void StatusNotifierItemSource::contextMenu(int x, int y)
{ {
if (m_menuImporter) { if (m_menuImporter) {
// Popup menu // Popup menu
m_menuImporter->updateMenu(); if (m_menuImporter->menu()) {
m_menuImporter->updateMenu();
if (m_menuImporter->menu())
m_menuImporter->menu()->popup(QPoint(x, y)); m_menuImporter->menu()->popup(QPoint(x, y));
}
} else { } else {
qWarning() << "Could not find DBusMenu interface, falling back to calling ContextMenu()"; qWarning() << "Could not find DBusMenu interface, falling back to calling ContextMenu()";
if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
@ -267,9 +267,9 @@ void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call)
// KStatusNotifierItem::setContextMenu(). // KStatusNotifierItem::setContextMenu().
qWarning() << "DBusMenu disabled for this application"; qWarning() << "DBusMenu disabled for this application";
} else { } else {
m_menuImporter = new MenuImporter(m_statusNotifierItemInterface->service(), m_menuImporter = new TrayMenuImporter(m_statusNotifierItemInterface->service(),
menuObjectPath, this); menuObjectPath, this);
connect(m_menuImporter, &MenuImporter::menuUpdated, this, [this](QMenu *menu) { connect(m_menuImporter, &TrayMenuImporter::menuUpdated, this, [this](QMenu *menu) {
if (menu == m_menuImporter->menu()) { if (menu == m_menuImporter->menu()) {
contextMenuReady(); contextMenuReady();
} }

@ -90,7 +90,7 @@ QVariant SystemTrayModel::data(const QModelIndex &index, int role) const
int SystemTrayModel::indexOf(const QString &id) int SystemTrayModel::indexOf(const QString &id)
{ {
for (StatusNotifierItemSource *item : m_items) { for (StatusNotifierItemSource *item : qAsConst(m_items)) {
if (item->id() == id) if (item->id() == id)
return m_items.indexOf(item); return m_items.indexOf(item);
} }

Loading…
Cancel
Save