diff options
author | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-25 12:13:31 +0200 |
---|---|---|
committer | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-25 12:13:31 +0200 |
commit | 1dbc0e3c88ba271ba35bc3f82e7864c4f35e1236 (patch) | |
tree | 8c491cd196e2eff4c59f8c23f566f7ff26981586 |
Initial Commit
41 files changed, 3177 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f78ff11 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,127 @@ +cmake_minimum_required(VERSION 3.14) + +project(KzXdgUtils VERSION 1.0.0 LANGUAGES CXX) + +include(GNUInstallDirs) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + + +if(NOT DEFINED QML_MODULE_INSTALL_PATH) + if(DEFINED OE_QMAKE_PATH_QML) + set(QML_MODULE_INSTALL_PATH ${OE_QMAKE_PATH_QML} ) + else() + set(QML_MODULE_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/qt6/qml ) + endif() +endif() + +find_package(Qt6 COMPONENTS Core Quick Qml DBus REQUIRED) + +set(PLUGIN_SOURCES + src/kzxdgutils_plugin.h src/kzxdgutils_plugin.cpp + qml/qmldir +) + +include_directories(src/lib) +set(LIBRARY_SOURCES + src/lib/debug.cpp + src/lib/xdgbasedir.cpp + src/lib/xdgentry.cpp + src/lib/xdgentries.cpp + src/lib/xdgdesktopentries.cpp + src/lib/xdgdesktopfilter.cpp + src/lib/xdgautostart.cpp + src/lib/xdgnotificationserver.cpp + src/lib/xdgnotificationmessage.cpp + src/lib/xdgnotificationmanager.cpp + src/lib/xdgnotificationactions.cpp + src/lib/xdgnotifier.cpp + src/lib/xdgstatusnotifieritem.cpp + src/lib/xdgstatusnotifierhost.cpp + src/lib/xdgstatusnotifierwatcher.cpp +) + +set(LIBRARY_HEADERS_PRIVATE + src/lib/debug.h +) + +set(LIBRARY_HEADERS + src/lib/xdgbasedir.h + src/lib/xdgentry.h + src/lib/xdgentries.h + src/lib/xdgdesktopentries.h + src/lib/xdgdesktopfilter.h + src/lib/xdgautostart.h + src/lib/xdgnotificationserver.h + src/lib/xdgnotificationmessage.h + src/lib/xdgnotificationmanager.h + src/lib/xdgnotificationactions.h + src/lib/xdgnotifier.h + src/lib/xdgstatusnotifieritem.h + src/lib/xdgstatusnotifierhost.h + src/lib/xdgstatusnotifierwatcher.h +) + + +list(APPEND DBUS_IFACE_SRCS api/org.freedesktop.Notifications.xml) +qt_add_dbus_adaptor( DBUS_IFACE_SRCS api/org.freedesktop.Notifications.xml xdgnotificationserver.h XdgNotificationServer) +qt_add_dbus_interface( DBUS_IFACE_SRCS api/org.freedesktop.Notifications.xml NotificationsIface) + +list(APPEND DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierItem.xml) +qt_add_dbus_adaptor( DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierItem.xml xdgstatusnotifieritem.h xdg::StatusNotifierItem) +qt_add_dbus_interface( DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierItem.xml XdgStatusNotifierItemIface) + +list(APPEND DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierWatcher.xml) +qt_add_dbus_adaptor( DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierWatcher.xml xdgstatusnotifierwatcher.h xdg::StatusNotifierWatcher) +qt_add_dbus_interface( DBUS_IFACE_SRCS api/org.freedesktop.StatusNotifierWatcher.xml XdgStatusNotifierWatcherIface) + +add_library(KzXdgUtilsPlugin SHARED ${PLUGIN_SOURCES}) +add_library(kzxdgutils SHARED ${LIBRARY_SOURCES} ${LIBRARY_HEADERS} ${LIBRARY_HEADERS_PRIVATE} ${DBUS_IFACE_SRCS} cmake/Config.cmake.in) +set_target_properties(kzxdgutils PROPERTIES PUBLIC_HEADER "${LIBRARY_HEADERS}") +set_target_properties(kzxdgutils PROPERTIES VERSION ${CMAKE_PROJECT_VERSION} SOVERSION 1) + +target_compile_definitions(KzXdgUtilsPlugin + PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG> +) + +target_link_libraries(KzXdgUtilsPlugin + PRIVATE + Qt6::Qml + kzxdgutils +) + +target_link_libraries(kzxdgutils + PRIVATE + Qt6::Core + Qt6::Quick + Qt6::DBus +) + +add_executable(test_xdg src/test/test_xdg.cpp) +target_link_libraries(test_xdg PRIVATE kzxdgutils Qt6::Core) + +install(TARGETS KzXdgUtilsPlugin DESTINATION ${QML_MODULE_INSTALL_PATH}/KaZoe/XdgUtils) +install(FILES qml/qmldir DESTINATION ${QML_MODULE_INSTALL_PATH}/KaZoe/XdgUtils) +install(TARGETS kzxdgutils PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/kzxdgutils") +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/notificationsadaptor.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/kzxdgutils" RENAME xdgnotificationsadaptor.h) + +# CMake Module +include(CMakePackageConfigHelpers) +set(CMakeModuleName "KzXdgUtils") +configure_package_config_file( + cmake/Config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/${CMakeModuleName}Config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMakeModuleName} +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/${CMakeModuleName}ConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion ) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMakeModuleName}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${CMakeModuleName}ConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMakeModuleName} +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd27666 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# XDG Free Desktop Qt implementation + +This module is used to help the creation of project supporting Freedesktop Specifications + +[Freedesktop Specifications can be find her](https://specifications.freedesktop.org/) + +FreeDesktop (or XDG) is an extention of GNU and POSIX specifications. + +This is why some "path" like library dirs (/lib:/usr/lib) is not managed by XDG, but by POSIX + +The Specification hierarchy is: + +POSIX > GNU > XDG + +When POSIX or GNU concern the whole system, XDG is more restrictive for the "Desktop" (the Gui used by the USER) + +KzXdgUtils provide a library using Qt Core only, and a Qt Module to use it directly in QML. + +# Build and deploy + +```sh +git clone https://kazoe.org/cgit/kazoe/kazoe-xdgutils.git +cd xdgutils +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=/usr +make +sudo make install +``` + + +# XdgUtils Specification and API + +## basedir-spec + +[basedir-spec](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) is implemented by the library. + +The order of base directories denotes their importance; the first directory listed is the most important. When the same information is defined in multiple places the information defined relative to the more important base directory takes precedent. The base directory defined by $XDG_DATA_HOME is considered more important than any of the base directories defined by $XDG_DATA_DIRS. The base directory defined by $XDG_CONFIG_HOME is considered more important than any of the base directories defined by $XDG_CONFIG_DIRS. + +This function are available by including xdgbasedir.h + + +```c +#include <xdgbasedir.h> +``` + +All XDG base directory can be set by environment variables. + +$XDG_DATA_HOME want to say, XDG_DATA_HOME environement variable. + +### QString xdg::dataHome() +XDG_DATA_HOME defines the base directory relative to which user-specific data files should be stored. +Return $XDG_DATA_HOME content or $HOME/.local/share if not set + +### QString xdg::configHome() +XDG_CONFIG_HOME defines the base directory relative to which user-specific configuration files should be stored. +Return $XDG_CONFIG_HOME content or $HOME/.config if not set + +### QString xdg::stateHome() +XDG_STATE_HOME defines the base directory relative to which user-specific state files should be stored. +If $XDG_STATE_HOME is either not set or empty, a default equal to $HOME/.local/state should be used. + +The $XDG_STATE_HOME contains state data that should persist between (application) restarts, but that is not important or portable enough to the user that it should be stored in $XDG_DATA_HOME. It may contain: + +- actions history (logs, history, recently used files, ...) +- current state of the application that can be reused on a restart (view, layout, open files, undo history, …) + +### QString xdg::dataDirs() +$XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data files in addition to the $XDG_DATA_HOME base directory. +The directories in $XDG_DATA_DIRS should be seperated with a colon ':'. + +If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. + +### QString xdg::configDirs() +XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for configuration files in addition to the $XDG_CONFIG_HOME base directory. The directories in $XDG_CONFIG_DIRS should be seperated with a colon ':'. + +If $XDG_CONFIG_DIRS is either not set or empty, a value equal to /etc/xdg should be used. + +### QString xdg::cacheHome() +XDG_CACHE_HOME defines the base directory relative to which user-specific non-essential data files should be stored. If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache should be used. + +### QString xdg::runtimeDir() +XDG_RUNTIME_DIR defines the base directory relative to which user-specific non-essential runtime files and other file objects (such as sockets, named pipes, ...) should be stored. The directory MUST be owned by the user, and he MUST be the only one having read and write access to it. Its Unix access mode MUST be 0700. + +The lifetime of the directory MUST be bound to the user being logged in. It MUST be created when the user first logs in and if the user fully logs out the directory MUST be removed. If the user logs in more than once he should get pointed to the same directory, and it is mandatory that the directory continues to exist from his first login to his last logout on the system, and not removed in between. Files in the directory MUST not survive reboot or a full logout/login cycle. + +The directory MUST be on a local file system and not shared with any other system. The directory MUST by fully-featured by the standards of the operating system. More specifically, on Unix-like operating systems AF_UNIX sockets, symbolic links, hard links, proper permissions, file locking, sparse files, memory mapping, file change notifications, a reliable hard link count must be supported, and no restrictions on the file name character set should be imposed. Files in this directory MAY be subjected to periodic clean-up. To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time or the 'sticky' bit should be set on the file. + +If $XDG_RUNTIME_DIR is not set applications should fall back to a replacement directory with similar capabilities and print a warning message. Applications should use this directory for communication and synchronization purposes and should not place larger files in it, since it might reside in runtime memory and cannot necessarily be swapped out to disk. + + +# Limitation + +Because of QSettings limitation, categories filed support only 1 category. This is because in INI format ";" is the begin of a comment +So, value of "Categories=Qt;KDE;Settings;" will return only "Qt" + + + + +# XdgNotificationServer + +To use XdgNotificationServer, you have to: +- Inherit your server class from XdgNotificationServer (#include <xdgnotificationserver.h>) ex: MyNotificationServer +- Instanciate your server class ex: MyNotificationServer myserver. +- create the adaptor ex: new NotificationsAdaptor(&myserver) +- Register to dbus session: XdgNotificationServer::RegisterServerToDbus(myserver) + + +# Debuging + +Debug traces in this library use QLoggingCategory. Many way exists to display additional debug traces (see https://doc.qt.io/qt-6/qloggingcategory.html ) +For example, you can create /etc/xdg/QtProject/qtlogging.ini + diff --git a/api/org.freedesktop.Notifications.xml b/api/org.freedesktop.Notifications.xml new file mode 100644 index 0000000..62345f2 --- /dev/null +++ b/api/org.freedesktop.Notifications.xml @@ -0,0 +1,37 @@ +<!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.freedesktop.Notifications"> + <signal name="NotificationClosed"> + <arg name="id" type="u" direction="out"/> + <arg name="reason" type="u" direction="out"/> + </signal> + <signal name="ActionInvoked"> + <arg name="id" type="u" direction="out"/> + <arg name="action_key" type="s" direction="out"/> + </signal> + <method name="Notify"> + <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/> + <arg type="u" direction="out"/> + <arg name="app_name" type="s" direction="in"/> + <arg name="replaces_id" type="u" direction="in"/> + <arg name="app_icon" type="s" direction="in"/> + <arg name="summary" type="s" direction="in"/> + <arg name="body" type="s" direction="in"/> + <arg name="actions" type="as" direction="in"/> + <arg name="hints" type="a{sv}" direction="in"/> + <arg name="timeout" type="i" direction="in"/> + </method> + <method name="CloseNotification"> + <arg name="id" type="u" direction="in"/> + </method> + <method name="GetCapabilities"> + <arg type="as" name="caps" direction="out"/> + </method> + <method name="GetServerInformation"> + <arg type="s" name="name" direction="out"/> + <arg type="s" name="vendor" direction="out"/> + <arg type="s" name="version" direction="out"/> + <arg type="s" name="spec_version" direction="out"/> + </method> + </interface> +</node> diff --git a/api/org.freedesktop.StatusNotifierItem.xml b/api/org.freedesktop.StatusNotifierItem.xml new file mode 100644 index 0000000..c425af6 --- /dev/null +++ b/api/org.freedesktop.StatusNotifierItem.xml @@ -0,0 +1,79 @@ +<!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.StatusNotifierItem"> + <!-- Based on https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ --> + + <property name="Category" type="s" access="read"/> + <property name="Id" type="s" access="read"/> + <property name="Title" type="s" access="read"/> + <property name="Status" type="s" access="read"/> + <property name="WindowId" type="i" access="read"/> + <property name="IconName" type="s" access="read" /> + + <!-- TODO: API not supported yet + <property name="IconPixmap" type="(iiay)" access="read"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="XdgDbusImageVector"/> + </property> + --> + + <property name="OverlayIconName" type="s" access="read"/> + + <!-- TODO: API not supported yet + <property name="OverlayIconPixmap" type="(iiay)" access="read"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="XdgDbusImageVector"/> + </property> + --> + + <property name="AttentionIconName" type="s" access="read"/> + + <!-- TODO: API not supported yet + <property name="AttentionIconPixmap" type="(iiay)" access="read"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="XdgDbusImageVector"/> + </property> + --> + + <property name="AttentionMovieName" type="s" access="read"/> + + + <!-- TODO: API not supported yet + org.freedesktop.StatusNotifierItem.ToolTip + org.freedesktop.StatusNotifierItem.ItemIsMenu + org.freedesktop.StatusNotifierItem.Menu + --> + + <!-- TODO: API not supported yet + <method name="ContextMenu"> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + --> + + <method name="Activate"> <!-- click --> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + + <method name="SecondaryActivate"> <!-- long press --> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + + <!-- TODO: API not supported yet + <method name="Scroll"> + <arg name="delta" type="i" direction="in"/> + <arg name="orientation" type="s" direction="in"/> + </method> + --> + + <signal name="NewTitle" /> + <signal name="NewIcon" /> + <signal name="NewAttentionIcon" /> + <signal name="NewOverlayIcon" /> + <signal name="NewToolTip" /> + <signal name="NewStatus"> + <arg name="status" type="s"/> + </signal> + + </interface> +</node> diff --git a/api/org.freedesktop.StatusNotifierWatcher.xml b/api/org.freedesktop.StatusNotifierWatcher.xml new file mode 100644 index 0000000..855af94 --- /dev/null +++ b/api/org.freedesktop.StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ +<!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.StatusNotifierWatcher"> + + <!-- methods --> + <method name="RegisterStatusNotifierItem"> + <arg name="service" type="s" direction="in"/> + </method> + + <method name="RegisterStatusNotifierHost"> + <arg name="service" type="s" direction="in"/> + </method> + + + <!-- properties --> + + <property name="RegisteredStatusNotifierItems" type="as" access="read"> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/> + </property> + + <property name="IsStatusNotifierHostRegistered" type="b" access="read"/> + + <property name="ProtocolVersion" type="i" access="read"/> + + + <!-- signals --> + + <signal name="StatusNotifierItemRegistered"> + <arg type="s"/> + </signal> + + <signal name="StatusNotifierItemUnregistered"> + <arg type="s"/> + </signal> + + <signal name="StatusNotifierHostRegistered"> + </signal> + + <signal name="StatusNotifierHostUnregistered"> + </signal> + </interface> +</node> diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in new file mode 100644 index 0000000..74bab40 --- /dev/null +++ b/cmake/Config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +set(KZXDGUTILS_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@/kzxdgutils/") +set(KZXDGUTILS_LIBRARIES "kzxdgutils") +include_directories(${ZXDGUTILS_INCLUDE_DIR}) diff --git a/qml/qmldir b/qml/qmldir new file mode 100644 index 0000000..73d2b63 --- /dev/null +++ b/qml/qmldir @@ -0,0 +1,2 @@ +module kzxdgutils +plugin KzXdgUtilsPlugin diff --git a/src/kzxdgutils_plugin.cpp b/src/kzxdgutils_plugin.cpp new file mode 100644 index 0000000..c68633c --- /dev/null +++ b/src/kzxdgutils_plugin.cpp @@ -0,0 +1,27 @@ +#include "kzxdgutils_plugin.h" + +#include "xdgentry.h" +#include "xdgdesktopentries.h" +#include "xdgdesktopfilter.h" +#include "xdgautostart.h" +#include "xdgnotificationmanager.h" +#include "xdgnotificationactions.h" +#include "xdgstatusnotifieritem.h" +#include "xdgstatusnotifierhost.h" +#include "xdgstatusnotifierwatcher.h" + +#include <qqml.h> + +void KzXdgUtilsPlugin::registerTypes(const char *uri) +{ + // @uri xdgutils + qmlRegisterType<xdg::DesktopEntries>(uri, 1, 0, "DesktopEntries"); + qmlRegisterType<xdg::Entry>(uri, 1, 0, "XdgEntry"); + qmlRegisterType<xdg::DesktopFilter>(uri, 1, 0, "DesktopFilter"); + qmlRegisterType<xdg::AutoStart>(uri, 1, 0, "AutoStart"); + qmlRegisterType<xdg::NotificationManager>(uri, 1, 0, "NotificationManager"); + qmlRegisterType<xdg::NotificationActions>(uri, 1, 0, "NotificationActions"); + qmlRegisterType<xdg::StatusNotifierItem>(uri, 1, 0, "StatusNotifierItem"); + qmlRegisterType<xdg::StatusNotifierHost>(uri, 1, 0, "StatusNotifierHost"); + qmlRegisterType<xdg::StatusNotifierWatcher>(uri, 1, 0, "StatusNotifierWatcher"); +} diff --git a/src/kzxdgutils_plugin.h b/src/kzxdgutils_plugin.h new file mode 100644 index 0000000..ea44c7d --- /dev/null +++ b/src/kzxdgutils_plugin.h @@ -0,0 +1,15 @@ +#ifndef KZXDGUTILS_PLUGIN_H +#define KZXDGUTILS_PLUGIN_H + +#include <QQmlExtensionPlugin> + +class KzXdgUtilsPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char *uri) override; +}; + +#endif // KZXDGUTILS_PLUGIN_H diff --git a/src/lib/debug.cpp b/src/lib/debug.cpp new file mode 100644 index 0000000..565bdbc --- /dev/null +++ b/src/lib/debug.cpp @@ -0,0 +1,4 @@ +#include "debug.h" + +Q_LOGGING_CATEGORY(AUTO_START, "xdg.auto_start") +Q_LOGGING_CATEGORY(SYSTEM_TRAY, "xdg.system_tray") diff --git a/src/lib/debug.h b/src/lib/debug.h new file mode 100644 index 0000000..978b470 --- /dev/null +++ b/src/lib/debug.h @@ -0,0 +1,9 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(AUTO_START) +Q_DECLARE_LOGGING_CATEGORY(SYSTEM_TRAY) + +#endif // DEBUG_H diff --git a/src/lib/xdgautostart.cpp b/src/lib/xdgautostart.cpp new file mode 100644 index 0000000..bd71261 --- /dev/null +++ b/src/lib/xdgautostart.cpp @@ -0,0 +1,38 @@ +#include "xdgautostart.h" +#include "xdgbasedir.h" +#include "xdgentry.h" +#include "debug.h" + +xdg::AutoStart::AutoStart(QObject *parent) + : QObject{parent} +{ + qCDebug(AUTO_START) << "Create Auto start"; + QStringList configdirs = xdg::configDirs(); + for (auto dir = configdirs.rbegin(); dir != configdirs.rend(); ++dir) + { + m_entries.addDirectory(*dir + "/autostart"); + } + m_entries.addDirectory(xdg::configHome() + "/autostart"); +} + +void xdg::AutoStart::start(QString key, QString value) +{ + qCDebug(AUTO_START) << "Start " << key << value; + for(xdg::Entry *entry: m_entries.getEntries()) + { + if((!value.isNull()) && (!key.isNull())) + { + QString c = entry->data(key); + if(c.trimmed().toLower() == value.trimmed().toLower()) + { + qCDebug(AUTO_START) << "Start app " << entry->appId() << "with" << key << value; + entry->start(QStringList()); + } + } + else + { + qCDebug(AUTO_START) << "Start app " << entry->appId(); + entry->start(QStringList()); + } + } +} diff --git a/src/lib/xdgautostart.h b/src/lib/xdgautostart.h new file mode 100644 index 0000000..fc6c02d --- /dev/null +++ b/src/lib/xdgautostart.h @@ -0,0 +1,24 @@ +#ifndef XDGAUTOSTART_H +#define XDGAUTOSTART_H + +#include <QObject> +#include "xdgentries.h" + + +namespace xdg { + class AutoStart : public QObject + { + Q_OBJECT + public: + explicit AutoStart(QObject *parent = nullptr); + xdg::Entries m_entries; + + public slots: + void start(QString key = QString(), QString value = QString()); + + signals: + + }; +} + +#endif // XDGAUTOSTART_H diff --git a/src/lib/xdgbasedir.cpp b/src/lib/xdgbasedir.cpp new file mode 100644 index 0000000..1f84109 --- /dev/null +++ b/src/lib/xdgbasedir.cpp @@ -0,0 +1,79 @@ +#include "xdgbasedir.h" +#include <QStringList> + +static inline QString user() +{ + QString user = QString::fromUtf8(qgetenv("USER")); + if(user.isEmpty()) return "root"; + return user; +} + +static inline QString home() +{ + QString home = QString::fromUtf8(qgetenv("HOME")); + if(home.isEmpty()) + { + QString u = user(); + if(u == "root") + return "/root"; + else + return "/home/" + u; + } + return home; +} + +static inline QString envOrDefault(const char *name, const QString &def) +{ + QString env = QString::fromUtf8(qgetenv(name)); + if(env.isEmpty()) + { + return def; + } + return env; +} + +QString xdg::dataHome() +{ + return envOrDefault("XDG_DATA_HOME", home() + "/.local/share"); +} + +QString xdg::configHome() +{ + return envOrDefault("XDG_CONFIG_HOME", home() + "/.config"); +} + +QString xdg::cacheHome() +{ + return envOrDefault("XDG_CACHE_HOME", home() + "/.cache"); +} + +QString xdg::stateHome() +{ + return envOrDefault("XDG_STATE_HOME", home() + "/.local/state"); +} + + +QStringList xdg::dataDirs() +{ + QString dataDirs = QString::fromUtf8(qgetenv("XDG_DATA_DIRS")); + if(dataDirs.isEmpty()) + { + return QStringList() << "/usr/local/share" << "/usr/share"; + } + return dataDirs.split(":"); +} + +QStringList xdg::configDirs() +{ + QString configDirs = QString::fromUtf8(qgetenv("XDG_CONFIG_DIRS")); + if(configDirs.isEmpty()) + { + return QStringList() << "/etc/xdg"; + } + return configDirs.split(":"); +} + +QString xdg::runtimeDir() +{ + return envOrDefault("XDG_RUNTIME_DIR", QString("/run/user/%i").arg(user())); +} diff --git a/src/lib/xdgbasedir.h b/src/lib/xdgbasedir.h new file mode 100644 index 0000000..bd7d688 --- /dev/null +++ b/src/lib/xdgbasedir.h @@ -0,0 +1,97 @@ +/*! \file xdgbasedir.h + \brief Implementation of basedir-spec + \author Fabien Proriol + + Implementation of https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html +*/ + +#ifndef XDGBASEDIR_H +#define XDGBASEDIR_H + +#include <QString> + +//! XDG basedir implementation +namespace xdg { +/*! + * \addtogroup XDG + * @{ + */ + +/*! \fn QString dataHome() + \brief Return XDG_DATA_HOME + XDG_DATA_HOME defines the base directory relative to + which user-specific data files should be stored. + \return $XDG_DATA_HOME content or $HOME/.local/share if not set +*/ + QString dataHome(); + + +/*! \fn QString configHome() + \brief Return XDG_CONFIG_HOME + XDG_CONFIG_HOME defines the base directory relative to + which user-specific configuration files should be stored. + \return $XDG_CONFIG_HOME content or $HOME/.config if not set +*/ + QString configHome(); + + +/*! \fn QString stateHome() + \brief Return XDG_STATE_HOME + XDG_STATE_HOME defines the base directory relative to + which user-specific state files should be stored. + \return $XDG_STATE_HOME content or $HOME/.local/state if not set +*/ + QString stateHome(); + + +/*! \fn QStringList dataDirs() + \brief Return XDG_DATA_DIRS + XDG_DATA_DIRS defines the preference-ordered set of + base directories to search for data files in addition + to the $XDG_DATA_HOME base directory. + The directories in $XDG_DATA_DIRS should be seperated + with a colon ':'. + \return $XDG_DATA_DIRS content or /usr/local/share:/usr/share if not set +*/ + QStringList dataDirs(); + +/*! \fn QStringList dataDirs() + \brief Return XDG_CONFIG_DIRS + XDG_CONFIG_DIRS defines the preference-ordered set of + base directories to search for configuration files in addition + to the $XDG_CONFIG_HOME base directory. + The directories in $XDG_CONFIG_DIRS should be seperated + with a colon ':'. + \return $XDG_CONFIG_DIRS content or /etc/xdg if not set +*/ + QStringList configDirs(); + + +/*! \fn QString cacheHome() + \brief Return XDG_CONFIG_DIRS + XDG_CACHE_HOME defines the base directory relative to + which user-specific non-essential data files should be + stored. + If $XDG_CACHE_HOME is either not set or empty, a default + equal to $HOME/.cache should be used. + \return $XDG_CONFIG_DIRS content or $HOME/.cache if not set +*/ + QString cacheHome(); + + +/*! \fn QString runtimeDir() + \brief Return XDG_CONFIG_DIRS + XDG_RUNTIME_DIR defines the base directory relative to + which user-specific non-essential runtime files and other + file objects (such as sockets, named pipes, ...) should + be stored. The directory MUST be owned by the user, + and he MUST be the only one having read and write access + to it. Its Unix access mode MUST be 0700. + \return $XDG_RUNTIME_DIR content or emit a warning and return /run/user/$USER +*/ + QString runtimeDir(); + +/*! @} End of XDG*/ +} + +#endif // XDGBASEDIR_H diff --git a/src/lib/xdgdesktopentries.cpp b/src/lib/xdgdesktopentries.cpp new file mode 100644 index 0000000..d1a3f95 --- /dev/null +++ b/src/lib/xdgdesktopentries.cpp @@ -0,0 +1,202 @@ +#include "xdgdesktopentries.h" +#include "xdgentry.h" +#include "xdgbasedir.h" +#include <QDir> +#include <QMutexLocker> +#include <qdebug.h> +#include <signal.h> + + + +xdg::DesktopEntries::DesktopEntries(QObject *parent) + : QAbstractListModel(parent) +{ + QStringList datadirs = xdg::dataDirs(); + for (auto dir = datadirs.rbegin(); dir != datadirs.rend(); ++dir) + { + m_entries.addDirectory(*dir + "/applications"); + } + m_entries.addDirectory(xdg::dataHome() + "/applications"); + QObject::connect(&m_entries, &xdg::Entries::entryDataChanged, this, &xdg::DesktopEntries::_dataChanged); + QObject::connect(&m_entries, &xdg::Entries::raiseProcess, this, &xdg::DesktopEntries::raiseProcess); + QObject::connect(&m_entries, &xdg::Entries::startCreateEntry, [this](int i){ + beginInsertRows(QModelIndex(), i, i); + }); + QObject::connect(&m_entries, &xdg::Entries::startCreateEntry, [this](int i){ + endInsertRows(); + emit categoriesChanged(categories()); + }); + + QObject::connect(&m_entries, &xdg::Entries::startRemoveEntry, [this](int i){ + beginRemoveRows(QModelIndex(), i, i); + }); + QObject::connect(&m_entries, &xdg::Entries::startRemoveEntry, [this](int i){ + endRemoveRows(); + emit categoriesChanged(categories()); + }); +} + +int xdg::DesktopEntries::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_entries.count(); +} + +QVariant xdg::DesktopEntries::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_entries.count()) + return QVariant(); + + const xdg::Entry *entry = m_entries.getEntry(index.row()); + + switch(role) { + case Qt::DisplayRole: + case AppNameRole: + case NameRole: + return entry->data(XDG_NAME); + case ExecRole: + return entry->data(XDG_EXEC); + case IconRole: + return entry->data(XDG_ICON); + case CategoryRole: + return entry->data(XDG_CATEGORIES); + case NoDisplayRole: + return entry->data(XDG_NODISPLAY); + case KeywordsRole: + return entry->data(XDG_KEYWORDS); + case PathRole: + case DesktopFileRole: + return entry->path(); + case AppIdRole: + return entry->appId(); + case EntryRole: + { + QVariant ventry; + ventry.setValue(entry); + return ventry; + } + } + qWarning() << "Not managed role" << role; + return QVariant(); +} + +QHash<int, QByteArray> xdg::DesktopEntries::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[IconRole] = "appIcon"; + roles[NameRole] = "name"; + roles[AppNameRole] = "appName"; + roles[ExecRole] = "exec"; + roles[PathRole] = "path"; + roles[NoDisplayRole] = "noDisplay"; + roles[CategoryRole] = "category"; + roles[KeywordsRole] = "keywords"; + roles[DesktopFileRole] = "desktopFile"; + roles[AppIdRole] = "appId"; + roles[EntryRole] = "entry"; + return roles; +} + +QStringList xdg::DesktopEntries::categories() +{ + QStringList ret; + for(xdg::Entry *entry: m_entries.getEntries()) + { + QString c = entry->data(XDG_CATEGORIES); + if(!ret.contains(c)) + ret.append(c); + } + return ret; +} + +void xdg::DesktopEntries::start(QString path, const QStringList& args) +{ + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->path() == path) + entry->start(args); + } +} + +void xdg::DesktopEntries::stop(QString path, const QStringList& args) +{ + bool isPid; + int pid = path.toInt(&isPid, 10); + for(xdg::Entry *entry: m_entries.getEntries()) + { + if((isPid && entry->pid() == pid) || (entry->path() == path)) + { + entry->stop(args); + return; + } + } + if(isPid) kill(pid, SIGTERM); +} + +QString xdg::DesktopEntries::appIdData(QString appid, QString key) +{ + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->appId() == appid) + return entry->data(key); + } + return QString(); +} + +bool xdg::DesktopEntries::_contains(QString path) +{ + for(const xdg::Entry *entry: m_entries.getEntries()) + { + if(entry->path() == path) + return true; + } + return false; +} + +QStringList xdg::DesktopEntries::_getEntriesFrom(QString path) +{ + QStringList entries; + for(xdg::Entry *entry: m_entries.getEntries()) + { + if(path == "" || entry->path().startsWith(path)) + entries.append(entry->path()); + } + return entries; +} + +void xdg::DesktopEntries::_dataChanged(const QString appid, const QString item, const QString value) +{ + if(item == XDG_CATEGORIES) + { + emit categoriesChanged(categories()); + } + + int role = -1; + if (item == XDG_ICON) + role = IconRole; + else if (item == XDG_NAME) + role = NameRole; + else if(item == XDG_EXEC) + role = ExecRole; + else if (item == XDG_CATEGORIES) + role = CategoryRole; + else if (item == XDG_KEYWORDS) + role = KeywordsRole; + else if (item == XDG_NODISPLAY) + role = NoDisplayRole; + else + qWarning() << "XdgEntries::_dataChanged: Unknwon role " << item; + if(role > 0) + { + for(int i = 0; i < m_entries.count(); i++) + { + const xdg::Entry *entry = m_entries.getEntry(i); + if(entry->appId() == appid) + { + QVector<int> r; + r.append(role); + emit dataChanged(index(i), index(i), r); + } + } + } +} diff --git a/src/lib/xdgdesktopentries.h b/src/lib/xdgdesktopentries.h new file mode 100644 index 0000000..89aff08 --- /dev/null +++ b/src/lib/xdgdesktopentries.h @@ -0,0 +1,58 @@ +#ifndef XDGDESKTOPENTRIES_H +#define XDGDESKTOPENTRIES_H + +#include <QAbstractListModel> +#include <QObject> +#include "xdgentries.h" + +namespace xdg { + class DesktopEntries : public QAbstractListModel + { + Q_OBJECT + Q_PROPERTY(QStringList categories READ categories NOTIFY categoriesChanged) + xdg::Entries m_entries; + + private slots: + bool _contains(QString path); + //void _reload(QString path); + QStringList _getEntriesFrom(QString path = ""); + void _dataChanged(const QString appid, const QString item, const QString value); + + public: + explicit DesktopEntries(QObject *parent = nullptr); + + enum XdgDesktopEntriesRoles { + PathRole = Qt::UserRole + 1, + NameRole, + IconRole, + CategoryRole, + NoDisplayRole, + KeywordsRole, + ExecRole, + DesktopFileRole, + AppIdRole, + AppNameRole, + EntryRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + QStringList categories(); + + public slots: + void start(QString path, const QStringList &args = QStringList()); + void stop(QString path, const QStringList &args = QStringList()); + QString appIdData(QString appid, QString key); + + signals: + void raiseProcess(int pid); + void categoriesChanged(QStringList categories); + + private: + QString m_sources; + }; +} + +#endif // XDGDESKTOPENTRIES_H diff --git a/src/lib/xdgdesktopfilter.cpp b/src/lib/xdgdesktopfilter.cpp new file mode 100644 index 0000000..78e2ca2 --- /dev/null +++ b/src/lib/xdgdesktopfilter.cpp @@ -0,0 +1,131 @@ +#include "xdgdesktopfilter.h" +#include "xdgdesktopentries.h" +#include "xdgentry.h" +#include <qdebug.h> + +xdg::DesktopFilter::DesktopFilter(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +QObject *xdg::DesktopFilter::model() const +{ + return static_cast<QObject*>(m_model); +} + +QString xdg::DesktopFilter::category() const +{ + return m_category; +} + +QString xdg::DesktopFilter::section() const +{ + return m_section; +} + + +bool xdg::DesktopFilter::showAll() const +{ + return m_showAll; +} + +bool xdg::DesktopFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QVariant ventry = sourceModel()->data(index, xdg::DesktopEntries::EntryRole); + const xdg::Entry *entry = qvariant_cast<const xdg::Entry *>(ventry); + if(!entry) + { + qWarning() << "Can't retrive entry for " << sourceModel()->data(index, xdg::DesktopEntries::NameRole).toString(); + return false; + } + + if(!m_section.isEmpty() && entry->data(m_section + "/Exec").isEmpty()) + { + return false; + } + if(entry->data(XDG_NAME).isEmpty()) + return false; + if(!m_showAll && entry->data(XDG_NODISPLAY).toLower() == "true") + return false; + if(m_category.size() > 0 && entry->data(XDG_CATEGORIES) != m_category) + return false; + + return true; +} + + +QVariant xdg::DesktopFilter::data(const QModelIndex &index, int role) const +{ + if(!m_section.isEmpty()) + { + const xdg::Entry *entry = QSortFilterProxyModel::data(index, xdg::DesktopEntries::EntryRole).value<const xdg::Entry *>(); + if(entry) + { + QString res; + switch(role) { + case Qt::DisplayRole: + case xdg::DesktopEntries::NameRole: + res = entry->data(m_section + "/Name"); + return (res.isEmpty())?(res):(entry->data(XDG_NAME)); + case xdg::DesktopEntries::ExecRole: + res = entry->data(m_section + "/Exec"); + return (!res.isEmpty())?(res):(entry->data(XDG_EXEC)); + case xdg::DesktopEntries::IconRole: + res = entry->data(m_section + "/Icon"); + return (!res.isEmpty())?(res):(entry->data(XDG_ICON)); + case xdg::DesktopEntries::CategoryRole: + res = entry->data(m_section + "/Categories"); + return (!res.isEmpty())?(res):(entry->data(XDG_CATEGORIES)); + case xdg::DesktopEntries::KeywordsRole: + res = entry->data(m_section + "/Keywords"); + return (!res.isEmpty())?(res):(entry->data(XDG_KEYWORDS)); + } + } + } + return QSortFilterProxyModel::data(index, role); +} + +void xdg::DesktopFilter::setModel(QObject *model) +{ + if (m_model == model) + return; + + xdg::DesktopEntries *entries = qobject_cast<xdg::DesktopEntries*>(model); + if(entries) + { + m_model = entries; + setSourceModel(m_model); + emit modelChanged(m_model); + } +} + +void xdg::DesktopFilter::setCategory(QString category) +{ + if (m_category == category) + return; + + m_category = category; + invalidateFilter(); + emit categoryChanged(m_category); +} + +void xdg::DesktopFilter::setSection(QString section) +{ + if (m_section == section) + return; + + m_section = section; + invalidateFilter(); + emit sectionChanged(m_section); +} + +void xdg::DesktopFilter::setShowAll(bool showall) +{ + if (m_showAll == showall) + return; + + m_showAll = showall; + invalidateFilter(); + emit showAllChanged(m_showAll); +} diff --git a/src/lib/xdgdesktopfilter.h b/src/lib/xdgdesktopfilter.h new file mode 100644 index 0000000..abe2f06 --- /dev/null +++ b/src/lib/xdgdesktopfilter.h @@ -0,0 +1,49 @@ +#ifndef XDGDESKTOPFILTER_H +#define XDGDESKTOPFILTER_H + +#include <QSortFilterProxyModel> +#include <QObject> +#include "xdgdesktopentries.h" + +namespace xdg { + class DesktopEntries; + + class DesktopFilter : public QSortFilterProxyModel + { + Q_OBJECT + Q_PROPERTY(QObject *model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged) + Q_PROPERTY(QString section READ section WRITE setSection NOTIFY sectionChanged) + Q_PROPERTY(bool showAll READ showAll WRITE setShowAll NOTIFY showAllChanged) + + xdg::DesktopEntries *m_model; + QString m_category; + QString m_section; + bool m_showAll {false}; + + public: + explicit DesktopFilter(QObject *parent = nullptr); + QObject *model() const; + QString category() const; + QString section() const; + bool showAll() const; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + public slots: + void setModel(QObject * model); + void setCategory(QString category); + void setSection(QString category); + void setShowAll(bool showall); + + signals: + void raise(int pid); + void modelChanged(QObject * model); + void categoryChanged(QString category); + void sectionChanged(QString category); + void showAllChanged(bool showall); + }; + +} +#endif // XDGDESKTOPFILTER_H diff --git a/src/lib/xdgentries.cpp b/src/lib/xdgentries.cpp new file mode 100644 index 0000000..790e472 --- /dev/null +++ b/src/lib/xdgentries.cpp @@ -0,0 +1,128 @@ +#include "xdgentries.h" +#include "xdgentry.h" +#include <QDebug> +#include <QDir> +#include <QFileInfo> +#include <set> +#include <algorithm> + +xdg::Entries::Entries(QObject *parent) + : QObject(parent) +{ + QObject::connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &Entries::_directoryChanged, Qt::QueuedConnection); +} + +void xdg::Entries::_directoryChanged(const QString &path) +{ + QDir directory(path); + QStringList old, now, removed, added; + for(xdg::Entry *entry: m_entries) + { + if(entry->path().startsWith(directory.absolutePath())) + { + old.append(entry->path()); + } + } + for(const QString &item: directory.entryList(QStringList() << "*.desktop", QDir::Files)) + { + QString current = directory.absoluteFilePath(item); + if(current.startsWith(directory.absolutePath())) + { + now.append(current); + } + } + std::set_difference(old.begin(), old.end(), now.begin(), now.end(), std::inserter(removed, removed.begin())); + std::set_difference(now.begin(), now.end(), old.begin(), old.end(), std::inserter(added, added.begin())); + for(const QString &item: removed) + { + unregister(item); + } + for(const QString &item: added) + { + add(item, true); + } +} + +void xdg::Entries::_entryDataChanged(const QString key, const QString value) +{ + xdg::Entry* ent = qobject_cast<xdg::Entry*>(QObject::sender()); + emit entryDataChanged(ent->appId(), key, value); +} + +void xdg::Entries::unregister(const QString &path) +{ + QFileInfo info(path); + QString id = info.completeBaseName(); + int oldid = m_entries.keys().indexOf(id); + emit startRemoveEntry(oldid); + xdg::Entry* old = m_entries[id]; + m_watcher.removePath(old->path()); + m_entries.remove(id); + emit endRemoveEntry(oldid); +} + +int xdg::Entries::add(const QString &path, bool replace) +{ + QFileInfo info(path); + QString id = info.completeBaseName(); + if(!info.isFile() || info.suffix() != "desktop") + { + qWarning() << path << "is not a desktop entry"; + return -1; + } + + if(m_entries.contains(id)) + { + if(replace) + { + unregister(path); + } + else + { + return -1; + } + } + + xdg::Entry* entry = new xdg::Entry(path); + QObject::connect(entry, &xdg::Entry::dataChanged, this, &Entries::_entryDataChanged); + QObject::connect(entry, &xdg::Entry::raiseProcess, this, &Entries::raiseProcess); + + m_entries[id] = entry; + emit startCreateEntry(m_entries.keys().indexOf(id)); + emit endCreateEntry(m_entries.keys().indexOf(id)); + return 0; +} + +int xdg::Entries::addDirectory(const QString &path, bool replace) +{ + QFileInfo info(path.trimmed()); + if(!info.isDir()) + { + return -1; + } + + QDir directory(path.trimmed()); + QStringList items = directory.entryList(QStringList() << "*.desktop", QDir::Files); + for(QString &item: items) + { + add(directory.absoluteFilePath(item), replace); + } + + m_watcher.addPath(path.trimmed()); + return 0; +} + +qsizetype xdg::Entries::count() const +{ + return m_entries.count(); +} + +const xdg::Entry *xdg::Entries::getEntry(int i) const +{ + return m_entries[m_entries.keys()[i]]; +} + +QList<xdg::Entry *> xdg::Entries::getEntries() +{ + return m_entries.values(); +} diff --git a/src/lib/xdgentries.h b/src/lib/xdgentries.h new file mode 100644 index 0000000..fd0458a --- /dev/null +++ b/src/lib/xdgentries.h @@ -0,0 +1,41 @@ +#ifndef XDGENTRIES_H +#define XDGENTRIES_H + +#include <QObject> +#include <QFileSystemWatcher> +#include <QMap> + +namespace xdg { + class Entry; + + class Entries : public QObject + { + Q_OBJECT + QString m_type; + QFileSystemWatcher m_watcher; + QMap<QString, xdg::Entry*> m_entries; + + private slots: + void _directoryChanged(const QString &path); + void _entryDataChanged(const QString key, const QString value); + + public: + explicit Entries(QObject *parent = nullptr); + void unregister(const QString &path); + int add(const QString &path, bool replace = false); + int addDirectory(const QString &path, bool replace = false); + qsizetype count() const; + const Entry *getEntry(int i) const; + QList<xdg::Entry *> getEntries(); + + signals: + void entryDataChanged(const QString appid, const QString key, const QString value); + void raiseProcess(int pid); + void startCreateEntry(int id); + void endCreateEntry(int id); + void startRemoveEntry(int id); + void endRemoveEntry(int id); + }; +} + +#endif // XDGENTRIES_H diff --git a/src/lib/xdgentry.cpp b/src/lib/xdgentry.cpp new file mode 100644 index 0000000..83438dd --- /dev/null +++ b/src/lib/xdgentry.cpp @@ -0,0 +1,315 @@ +#include "xdgentry.h" +#include <QSettings> +#include <qdebug.h> +#include <signal.h> +#include <vector> +#include <string> +#include <QFileInfo> +#include "xdgbasedir.h" + +static inline QStringList tokenize(const QString& command) +{ + std::string s = command.toStdString(); + std::vector<std::string> result; + + std::string token; + char quote{}; + bool escape{false}; + + for (char c : s) + { + if (escape) + { + escape = false; + if (quote && c != '\\' && c != quote) + token += '\\'; + token += c; + } + else if (c == '\\') + { + escape = true; + } + else if (!quote && (c == '\'' || c == '\"')) + { + quote = c; + } + else if (quote && c == quote) + { + quote = '\0'; + if (token.empty()) + result.emplace_back(); + } + else if (!isspace(c) || quote) + { + token += c; + } + else if (!token.empty()) + { + result.push_back(std::move(token)); + token.clear(); + } + } + + if (!token.empty()) + { + result.push_back(std::move(token)); + token.clear(); + } + + + QStringList l; + for (auto t: result) + l.append(QString(t.c_str())); + return l; +} + +static inline QStringList args_substitution(const QStringList &tokens, const QStringList &args) +{ + QStringList out; + for (auto t : tokens) + { + if (t == "%d" || t == "%D" || t == "%n" || t == "%N" || t == "%v" || t == "%m") { //deprectaed stuff + continue; + } else if (t == "%i" || t == "%c") { //unsupported stuff + qWarning() << "XdgEntry: %i and %c are not supported (found in " << tokens.join(" ")<<")"; + continue; + } else if (t == "%f" || t == "%F") { // files + if (t == "%f") + { + if (args.length() > 0) + { + out.append(args[0]); + } + else + { + out.append(args); + } + } + } else if (t == "%u" || t == "%U") { // urls + // TODO : handle URLs properly + if (t == "%u") + { + if (args.length() > 0) + { + out.append(args[0]); + } + else + { + out.append(args); + } + } + } else { + out.append(t); + } + } + return out; +} + + +xdg::Entry::Entry(QString desktopfile, QObject *parent) + : QObject(parent) + , m_path(desktopfile) + , m_file(new QSettings(desktopfile, QSettings::IniFormat, this)) + , m_watcher(QStringList() << desktopfile) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + +xdg::Entry::Entry(QObject *parent) + : QObject(parent) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + +xdg::Entry::Entry(const Entry &origin) + : QObject(origin.parent()) + , m_path(origin.path()) + , m_file(new QSettings(origin.path(), QSettings::IniFormat, this)) + , m_watcher(QStringList() << origin.path()) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland-egl"); + env.remove("QT_IM_MODULE"); + m_process.setProcessEnvironment(env); + QObject::connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &Entry::_desktopFileChanged, Qt::QueuedConnection); + QObject::connect(&m_process, &QProcess::stateChanged, this, &Entry::_processStateChanged); +} + + +QString xdg::Entry::data(const QString &key) const +{ + if(!m_file) + return QString(); + if(key == XDG_KEYWORDS) + { + QString keywords = m_file->value(key).toString(); + if(keywords.contains("RemoteProcess")) + return keywords; + + switch(m_process.state()) + { + case QProcess::Running: + keywords.append(" running"); + break; + case QProcess::NotRunning: + keywords.append(" stopped"); + break; + case QProcess::Starting: + keywords.append(" starting"); + break; + } + return keywords; + } + if(!m_used.contains(key)) + m_used[key] = m_file->value(key).toString(); + return m_file->value(key).toString(); +} + +void xdg::Entry::start(QStringList args) +{ + QString exec = data(XDG_EXEC); + QString startExec = data(XDG_ACTION_START); + + if(exec.size() > 0) + { + QStringList param = tokenize(exec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + if(m_process.state() == QProcess::Running) + { + emit raiseProcess(m_process.processId()); + } + else + { + m_process.start(cmd, argv); + } + } + else if(startExec.size() > 0) + { + QStringList param = tokenize(startExec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + m_process.start(cmd, argv); + } + else + { + qWarning() << "Can't find a way to start " << m_path; + } +} + +void xdg::Entry::stop(QStringList args) +{ + QString stopExec = data(XDG_ACTION_STOP); + + if(stopExec.size() > 0) + { + QStringList param = tokenize(stopExec); + QString cmd = param.takeFirst(); + QStringList argv = args_substitution(param, args); + + m_process.start(cmd, argv); + } + else + { + if(m_process.state() == QProcess::NotRunning) + return; + kill(m_process.processId(), SIGTERM); + } +} + +QString xdg::Entry::path() const +{ + return m_path; +} + +QString xdg::Entry::appId() const +{ + QFileInfo fi(path()); + return fi.completeBaseName(); +} + +QString xdg::Entry::icon() const +{ + return data(XDG_ICON); +} + +int xdg::Entry::pid() const +{ + return m_process.processId(); +} + +void xdg::Entry::setDesktopFile(QString desktopFile) +{ + m_path = desktopFile; + m_file = new QSettings(desktopFile, QSettings::IniFormat); + m_watcher.addPaths(QStringList() << desktopFile); + emit iconChanged(data(XDG_ICON)); +} + +void xdg::Entry::setAppId(QString appId) +{ + /* Search for appid destopfile */ + QString filename; + + filename = xdg::dataHome() + "/applications/" + appId + ".desktop"; + if(filename.length()) + { + QFileInfo info(filename); + if(info.exists()) + { + setDesktopFile(filename); + return; + } + } + + for(QString dir: xdg::dataDirs()) + { + filename = dir + "/applications/" + appId + ".desktop"; + QFileInfo info(filename); + if(info.exists()) + { + setDesktopFile(filename); + return; + } + } + qWarning() << "Can't find Desktop Entry for " + appId; +} + + +void xdg::Entry::_desktopFileChanged(const QString &path) +{ + if(!m_file) + return; + QFileInfo info(path); + if(!info.isFile()) + { + emit removed(path); + return; + } + m_file->sync(); + for(const QString &key: m_used.keys()) + { + if(m_file->value(key).toString() != m_used[key]) + { + m_used[key] = m_file->value(key).toString(); + emit dataChanged(key, m_used[key]); + } + } +} + +void xdg::Entry::_processStateChanged(QProcess::ProcessState newState) +{ + emit dataChanged(XDG_KEYWORDS, data(XDG_KEYWORDS)); +} diff --git a/src/lib/xdgentry.h b/src/lib/xdgentry.h new file mode 100644 index 0000000..006d3ae --- /dev/null +++ b/src/lib/xdgentry.h @@ -0,0 +1,67 @@ +#ifndef XDGENTRY_H +#define XDGENTRY_H + +#include <QObject> +#include <QFileSystemWatcher> +#include <QProcess> +#include <QSettings> + +#define XDG_EXEC "Desktop Entry/Exec" +#define XDG_ICON "Desktop Entry/Icon" +#define XDG_NAME "Desktop Entry/Name" +#define XDG_KEYWORDS "Desktop Entry/Keywords" +#define XDG_NODISPLAY "Desktop Entry/NoDisplay" +#define XDG_CATEGORIES "Desktop Entry/Categories" + +#define XDG_ACTION_START "Desktop Action Start/Exec" +#define XDG_ACTION_STOP "Desktop Action Stop/Exec" + +namespace xdg { + class Entry: public QObject + { + Q_OBJECT + + Q_PROPERTY(QString appId READ appId WRITE setAppId NOTIFY appIdChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString desktopFile READ path WRITE setDesktopFile NOTIFY desktopFileChanged) + + QString m_path; + QSettings *m_file {nullptr}; + QFileSystemWatcher m_watcher; + QProcess m_process; + mutable QMap<QString, QString> m_used; + + private slots: + void _desktopFileChanged(const QString &path); + void _processStateChanged(QProcess::ProcessState newState); + + public: + explicit Entry(QString desktopfile, QObject *parent = nullptr); + explicit Entry(QObject *parent = nullptr); + explicit Entry(const xdg::Entry& origin); + + QString path() const; + QString appId() const; + QString icon() const; + int pid() const; + void setDesktopFile(QString desktopFile); + void setAppId(QString appId); + + public slots: + void start(QStringList args); + void stop(QStringList args); + QString data(const QString &key) const; + + signals: + void dataChanged(const QString key, const QString value); + void raiseProcess(int pid); + void removed(const QString path); + void appIdChanged(QString appId); + void iconChanged(QString icon); + void desktopFileChanged(QString desktopFile); + }; +} + +Q_DECLARE_METATYPE(const xdg::Entry*) + +#endif // XDGENTRY_H diff --git a/src/lib/xdgnotificationactions.cpp b/src/lib/xdgnotificationactions.cpp new file mode 100644 index 0000000..afa7c19 --- /dev/null +++ b/src/lib/xdgnotificationactions.cpp @@ -0,0 +1,63 @@ +#include "xdgnotificationactions.h" +#include <QDebug> + +xdg::NotificationActions::NotificationActions(QObject *parent) + : QAbstractListModel(parent) +{ + +} + +QStringList xdg::NotificationActions::actionslist() const +{ + return m_actionslist; +} + +void xdg::NotificationActions::setActionslist(const QStringList &newActionslist) +{ + if(newActionslist.count() % 2 != 0) + { + qWarning() << "Error in actionlist:" << newActionslist; + } + + if (m_actionslist == newActionslist) + return; + m_actionslist = newActionslist; + + beginResetModel(); + m_actions.clear(); + for(int i = 0; i < newActionslist.count() / 2; i++) + { + m_actions.append(QPair<QString, QString>(newActionslist[i * 2], newActionslist[(i * 2) + 1])); + } + endResetModel(); + + emit actionslistChanged(); +} + +int xdg::NotificationActions::rowCount(const QModelIndex &parent) const +{ + return m_actions.count(); +} + +QVariant xdg::NotificationActions::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_actions.count()) + return QVariant(); + + switch(role) { + case Qt::DisplayRole: + case TextRole: + return m_actions[index.row()].second; + case KeyRole: + return m_actions[index.row()].first; + } + return QVariant(); +} + +QHash<int, QByteArray> xdg::NotificationActions::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[KeyRole] = "key"; + roles[TextRole] = "actionText"; + return roles; +} diff --git a/src/lib/xdgnotificationactions.h b/src/lib/xdgnotificationactions.h new file mode 100644 index 0000000..1d1e449 --- /dev/null +++ b/src/lib/xdgnotificationactions.h @@ -0,0 +1,40 @@ +#ifndef XDGNOTIFICATIONACTIONS_H +#define XDGNOTIFICATIONACTIONS_H + +#include <QAbstractListModel> + +namespace xdg { + + class NotificationActions: public QAbstractListModel + { + Q_OBJECT + + QList<QPair<QString, QString>> m_actions; + + Q_PROPERTY(QStringList actionslist READ actionslist WRITE setActionslist NOTIFY actionslistChanged) + + public: + NotificationActions(QObject *parent = nullptr); + + enum NotificationActionsRoles { + KeyRole = Qt::UserRole + 1, + TextRole, + }; + + QStringList actionslist() const; + void setActionslist(const QStringList &newActionslist); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + signals: + void actionslistChanged(); + private: + QStringList m_actionslist; + }; + +} + + +#endif // XDGNOTIFICATIONACTIONS_H diff --git a/src/lib/xdgnotificationmanager.cpp b/src/lib/xdgnotificationmanager.cpp new file mode 100644 index 0000000..06e8594 --- /dev/null +++ b/src/lib/xdgnotificationmanager.cpp @@ -0,0 +1,173 @@ +#include "xdgnotificationmanager.h" +#include "xdgnotificationserver.h" +#include "xdgnotificationmessage.h" +#include "notificationsadaptor.h" + +uint xdg::serverNotify(QObject *obj, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + xdg::NotificationManager *manager = qobject_cast<xdg::NotificationManager*>(obj); + if(replaces_id != 0 && manager->m_pending.contains(replaces_id)) + { + xdg::NotificationMessage *message = manager->m_pending[replaces_id]; + message->update(app_name, app_icon, summary, body, actions, hints, timeout); + } + else + { + xdg::NotificationMessage *message = new xdg::NotificationMessage(app_name, app_icon, summary, body, actions, hints, timeout, manager); + + QObject::connect(message, &xdg::NotificationMessage::appNameChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::AppNameRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::appIconChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::AppIconRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::summaryChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::SummaryRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::bodyChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::BodyRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::actionsChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::ActionsRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + QObject::connect(message, &xdg::NotificationMessage::hintsChanged, [manager, replaces_id](){ + const QVector<int> roles = {xdg::NotificationManager::HintsRole}; + emit manager->dataChanged( + manager->index(replaces_id), + manager->index(replaces_id), + roles); + }); + + + QObject::connect(message, &xdg::NotificationMessage::expired, [manager](QObject *obj){ + xdg::NotificationMessage *msg = qobject_cast<xdg::NotificationMessage*>(obj); + if(msg) + { + manager->close(manager->m_pending.key(msg), 1); + } + }); + + manager->beginInsertRows(QModelIndex(), 0, 0); + manager->m_pending[++manager->m_last] = message; + message->setId(manager->m_last); + manager->m_ids.insert(0, manager->m_last); + manager->endInsertRows(); + } + return manager->m_last; +} + + +xdg::NotificationManager::NotificationManager(QObject *parent) + : QAbstractListModel(parent) +{ + m_server = new XdgNotificationServer(this); + m_server->setNotify(serverNotify, this); + QObject::connect(m_server, &XdgNotificationServer::closeNotification, this, &NotificationManager::_closeNotification); + QObject::connect(this, &NotificationManager::rowsRemoved, this, &NotificationManager::notificationsChanged); + QObject::connect(this, &NotificationManager::rowsInserted, this, &NotificationManager::notificationsChanged); + new NotificationsAdaptor(m_server); + XdgNotificationServer::RegisterServerToDbus(*m_server); +} + +int xdg::NotificationManager::rowCount(const QModelIndex &parent) const +{ + return m_ids.count(); +} + +QVariant xdg::NotificationManager::data(const QModelIndex &index, int role) const +{ + if(index.row() > m_ids.count()) + return QVariant(); + + const NotificationMessage *message = m_pending[m_ids[index.row()]]; + if(message == nullptr) + return QVariant(); + + switch(role) { + case Qt::DisplayRole: + case AppNameRole: + return message->appName(); + case AppIconRole: + return message->appIcon(); + case SummaryRole: + return message->summary(); + case BodyRole: + return message->body(); + case ActionsRole: + return message->actions(); + case HintsRole: + return message->hints(); + case IdRole: + return message->id(); + } + return QVariant(); +} + +QHash<int, QByteArray> xdg::NotificationManager::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[AppNameRole] = "appName"; + roles[AppIconRole] = "appIcon"; + roles[SummaryRole] = "summary"; + roles[BodyRole] = "body"; + roles[ActionsRole] = "actions"; + roles[HintsRole] = "hints"; + roles[IdRole] = "appId"; + return roles; +} + + +int xdg::NotificationManager::nbNotifications() +{ + return rowCount(); +} + +void xdg::NotificationManager::close(unsigned int id, unsigned int reason) +{ + unsigned int index = m_ids.indexOf(id); + beginRemoveRows(QModelIndex(), index, index); + m_ids.removeAll(id); + NotificationMessage *message = m_pending[id]; + m_pending.remove(id); + if (message != NULL) { + message->deleteLater(); + } + endRemoveRows(); + m_server->sendNotificationClosed(id, reason); +} + +void xdg::NotificationManager::returnAction(unsigned int appid, const QString &key) +{ + m_server->sendActionInvoked(appid, key); + close(appid, 2); +} + +void xdg::NotificationManager::_closeNotification(unsigned int id) +{ + close(id, 3); +} diff --git a/src/lib/xdgnotificationmanager.h b/src/lib/xdgnotificationmanager.h new file mode 100644 index 0000000..181bf0b --- /dev/null +++ b/src/lib/xdgnotificationmanager.h @@ -0,0 +1,55 @@ +#ifndef SystrayNotifyManager_H +#define SystrayNotifyManager_H + +#include <QAbstractListModel> +#include <QObject> + +class XdgNotificationServer; + +namespace xdg { + uint serverNotify(QObject *manager, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + class NotificationMessage; + + class NotificationManager : public QAbstractListModel + { + Q_OBJECT + Q_PROPERTY(int nbNotifications READ nbNotifications NOTIFY notificationsChanged) + + XdgNotificationServer *m_server; + unsigned int m_last = 0; + QMap<unsigned int, NotificationMessage *> m_pending; + QList<unsigned int> m_ids; + + friend uint serverNotify(QObject *manager, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + public: + explicit NotificationManager(QObject *parent = nullptr); + + enum SystrayNotifyRoles { + AppNameRole = Qt::UserRole + 1, + AppIconRole, + SummaryRole, + BodyRole, + ActionsRole, + HintsRole, + IdRole, + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + + public slots: + int nbNotifications(); + void close(unsigned int id, unsigned int reason = 2); + void returnAction(unsigned int appid, const QString &key); + + private slots: + void _closeNotification(unsigned int id); + + signals: + void notificationsChanged(); + }; +} + +#endif // SystrayNotifyManager_H diff --git a/src/lib/xdgnotificationmessage.cpp b/src/lib/xdgnotificationmessage.cpp new file mode 100644 index 0000000..3dc0e2e --- /dev/null +++ b/src/lib/xdgnotificationmessage.cpp @@ -0,0 +1,119 @@ +#include "xdgnotificationmessage.h" +#include <QTimer> + + +QString xdg::NotificationMessage::appName() const +{ + return m_appName; +} + +QString xdg::NotificationMessage::appIcon() const +{ + return m_appIcon; +} + +QString xdg::NotificationMessage::summary() const +{ + return m_summary; +} + +QString xdg::NotificationMessage::body() const +{ + return m_body; +} + +QStringList xdg::NotificationMessage::actions() const +{ + return m_actions; +} + +QVariantMap xdg::NotificationMessage::hints() const +{ + return m_hints; +} + +uint xdg::NotificationMessage::id() const +{ + return m_id; +} + +void xdg::NotificationMessage::_expired() +{ + emit expired(this); +} + + +xdg::NotificationMessage::NotificationMessage( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout, + QObject *parent) + : QObject(parent) + , m_appName(app_name) + , m_appIcon(app_icon) + , m_summary(summary) + , m_body(body) + , m_actions(actions) + , m_hints(hints) + , m_timeout(timeout) +{ + if(m_timeout > 0) + QTimer::singleShot(m_timeout, this, &xdg::NotificationMessage::_expired); +} + +void xdg::NotificationMessage::update(const QString &app_name, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + if(m_appName != app_name) + { + m_appName = app_name; + emit appNameChanged(m_appName); + } + + if(m_appIcon != app_icon) + { + m_appIcon = app_icon; + emit appIconChanged(m_appIcon); + } + + if(m_summary != summary) + { + m_summary = summary; + emit summaryChanged(m_summary); + } + + if(m_body != app_name) + { + m_body = body; + emit bodyChanged(m_body); + } + + if(m_actions != actions) + { + m_actions = actions; + emit actionsChanged(m_actions); + } + + if(m_hints != hints) + { + m_hints = hints; + emit hintsChanged(m_hints); + } + + if(m_timeout != timeout) + { + m_timeout = timeout; + emit timeoutChanged(m_timeout); + } + if(m_timeout > 0) + QTimer::singleShot(m_timeout, this, &xdg::NotificationMessage::_expired); + +} + +void xdg::NotificationMessage::setId(uint id) +{ + m_id = id; +} diff --git a/src/lib/xdgnotificationmessage.h b/src/lib/xdgnotificationmessage.h new file mode 100644 index 0000000..8e62cab --- /dev/null +++ b/src/lib/xdgnotificationmessage.h @@ -0,0 +1,67 @@ +#ifndef NOTIFYMESSAGE_H +#define NOTIFYMESSAGE_H + +#include <QObject> +#include <QVariantMap> + +namespace xdg { + class NotificationMessage : public QObject + { + Q_OBJECT + + QString m_appName; + QString m_appIcon; + QString m_summary; + QString m_body; + QStringList m_actions; + QVariantMap m_hints; + uint m_id; + int m_timeout; + + + public: + explicit NotificationMessage( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout, + QObject *parent = nullptr); + + void update( + const QString &app_name, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout); + + void setId(uint id); + + QString appName() const; + QString appIcon() const; + QString summary() const; + QString body() const; + QStringList actions() const; + QVariantMap hints() const; + uint id() const; + + private slots: + void _expired(); + + signals: + void appNameChanged(QString appName); + void appIconChanged(QString appIcon); + void summaryChanged(QString summary); + void bodyChanged(QString body); + void actionsChanged(QStringList actions); + void hintsChanged(QVariantMap hints); + void timeoutChanged(int timeout); + void expired(QObject *obj); + }; +} + +#endif // NOTIFYMESSAGE_H diff --git a/src/lib/xdgnotificationserver.cpp b/src/lib/xdgnotificationserver.cpp new file mode 100644 index 0000000..3f83cea --- /dev/null +++ b/src/lib/xdgnotificationserver.cpp @@ -0,0 +1,87 @@ +#include "xdgnotificationserver.h" +#include <QDBusConnection> +#include <QDebug> + +void XdgNotificationServer::setNotify(NotifyCallback newNotify, QObject *obj) +{ + m_notify = newNotify; + m_notifyData = obj; +} + +void XdgNotificationServer::sendActionInvoked(uint id, const QString &action_key) +{ + emit ActionInvoked(id, action_key); +} + +void XdgNotificationServer::sendNotificationClosed(uint id, uint reason) +{ + emit NotificationClosed(id, reason); +} + +XdgNotificationServer::XdgNotificationServer(QObject *parent) + : QObject{parent} +{ + +} + +void XdgNotificationServer::RegisterServerToDbus(XdgNotificationServer &server) +{ + const QString service = "org.freedesktop.Notifications"; + const QString path = "/org/freedesktop/Notifications"; + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + } + bus.registerObject(path, &server); + + bool conres = bus.registerService(service); + if(!conres) qWarning() << "Can't register dbus service " << service; +} + +void XdgNotificationServer::CloseNotification(uint id) +{ + emit closeNotification(id); +} + +QStringList XdgNotificationServer::GetCapabilities() +{ + return QStringList(); +} + +QString XdgNotificationServer::GetServerInformation(QString &vendor, QString &version, QString &spec_version) +{ + vendor = "KaZoe"; + version = "1.0.0"; + spec_version = "1.2"; + return QString(); // Why return string ? spec is return void +} + +uint XdgNotificationServer::Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) +{ + if(m_notify) + { + return m_notify(m_notifyData, app_name, replaces_id, app_icon, summary, body, actions, hints, timeout); + } + else + { + qDebug() << "XdgNotificationServer::Notify(" + << app_name + << ", " + << replaces_id + << ", " + << app_icon + << ", " + << summary + << ", " + << body + << ", " + << actions + << ", " + << hints + << ", " + << timeout + << ")"; + } + return 0; +} diff --git a/src/lib/xdgnotificationserver.h b/src/lib/xdgnotificationserver.h new file mode 100644 index 0000000..5bdfbdf --- /dev/null +++ b/src/lib/xdgnotificationserver.h @@ -0,0 +1,42 @@ +#ifndef XDGNOTIFICATIONSERVER_H +#define XDGNOTIFICATIONSERVER_H + +#include <QObject> + +typedef uint (*NotifyCallback)(QObject *obj, const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) override; + + +class XdgNotificationServer : public QObject +{ + Q_OBJECT + + NotifyCallback m_notify; + QObject *m_notifyData; + +public: + explicit XdgNotificationServer(QObject *parent = nullptr); + static void RegisterServerToDbus(XdgNotificationServer &server); + + void setNotify(NotifyCallback newNotify, QObject *obj); + void sendActionInvoked(uint id, const QString &action_key); + void sendNotificationClosed(uint id, uint reason); + +public slots: + + /* DBus API */ + virtual void CloseNotification(uint id); + virtual QStringList GetCapabilities(); + virtual QString GetServerInformation(QString &vendor, QString &version, QString &spec_version); + virtual uint Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); + + + +signals: + void closeNotification(uint id); + + void ActionInvoked(uint id, const QString &action_key); + void NotificationClosed(uint id, uint reason); + +}; + +#endif // XDGNOTIFICATIONSERVER_H diff --git a/src/lib/xdgnotifier.cpp b/src/lib/xdgnotifier.cpp new file mode 100644 index 0000000..da52ec5 --- /dev/null +++ b/src/lib/xdgnotifier.cpp @@ -0,0 +1,99 @@ +#include "xdgnotifier.h" +#include "NotificationsIface.h" +#include <QGuiApplication> + +XdgNotifier::XdgNotifier(QObject *parent) + : QObject{parent} +{ + QDBusConnection connection = QDBusConnection::sessionBus(); + + if (!connection.isConnected()) { + qWarning("Cannot connect to the D-Bus session bus.\n" + "Please check your system settings and try again.\n"); + } + const QString service = "org.freedesktop.Notifications"; + const QString path = "/org/freedesktop/Notifications"; + + m_iface = new OrgFreedesktopNotificationsInterface(service, path, connection); + + QObject::connect(m_iface, &OrgFreedesktopNotificationsInterface::ActionInvoked, [this](uint id, const QString &action_key){ + qDebug() << "Recive signal ActionInvoked(" << id << ", " << action_key << ")"; + emit actionInvoked(action_key); + }); + + QObject::connect(m_iface, &OrgFreedesktopNotificationsInterface::NotificationClosed, [this](uint id, uint reason){ + qDebug() << "Recive signal NotificationClosed(" << id << ", " << reason << ")"; + switch(reason) + { + case 1: + emit notificationClosed("The notification expired."); + break; + case 2: + emit notificationClosed("The notification was dismissed by the user."); + break; + case 3: + emit notificationClosed(" The notification was closed by a call to CloseNotification."); + break; + default: + emit notificationClosed("Undefined/reserved reasons."); + break; + } + }); +} + +void XdgNotifier::notifyTextOnly(QString summary, QString body, bool replace) +{ + qDebug() << "notifyTextOnly(" << summary << ", " << body << ")"; + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + QStringList(), + QVariantMap(), + -1 + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::notifyWithActions(QString summary, QString body, bool replace, QStringList actions) +{ + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + actions, + QVariantMap(), + -1 + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::notify(const QString &summary, const QString &body, bool replace, const QStringList &actions, int timeout) +{ + auto reply = m_iface->Notify( + QGuiApplication::applicationDisplayName(), + (replace)?(m_lastid):(0), + QString(), + summary, + body, + actions, + QVariantMap(), + timeout + ); + reply.waitForFinished(); + if(!replace) + m_lastid = reply.value(); +} + +void XdgNotifier::closeNotification() +{ + auto reply = m_iface->CloseNotification(m_lastid); +} diff --git a/src/lib/xdgnotifier.h b/src/lib/xdgnotifier.h new file mode 100644 index 0000000..7147626 --- /dev/null +++ b/src/lib/xdgnotifier.h @@ -0,0 +1,28 @@ +#ifndef NOTIFIER_H +#define NOTIFIER_H + +#include <QObject> +#include <QDBusConnection> + +class OrgFreedesktopNotificationsInterface; + +class XdgNotifier : public QObject +{ + Q_OBJECT + OrgFreedesktopNotificationsInterface *m_iface; + int m_lastid; +public: + explicit XdgNotifier(QObject *parent = nullptr); + +public slots: + void notifyTextOnly(QString summary, QString body, bool replace); + void notifyWithActions(QString summary, QString body, bool replace, QStringList actions); + void notify(const QString &summary, const QString &body, bool replace, const QStringList &actions, int timeout); + void closeNotification(); + +signals: + void actionInvoked(QString action); + void notificationClosed(QString reason); +}; + +#endif // NOTIFIER_H diff --git a/src/lib/xdgstatusnotifierhost.cpp b/src/lib/xdgstatusnotifierhost.cpp new file mode 100644 index 0000000..49a17b1 --- /dev/null +++ b/src/lib/xdgstatusnotifierhost.cpp @@ -0,0 +1,187 @@ +#include "xdgstatusnotifierhost.h" +#include "XdgStatusNotifierItemIface.h" +#include "XdgStatusNotifierWatcherIface.h" +#include <QDBusConnection> +#include "debug.h" + +using namespace org::kde; + +namespace xdg { + class StatusNotifierHostPrivate { + Q_DISABLE_COPY(StatusNotifierHostPrivate) + Q_DECLARE_PUBLIC(StatusNotifierHost) + StatusNotifierHost * const q_ptr; + StatusNotifierHostPrivate(StatusNotifierHost *parent) + : q_ptr(parent) + , m_bus(QDBusConnection::sessionBus()) + {}; + + QMap<QString, StatusNotifierItem*> m_items; + QList<QString> m_itemsorder; + StatusNotifierWatcher *m_watcher; + QDBusConnection m_bus; + QString m_serviceName; + + void addNotifier(QString item) + { + StatusNotifierItem *stat = new StatusNotifierItem(item.split("/").first(), "/StatusNotifierItem", m_bus, q_ptr); + int index = m_itemsorder.size(); + q_ptr->beginInsertRows(QModelIndex(), index, index); + m_itemsorder.append(item); + m_items[item] = stat; + + QObject::connect(stat, &StatusNotifierItem::NewAttentionIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::AttentionIconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::IconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewOverlayIcon, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::OverlayIconNameRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewStatus, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::StatusRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + QObject::connect(stat, &StatusNotifierItem::NewTitle, [index, this](){ + QVector<int> r; + r.append(StatusNotifierHost::TitleRole); + emit q_ptr->dataChanged(q_ptr->index(index), q_ptr->index(index), r); + }); + + q_ptr->endInsertRows(); + } + + void removeNotifier(QString item) + { + int index = m_itemsorder.indexOf(item); + q_ptr->beginRemoveRows(QModelIndex(), index, index); + m_itemsorder.removeAll(item); + m_items[item]->deleteLater(); + m_items.remove(item); + q_ptr->endRemoveRows(); + } + }; +}; + +using namespace xdg; + +StatusNotifierHost::StatusNotifierHost(QObject *parent) + : QAbstractListModel{parent} + , d_ptr(new StatusNotifierHostPrivate(this)) +{ + Q_D(StatusNotifierHost); + + d->m_serviceName = "org.kde.StatusNotifierHost-"; + d->m_serviceName.append(QString::number(QCoreApplication::applicationPid())); + qCDebug(SYSTEM_TRAY) << "Register Host service" << d->m_serviceName << ":" << d->m_bus.registerService(d->m_serviceName); + + d->m_watcher = new StatusNotifierWatcher("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", d->m_bus, this); + qCDebug(SYSTEM_TRAY) << "Find Watcher " << d->m_watcher->isValid() << " with protocol " << d->m_watcher->protocolVersion(); + + if(!d->m_watcher->isValid()) + { + qCWarning(SYSTEM_TRAY) << "Can't find watcher org.kde.StatusNotifierWatcher/StatusNotifierWatcher"; + } + + + d->m_watcher->RegisterStatusNotifierHost(d->m_serviceName); + + QObject::connect(d->m_watcher, &StatusNotifierWatcher::StatusNotifierItemRegistered, [this](QString in0){ + Q_D(StatusNotifierHost); + d->addNotifier(in0); + }); + + QObject::connect(d->m_watcher, &StatusNotifierWatcher::StatusNotifierItemUnregistered, [this](QString in0){ + Q_D(StatusNotifierHost); + d->removeNotifier(in0); + }); + + for(QString item: d->m_watcher->registeredStatusNotifierItems()) + { + d->addNotifier(item); + } +} + +StatusNotifierHost::~StatusNotifierHost() = default; + +int StatusNotifierHost::rowCount(const QModelIndex &parent) const +{ + Q_D(const StatusNotifierHost); + return d->m_items.size(); +} + +QVariant StatusNotifierHost::data(const QModelIndex &index, int role) const +{ + Q_D(const StatusNotifierHost); + + if(index.row() > d->m_itemsorder.count()) + return QVariant(); + + const StatusNotifierItem *stat = d->m_items[d->m_itemsorder[index.row()]]; + + switch(role) { + case CategoryRole: + return stat->category(); + case IdRole: + return stat->id(); + case TitleRole: + return stat->title(); + case StatusRole: + return stat->status(); + case WindowIdRole: + return stat->windowId(); + case IconNameRole: + return stat->iconName(); + case OverlayIconNameRole: + return stat->overlayIconName(); + case AttentionIconNameRole: + return stat->attentionIconName(); + case AttentionMovieNameRole: + return stat->attentionMovieName(); + } + + return QVariant(); +} + +QHash<int, QByteArray> StatusNotifierHost::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); + roles[CategoryRole] = "category"; + roles[IdRole] = "idItem"; + roles[TitleRole] = "title"; + roles[StatusRole] = "status"; + roles[WindowIdRole] = "windowId"; + roles[IconNameRole] = "iconName"; + roles[OverlayIconNameRole] = "overlayIconName"; + roles[AttentionIconNameRole] = "attentionIconName"; + roles[AttentionMovieNameRole] = "attentionMovieName"; + return roles; +} + +void StatusNotifierHost::activate(int x, int y, QString id, int mode) +{ + Q_D(const StatusNotifierHost); + for(const QString &key: d->m_itemsorder) + { + if(d->m_items[key]->id() == id) + { + if(mode == 1) + d->m_items[key]->SecondaryActivate(x, y); + else + d->m_items[key]->Activate(x, y); + return; + } + } +} diff --git a/src/lib/xdgstatusnotifierhost.h b/src/lib/xdgstatusnotifierhost.h new file mode 100644 index 0000000..20c36ed --- /dev/null +++ b/src/lib/xdgstatusnotifierhost.h @@ -0,0 +1,44 @@ +#ifndef XDGSTATUSNOTIFIERHOST_H +#define XDGSTATUSNOTIFIERHOST_H + +#include <QAbstractListModel> +#include <QObject> +#include <QScopedPointer> + +namespace xdg { + class StatusNotifierHostPrivate; + + class StatusNotifierHost : public QAbstractListModel + { + Q_OBJECT + public: + explicit StatusNotifierHost(QObject *parent = nullptr); + virtual ~StatusNotifierHost(); + + enum XdgDesktopEntriesRoles { + CategoryRole = Qt::UserRole + 1, + IdRole, + TitleRole, + StatusRole, + WindowIdRole, + IconNameRole, + OverlayIconNameRole, + AttentionIconNameRole, + AttentionMovieNameRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + public slots: + void activate(int x, int y, QString id, int mode); + + private: + Q_DISABLE_COPY(StatusNotifierHost) + Q_DECLARE_PRIVATE(StatusNotifierHost) + QScopedPointer<StatusNotifierHostPrivate> const d_ptr; + }; +}; + +#endif // XDGSTATUSNOTIFIERHOST_H diff --git a/src/lib/xdgstatusnotifieritem.cpp b/src/lib/xdgstatusnotifieritem.cpp new file mode 100644 index 0000000..1a14846 --- /dev/null +++ b/src/lib/xdgstatusnotifieritem.cpp @@ -0,0 +1,169 @@ +#include "xdgstatusnotifieritem.h" +#include <statusnotifieritemadaptor.h> +#include <XdgStatusNotifierWatcherIface.h> +#include <QDBusConnection> +#include <QGuiApplication> +#include <debug.h> + +using namespace xdg; + +StatusNotifierItem::StatusNotifierItem(QObject *parent) + : QObject{parent} +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qCWarning(SYSTEM_TRAY) << "Cannot connect to the D-Bus session bus.\n" << "Please check your system settings and try again.\n"; + } + QString serviceName = "org.xdgutils."; + setId((QGuiApplication::desktopFileName().isEmpty())?(QCoreApplication::applicationName()):(QGuiApplication::desktopFileName())); + serviceName.append(Id()); + serviceName.append("-"); + serviceName.append(QString::number(QCoreApplication::applicationPid())); + qCDebug(SYSTEM_TRAY) << "Register Item service" << serviceName << ":" << bus.registerService(serviceName); + new StatusNotifierItemAdaptor(this); + bus.registerObject("/StatusNotifierItem", this); + OrgKdeStatusNotifierWatcherInterface *watcher = new OrgKdeStatusNotifierWatcherInterface("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", bus); + watcher->RegisterStatusNotifierItem(serviceName); +} + +QString StatusNotifierItem::Title() const +{ + return m_Title; +} + +void StatusNotifierItem::setTitle(const QString &newTitle) +{ + if (m_Title == newTitle) + return; + m_Title = newTitle; + emit NewTitle(); + emit TitleChanged(); +} + +QString StatusNotifierItem::IconName() const +{ + return m_IconName; +} + +void StatusNotifierItem::setIconName(const QString &newIconName) +{ + if (m_IconName == newIconName) + return; + m_IconName = newIconName; + emit IconNameChanged(); + emit NewIcon(); +} + + +void StatusNotifierItem::Activate(uint x, uint y) +{ + emit activated(x, y); +} + +void StatusNotifierItem::SecondaryActivate(uint x, uint y) +{ + emit secondaryActivated(x, y); +} + +QString StatusNotifierItem::Status() const +{ + return m_Status; +} + +void StatusNotifierItem::setStatus(const QString &newStatus) +{ + if (m_Status == newStatus) + return; + if(!xdgStatus.contains(newStatus)) + { + qCWarning(SYSTEM_TRAY) << "Status " << newStatus << "not authorized"; + return; + } + m_Status = newStatus; + emit StatusChanged(); + emit NewStatus(m_Status); +} + +QString StatusNotifierItem::OverlayIconName() const +{ + return m_OverlayIconName; +} + +void StatusNotifierItem::setOverlayIconName(const QString &newOverlayIconName) +{ + if (m_OverlayIconName == newOverlayIconName) + return; + m_OverlayIconName = newOverlayIconName; + emit overlayIconNameChanged(); + emit NewOverlayIcon(); +} + +QString StatusNotifierItem::Category() const +{ + return m_Category; +} + +void StatusNotifierItem::setCategory(const QString &newCategory) +{ + if (m_Category == newCategory) + return; + if(!xdgCategories.contains(newCategory)) + { + qCWarning(SYSTEM_TRAY) << "Category " << newCategory << "not authorized"; + return; + } + m_Category = newCategory; + emit CategoryChanged(); +} + +QString StatusNotifierItem::Id() const +{ + return m_Id; +} + +void StatusNotifierItem::setId(const QString &newId) +{ + if (m_Id == newId) + return; + m_Id = newId; + emit IdChanged(); +} + +QString StatusNotifierItem::WindowId() const +{ + return m_WindowId; +} + +void StatusNotifierItem::setWindowId(const QString &newWindowId) +{ + if (m_WindowId == newWindowId) + return; + m_WindowId = newWindowId; + emit WindowIdChanged(); +} + +QString StatusNotifierItem::AttentionIconName() const +{ + return m_AttentionIconName; +} + +void StatusNotifierItem::setAttentionIconName(const QString &newAttentionIconName) +{ + if (m_AttentionIconName == newAttentionIconName) + return; + m_AttentionIconName = newAttentionIconName; + emit AttentionIconNameChanged(); +} + +QString StatusNotifierItem::AttentionMovieName() const +{ + return m_AttentionMovieName; +} + +void StatusNotifierItem::setAttentionMovieName(const QString &newAttentionMovieName) +{ + if (m_AttentionMovieName == newAttentionMovieName) + return; + m_AttentionMovieName = newAttentionMovieName; + emit AttentionMovieNameChanged(); +} diff --git a/src/lib/xdgstatusnotifieritem.h b/src/lib/xdgstatusnotifieritem.h new file mode 100644 index 0000000..7f881a4 --- /dev/null +++ b/src/lib/xdgstatusnotifieritem.h @@ -0,0 +1,119 @@ +#ifndef XDGSTATUSNOTIFIERITEM_H +#define XDGSTATUSNOTIFIERITEM_H + +#include <QObject> + +#define XDG_CATEGORY_APPLICATIONSTATUS "ApplicationStatus" +#define XDG_CATEGORY_COMMUNICATIONS "Communications" +#define XDG_CATEGORY_SYSTEMSERVICE "SystemServices" +#define XDG_CATEGORY_HARDWARE "Hardware" + +#define XDG_STATUS_PASSIVE "Passive" +#define XDG_STATUS_ACTIVE "Active" +#define XDG_STATUS_NEEDSATTENTION "NeedsAttention" + +namespace xdg { + + class StatusNotifierItem : public QObject + { + Q_OBJECT + + /* This properties are used for DBus object connection */ + Q_PROPERTY(QString Category READ Category WRITE setCategory NOTIFY CategoryChanged) + Q_PROPERTY(QString Id READ Id WRITE setId NOTIFY IdChanged) + Q_PROPERTY(QString Title READ Title WRITE setTitle NOTIFY TitleChanged) + Q_PROPERTY(QString Status READ Status WRITE setStatus NOTIFY StatusChanged) + Q_PROPERTY(QString WindowId READ WindowId WRITE setWindowId NOTIFY WindowIdChanged) + Q_PROPERTY(QString IconName READ IconName WRITE setIconName NOTIFY IconNameChanged) + Q_PROPERTY(QString OverlayIconName READ OverlayIconName WRITE setOverlayIconName NOTIFY overlayIconNameChanged) + Q_PROPERTY(QString AttentionIconName READ AttentionIconName WRITE setAttentionIconName NOTIFY AttentionIconNameChanged) + Q_PROPERTY(QString AttentionMovieName READ AttentionMovieName WRITE setAttentionMovieName NOTIFY AttentionMovieNameChanged) + + /* This properties are used for QML */ + Q_PROPERTY(QString category READ Category WRITE setCategory NOTIFY CategoryChanged) + Q_PROPERTY(QString idItem READ Id WRITE setId NOTIFY IdChanged) + Q_PROPERTY(QString title READ Title WRITE setTitle NOTIFY TitleChanged) + Q_PROPERTY(QString status READ Status WRITE setStatus NOTIFY StatusChanged) + Q_PROPERTY(QString windowId READ WindowId WRITE setWindowId NOTIFY WindowIdChanged) + Q_PROPERTY(QString iconName READ IconName WRITE setIconName NOTIFY IconNameChanged) + Q_PROPERTY(QString overlayIconName READ OverlayIconName WRITE setOverlayIconName NOTIFY overlayIconNameChanged) + Q_PROPERTY(QString attentionIconName READ AttentionIconName WRITE setAttentionIconName NOTIFY AttentionIconNameChanged) + Q_PROPERTY(QString attentionMovieName READ AttentionMovieName WRITE setAttentionMovieName NOTIFY AttentionMovieNameChanged) + + public: + const QStringList xdgCategories {XDG_CATEGORY_APPLICATIONSTATUS, XDG_CATEGORY_COMMUNICATIONS, XDG_CATEGORY_SYSTEMSERVICE, XDG_CATEGORY_HARDWARE}; + const QStringList xdgStatus {XDG_STATUS_PASSIVE, XDG_STATUS_ACTIVE, XDG_STATUS_NEEDSATTENTION}; + + explicit StatusNotifierItem(QObject *parent = nullptr); + + QString Title() const; + void setTitle(const QString &newTitle); + + QString IconName() const; + void setIconName(const QString &newIconName); + + QString Status() const; + void setStatus(const QString &newStatus); + + + QString OverlayIconName() const; + void setOverlayIconName(const QString &newOverlayIconName); + + QString Category() const; + void setCategory(const QString &newCategory); + + QString Id() const; + void setId(const QString &newId); + + QString WindowId() const; + void setWindowId(const QString &newWindowId); + + QString AttentionIconName() const; + void setAttentionIconName(const QString &newAttentionIconName); + + QString AttentionMovieName() const; + void setAttentionMovieName(const QString &newAttentionMovieName); + + public slots: + /* DBus API */ + virtual void Activate(uint x, uint y); + virtual void SecondaryActivate(uint x, uint y); + + signals: + void NewTitle(); + void NewIcon(); + void NewAttentionIcon(); + void NewOverlayIcon(); + void NewToolTip(); + void NewStatus(QString status); + + void TitleChanged(); + void IconNameChanged(); + void StatusChanged(); + void overlayIconNameChanged(); + void CategoryChanged(); + void IdChanged(); + void WindowIdChanged(); + + void activated(uint x, uint y); + void secondaryActivated(uint x, uint y); + + + void AttentionIconNameChanged(); + + void AttentionMovieNameChanged(); + + private: + QString m_Title; + QString m_IconName; + QString m_Status {XDG_STATUS_ACTIVE}; + QString m_OverlayIconName; + QString m_Category {XDG_CATEGORY_APPLICATIONSTATUS}; + QString m_Id; + QString m_WindowId; + QString m_AttentionIconName; + QString m_AttentionMovieName; + }; +}; + +#endif // XDGSTATUSNOTIFIERITEM_H diff --git a/src/lib/xdgstatusnotifierwatcher.cpp b/src/lib/xdgstatusnotifierwatcher.cpp new file mode 100644 index 0000000..c07a0f6 --- /dev/null +++ b/src/lib/xdgstatusnotifierwatcher.cpp @@ -0,0 +1,116 @@ +#include "xdgstatusnotifierwatcher.h" +#include <statusnotifierwatcheradaptor.h> +#include <XdgStatusNotifierItemIface.h> +#include <QDBusConnection> +#include <debug.h> + +using namespace xdg; + +StatusNotifierWatcher::StatusNotifierWatcher(QObject *parent) + : QObject{parent} +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qCWarning(SYSTEM_TRAY) << "Cannot connect to the D-Bus session bus.\n" << "Please check your system settings and try again.\n"; + } + if(!bus.registerService("org.kde.StatusNotifierWatcher")) + { + qCWarning(SYSTEM_TRAY) << "Don't register StatusNotifierWatcher service, probably already exists"; + return; + } + qCDebug(SYSTEM_TRAY) << "Register StatusNotifierWatcher service"; + new StatusNotifierWatcherAdaptor(this); + bus.registerObject("/StatusNotifierWatcher", this); + + m_serviceWatcher = new QDBusServiceWatcher(this); + m_serviceWatcher->setConnection(bus); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &StatusNotifierWatcher::_serviceUnregistered); +} + +StatusNotifierWatcher::~StatusNotifierWatcher() +{ + +} + +void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString &service) +{ + QString path; + path = QStringLiteral("/StatusNotifierItem"); + qCDebug(SYSTEM_TRAY) << "Watcher: RegisterStatusNotifierItem(" << service << ")"; + QString notifierItemId = service + path; + if (m_items.contains(notifierItemId)) { + return; + } + m_serviceWatcher->addWatchedService(service); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(service).value()) { + // check if the service has registered a SystemTray object + org::kde::StatusNotifierItem trayclient(service, path, QDBusConnection::sessionBus()); + if (trayclient.isValid()) { + qDebug() << "Registering" << notifierItemId << "to system tray"; + m_items.append(notifierItemId); + emit StatusNotifierItemRegistered(notifierItemId); + } else { + m_serviceWatcher->removeWatchedService(service); + } + } else { + m_serviceWatcher->removeWatchedService(service); + } +} + +void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString &service) +{ + qCDebug(SYSTEM_TRAY) << "Watcher: RegisterStatusNotifierHost(" << service << ")"; + if (m_hosts.contains(service)) { + return; + } + m_serviceWatcher->addWatchedService(service); + m_hosts.append(service); +} + +QStringList StatusNotifierWatcher::RegisteredStatusNotifierItems() const +{ + return m_items; +} + + +bool StatusNotifierWatcher::IsStatusNotifierHostRegistered() const +{ + return m_hosts.size() > 0; +} + + +int StatusNotifierWatcher::ProtocolVersion() const +{ + return XDG_STATUS_NOTIFIER_WATCHER_PROTOCOL_VERSION; +} + +void StatusNotifierWatcher::_serviceUnregistered(const QString &name) +{ + qCDebug(SYSTEM_TRAY) << "Service " << name << "unregistered (ITEMS:" << m_items << " HOSTS:" << m_hosts << ")"; + m_serviceWatcher->removeWatchedService(name); + + const QString match = name + QLatin1Char('/'); + QStringList::Iterator it = m_items.begin(); + while (it != m_items.end()) { + if (it->startsWith(match)) { + QString name = *it; + it = m_items.erase(it); + emit StatusNotifierItemUnregistered(name); + } else { + ++it; + } + } + + it = m_hosts.begin(); + while (it != m_hosts.end()) { + if (it->startsWith(match)) { + QString name = *it; + it = m_hosts.erase(it); + emit StatusNotifierHostUnregistered(); + } else { + ++it; + } + } +} diff --git a/src/lib/xdgstatusnotifierwatcher.h b/src/lib/xdgstatusnotifierwatcher.h new file mode 100644 index 0000000..8e2275c --- /dev/null +++ b/src/lib/xdgstatusnotifierwatcher.h @@ -0,0 +1,48 @@ +#ifndef XDGSTATUSNOTIFIERWATCHER_H +#define XDGSTATUSNOTIFIERWATCHER_H + +#include <QObject> + +class QDBusServiceWatcher; +#define XDG_STATUS_NOTIFIER_WATCHER_PROTOCOL_VERSION 1 + +namespace xdg { + + class StatusNotifierWatcher: public QObject + { + Q_OBJECT + + Q_PROPERTY(QStringList RegisteredStatusNotifierItems READ RegisteredStatusNotifierItems NOTIFY RegisteredStatusNotifierItemsChanged) + Q_PROPERTY(bool IsStatusNotifierHostRegistered READ IsStatusNotifierHostRegistered NOTIFY IsStatusNotifierHostRegisteredChanged) + Q_PROPERTY(int ProtocolVersion READ ProtocolVersion CONSTANT) + + public: + StatusNotifierWatcher(QObject *parent = nullptr); + virtual ~StatusNotifierWatcher(); + + void RegisterStatusNotifierItem(const QString &service); + void RegisterStatusNotifierHost(const QString &service); + + QStringList RegisteredStatusNotifierItems() const; + bool IsStatusNotifierHostRegistered() const; + int ProtocolVersion() const; + + signals: + void StatusNotifierItemRegistered(QString id); + void StatusNotifierItemUnregistered(QString id); + void StatusNotifierHostRegistered(); + void StatusNotifierHostUnregistered(); + void RegisteredStatusNotifierItemsChanged(); + void IsStatusNotifierHostRegisteredChanged(); + + private: + QStringList m_hosts; + QStringList m_items; + QDBusServiceWatcher *m_serviceWatcher = nullptr; + + private slots: + void _serviceUnregistered(const QString &name); + }; +}; + +#endif // XDGSTATUSNOTIFIERWATCHER_H diff --git a/src/test/test_xdg.cpp b/src/test/test_xdg.cpp new file mode 100644 index 0000000..44c6dc2 --- /dev/null +++ b/src/test/test_xdg.cpp @@ -0,0 +1,31 @@ +#include <QCoreApplication> +#include <QDebug> +#include <QTimer> +#include "xdgdesktopentries.h" +#include "xdgentry.h" +#include "xdgentries.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + + xdg::Entries entries; + entries.addDirectory("/usr/share/applications"); +/* + QTimer t; + QObject::connect(&t, &QTimer::timeout, [](){ + XdgDesktopEntries ent; + for(int i = 0; i < ent.rowCount(); i++) + { + QVariant data = ent.data(ent.index(i), XdgDesktopEntries::DesktopFileRole); + qDebug() << data.toString(); + } + //QCoreApplication::exit(0); + }); + + t.setInterval(0); + t.setSingleShot(true); + t.start(); + */ + return a.exec(); +} |