Compare commits
3 commits
6a471bd822
...
47ab4cfcec
Author | SHA1 | Date | |
---|---|---|---|
outfoxxed | 47ab4cfcec | ||
24f54f579f | |||
outfoxxed | 497c9c4e50 |
|
@ -25,6 +25,8 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
|||
option(SERVICE_MPRIS "Mpris service" ON)
|
||||
option(SERVICE_PAM "Pam service" ON)
|
||||
option(SERVICE_GREETD "Greet service" ON)
|
||||
option(SERVICE_UPOWER "UPower service" ON)
|
||||
option(SERVICE_NOTIFICATIONS "Notification server" ON)
|
||||
|
||||
message(STATUS "Quickshell configuration")
|
||||
message(STATUS " Jemalloc: ${USE_JEMALLOC}")
|
||||
|
@ -43,6 +45,8 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
|||
message(STATUS " Mpris: ${SERVICE_MPRIS}")
|
||||
message(STATUS " Pam: ${SERVICE_PAM}")
|
||||
message(STATUS " Greetd: ${SERVICE_GREETD}")
|
||||
message(STATUS " UPower: ${SERVICE_UPOWER}")
|
||||
message(STATUS " Notifications: ${SERVICE_NOTIFICATIONS}")
|
||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||
if (HYPRLAND)
|
||||
message(STATUS " IPC: ${HYPRLAND_IPC}")
|
||||
|
|
|
@ -37,7 +37,7 @@ qt_add_library(quickshell-core STATIC
|
|||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
|
||||
|
||||
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
|
||||
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate)
|
||||
qs_pch(quickshell-core)
|
||||
|
||||
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)
|
||||
|
|
|
@ -72,3 +72,8 @@ bool UntypedObjectModel::removeObject(const QObject* object) {
|
|||
}
|
||||
|
||||
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
|
||||
|
||||
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
|
||||
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,8 @@ public:
|
|||
|
||||
Q_INVOKABLE qsizetype indexOf(QObject* object);
|
||||
|
||||
static UntypedObjectModel* emptyInstance();
|
||||
|
||||
signals:
|
||||
void valuesChanged();
|
||||
/// Sent immediately before an object is inserted into the list.
|
||||
|
@ -82,6 +84,10 @@ class ObjectModel: public UntypedObjectModel {
|
|||
public:
|
||||
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
|
||||
|
||||
[[nodiscard]] QVector<T*>& valueList() {
|
||||
return *reinterpret_cast<QVector<T*>*>(&this->valuesList); // NOLINT
|
||||
}
|
||||
|
||||
[[nodiscard]] const QVector<T*>& valueList() const {
|
||||
return *reinterpret_cast<const QVector<T*>*>(&this->valuesList); // NOLINT
|
||||
}
|
||||
|
@ -91,4 +97,8 @@ public:
|
|||
}
|
||||
|
||||
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
|
||||
|
||||
static ObjectModel<T>* emptyInstance() {
|
||||
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qwindow.h>
|
||||
#include <private/qquickwindow_p.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
|
@ -182,6 +183,7 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
|
|||
|
||||
if (visible) {
|
||||
this->createWindow();
|
||||
this->polishItems();
|
||||
this->window->setVisible(true);
|
||||
emit this->backerVisibilityChanged();
|
||||
} else {
|
||||
|
@ -192,11 +194,22 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
|
|||
}
|
||||
}
|
||||
} else if (this->window != nullptr) {
|
||||
if (visible) this->polishItems();
|
||||
this->window->setVisible(visible);
|
||||
emit this->backerVisibilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::polishItems() {
|
||||
// Due to QTBUG-126704, layouts in invisible windows don't update their dimensions.
|
||||
// Usually this isn't an issue, but it is when the size of a window is based on the size
|
||||
// of its content, and that content is in a layout.
|
||||
//
|
||||
// This hack manually polishes the item tree right before showing the window so it will
|
||||
// always be created with the correct size.
|
||||
QQuickWindowPrivate::get(this->window)->polishItems();
|
||||
}
|
||||
|
||||
qint32 ProxyWindowBase::x() const {
|
||||
if (this->window == nullptr) return 0;
|
||||
else return this->window->x();
|
||||
|
|
|
@ -133,5 +133,6 @@ protected:
|
|||
bool reloadComplete = false;
|
||||
|
||||
private:
|
||||
void polishItems();
|
||||
void updateMask();
|
||||
};
|
||||
|
|
|
@ -17,3 +17,11 @@ endif()
|
|||
if (SERVICE_GREETD)
|
||||
add_subdirectory(greetd)
|
||||
endif()
|
||||
|
||||
if (SERVICE_UPOWER)
|
||||
add_subdirectory(upower)
|
||||
endif()
|
||||
|
||||
if (SERVICE_NOTIFICATIONS)
|
||||
add_subdirectory(notifications)
|
||||
endif()
|
||||
|
|
29
src/services/notifications/CMakeLists.txt
Normal file
29
src/services/notifications/CMakeLists.txt
Normal file
|
@ -0,0 +1,29 @@
|
|||
qt_add_dbus_adaptor(DBUS_INTERFACES
|
||||
org.freedesktop.Notifications.xml
|
||||
server.hpp
|
||||
qs::service::notifications::NotificationServer
|
||||
dbus_notifications
|
||||
DBusNotificationServer
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-service-notifications STATIC
|
||||
server.cpp
|
||||
notification.cpp
|
||||
dbusimage.cpp
|
||||
qml.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
# dbus headers
|
||||
target_include_directories(quickshell-service-notifications PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
qt_add_qml_module(quickshell-service-notifications
|
||||
URI Quickshell.Services.Notifications
|
||||
VERSION 0.1
|
||||
)
|
||||
|
||||
target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus)
|
||||
target_link_libraries(quickshell PRIVATE quickshell-service-notificationsplugin)
|
||||
|
||||
qs_pch(quickshell-service-notifications)
|
||||
qs_pch(quickshell-service-notificationsplugin)
|
72
src/services/notifications/dbusimage.cpp
Normal file
72
src/services/notifications/dbusimage.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#include "dbusimage.hpp"
|
||||
#include <QtCore/qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qquickimageprovider.h>
|
||||
#include <qsysinfo.h>
|
||||
#include <qtypes.h>
|
||||
#include <qimage.h>
|
||||
#include <qdbusargument.h>
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp
|
||||
|
||||
QImage DBusNotificationImage::createImage() const {
|
||||
auto format = this->hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
|
||||
|
||||
return QImage(
|
||||
reinterpret_cast<const uchar*>(this->data.data()), // NOLINT
|
||||
this->width,
|
||||
this->height,
|
||||
format
|
||||
);
|
||||
}
|
||||
|
||||
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap) {
|
||||
argument.beginStructure();
|
||||
argument >> pixmap.width;
|
||||
argument >> pixmap.height;
|
||||
auto rowstride = qdbus_cast<qint32>(argument);
|
||||
argument >> pixmap.hasAlpha;
|
||||
auto sampleBits = qdbus_cast<qint32>(argument);
|
||||
auto channels = qdbus_cast<qint32>(argument);
|
||||
argument >> pixmap.data;
|
||||
argument.endStructure();
|
||||
|
||||
if (sampleBits != 8) {
|
||||
qCWarning(logNotifications) << "Unable to parse pixmap as sample count is incorrect. Got" << sampleBits << "expected" << 8;
|
||||
} else if (channels != (pixmap.hasAlpha ? 4 : 3)) {
|
||||
qCWarning(logNotifications) << "Unable to parse pixmap as channel count is incorrect."
|
||||
<< "Got " << channels << "expected" << (pixmap.hasAlpha ? 4 : 3);
|
||||
} else if (rowstride != pixmap.width * sampleBits * channels) {
|
||||
qCWarning(logNotifications) << "Unable to parse pixmap as rowstride is incorrect. Got"
|
||||
<< rowstride << "expected"
|
||||
<< (pixmap.width * sampleBits * channels);
|
||||
}
|
||||
|
||||
return argument;
|
||||
}
|
||||
|
||||
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap) {
|
||||
argument.beginStructure();
|
||||
argument << pixmap.width;
|
||||
argument << pixmap.height;
|
||||
argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3) * 8;
|
||||
argument << pixmap.hasAlpha;
|
||||
argument << 8;
|
||||
argument << (pixmap.hasAlpha ? 4 : 3);
|
||||
argument << pixmap.data;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
QImage
|
||||
NotificationImage::requestImage(const QString& /*unused*/, QSize* size, const QSize& /*unused*/) {
|
||||
auto image = this->image.createImage();
|
||||
|
||||
if (size != nullptr) *size = image.size();
|
||||
return image;
|
||||
}
|
||||
|
||||
}
|
34
src/services/notifications/dbusimage.hpp
Normal file
34
src/services/notifications/dbusimage.hpp
Normal file
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <qimage.h>
|
||||
#include <qdbusargument.h>
|
||||
#include <qobject.h>
|
||||
#include "../../core/imageprovider.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
struct DBusNotificationImage {
|
||||
qint32 width = 0;
|
||||
qint32 height = 0;
|
||||
bool hasAlpha = false;
|
||||
QByteArray data;
|
||||
|
||||
// valid only for the lifetime of the pixmap
|
||||
[[nodiscard]] QImage createImage() const;
|
||||
};
|
||||
|
||||
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap);
|
||||
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap);
|
||||
|
||||
class NotificationImage: public QsImageHandle {
|
||||
public:
|
||||
explicit NotificationImage(DBusNotificationImage image, QObject* parent)
|
||||
: QsImageHandle(QQuickAsyncImageProvider::Image, parent)
|
||||
, image(std::move(image)) {}
|
||||
|
||||
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
|
||||
DBusNotificationImage image;
|
||||
};
|
||||
}
|
257
src/services/notifications/notification.cpp
Normal file
257
src/services/notifications/notification.cpp
Normal file
|
@ -0,0 +1,257 @@
|
|||
#include "notification.hpp"
|
||||
#include <utility>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusargument.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include "dbusimage.hpp"
|
||||
#include "server.hpp"
|
||||
#include "../../core/iconimageprovider.hpp"
|
||||
#include "../../core/desktopentry.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp
|
||||
|
||||
QString NotificationUrgency::toString(NotificationUrgency::Enum value) {
|
||||
switch (value) {
|
||||
case NotificationUrgency::Low: return "Low";
|
||||
case NotificationUrgency::Normal: return "Normal";
|
||||
case NotificationUrgency::Critical: return "Critical";
|
||||
default: return "Invalid notification urgency";
|
||||
}
|
||||
}
|
||||
|
||||
QString NotificationCloseReason::toString(NotificationCloseReason::Enum value) {
|
||||
switch (value) {
|
||||
case NotificationCloseReason::Expired: return "Expired";
|
||||
case NotificationCloseReason::Dismissed: return "Dismissed";
|
||||
case NotificationCloseReason::CloseRequested: return "CloseRequested";
|
||||
default: return "Invalid notification close reason";
|
||||
}
|
||||
}
|
||||
|
||||
QString NotificationAction::identifier() const { return this->mIdentifier; }
|
||||
QString NotificationAction::text() const { return this->mText; }
|
||||
|
||||
void NotificationAction::invoke() {
|
||||
NotificationServer::instance()->ActionInvoked(this->notification->id(), this->mIdentifier);
|
||||
|
||||
if (!this->notification->isResident()) {
|
||||
this->notification->close(NotificationCloseReason::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationAction::setText(const QString& text) {
|
||||
if (text != this->mText) return;
|
||||
|
||||
this->mText = text;
|
||||
emit this->textChanged();
|
||||
}
|
||||
|
||||
void Notification::expire() {
|
||||
this->close(NotificationCloseReason::Expired);
|
||||
}
|
||||
|
||||
void Notification::dismiss() {
|
||||
this->close(NotificationCloseReason::Dismissed);
|
||||
}
|
||||
|
||||
void Notification::close(NotificationCloseReason::Enum reason) {
|
||||
this->mCloseReason = reason;
|
||||
|
||||
if (reason != 0) {
|
||||
NotificationServer::instance()->deleteNotification(this, reason);
|
||||
}
|
||||
}
|
||||
|
||||
void Notification::updateProperties(
|
||||
const QString& appName,
|
||||
QString appIcon,
|
||||
const QString& summary,
|
||||
const QString& body,
|
||||
const QStringList& actions,
|
||||
QVariantMap hints,
|
||||
qint32 expireTimeout
|
||||
) {
|
||||
auto urgency = hints.contains("urgency") ? hints.value("urgency").value<quint8>() : NotificationUrgency::Normal;
|
||||
auto hasActionIcons = hints.value("action-icons").value<bool>();
|
||||
auto isResident = hints.value("resident").value<bool>();
|
||||
auto isTransient = hints.value("transient").value<bool>();
|
||||
auto desktopEntry = hints.value("desktop-entry").value<QString>();
|
||||
|
||||
QString imageDataName;
|
||||
if (hints.contains("image-data")) imageDataName = "image-data";
|
||||
else if (hints.contains("image_data")) imageDataName = "image_data";
|
||||
else if (hints.contains("icon_data")) imageDataName = "icon_data";
|
||||
|
||||
NotificationImage* imagePixmap = nullptr;
|
||||
if (!imageDataName.isEmpty()) {
|
||||
auto value = hints.value(imageDataName).value<QDBusArgument>();
|
||||
DBusNotificationImage image;
|
||||
value >> image;
|
||||
imagePixmap = new NotificationImage(std::move(image), this);
|
||||
}
|
||||
|
||||
// don't store giant byte arrays more than necessary
|
||||
hints.remove("image-data");
|
||||
hints.remove("image_data");
|
||||
hints.remove("icon_data");
|
||||
|
||||
QString imagePath;
|
||||
if (!imagePixmap) {
|
||||
QString imagePathName;
|
||||
if (hints.contains("image-path")) imagePathName = "image-path";
|
||||
else if (hints.contains("image_path")) imagePathName = "image_path";
|
||||
|
||||
if (!imagePathName.isEmpty()) {
|
||||
imagePath = hints.value(imagePathName).value<QString>();
|
||||
|
||||
if (!imagePath.startsWith("file:")) {
|
||||
imagePath = IconImageProvider::requestString(imagePath, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appIcon.isEmpty() && !desktopEntry.isEmpty()) {
|
||||
if (auto* entry = DesktopEntryManager::instance()->byId(desktopEntry)) {
|
||||
appIcon = entry->mIcon;
|
||||
}
|
||||
}
|
||||
|
||||
auto appNameChanged = appName != this->mAppName;
|
||||
auto appIconChanged = appIcon != this->mAppIcon;
|
||||
auto summaryChanged = summary != this->mSummary;
|
||||
auto bodyChanged = body != this->mBody;
|
||||
auto expireTimeoutChanged = expireTimeout != this->mExpireTimeout;
|
||||
auto urgencyChanged = urgency != this->mUrgency;
|
||||
auto hasActionIconsChanged = hasActionIcons != this->mHasActionIcons;
|
||||
auto isResidentChanged = isResident != this->mIsResident;
|
||||
auto isTransientChanged = isTransient != this->mIsTransient;
|
||||
auto desktopEntryChanged = desktopEntry != this->mDesktopEntry;
|
||||
auto imageChanged = imagePixmap || imagePath != this->mImagePath;
|
||||
auto hintsChanged = hints != this->mHints;
|
||||
|
||||
if (appNameChanged) this->mAppName = appName;
|
||||
if (appIconChanged) this->mAppIcon = appIcon;
|
||||
if (summaryChanged) this->mSummary = summary;
|
||||
if (bodyChanged) this->mBody = body;
|
||||
if (expireTimeoutChanged) this->mExpireTimeout = expireTimeout;
|
||||
if (urgencyChanged) this->mUrgency = static_cast<NotificationUrgency::Enum>(urgency);
|
||||
if (hasActionIcons) this->mHasActionIcons = hasActionIcons;
|
||||
if (isResidentChanged) this->mIsResident = isResident;
|
||||
if (isTransientChanged) this->mIsTransient = isTransient;
|
||||
if (desktopEntryChanged) this->mDesktopEntry = desktopEntry;
|
||||
|
||||
NotificationImage* oldImage = nullptr;
|
||||
|
||||
if (imageChanged) {
|
||||
oldImage = this->mImagePixmap;
|
||||
this->mImagePixmap = imagePixmap;
|
||||
this->mImagePath = imagePath;
|
||||
}
|
||||
|
||||
if (hintsChanged) this->mHints = hints;
|
||||
|
||||
bool actionsChanged = false;
|
||||
auto deletedActions = QVector<NotificationAction*>();
|
||||
|
||||
if (actions.length() % 2 == 0) {
|
||||
int ai = 0;
|
||||
for (auto i = 0; i != actions.length(); i += 2) {
|
||||
ai = i / 2;
|
||||
const auto& identifier = actions.at(i);
|
||||
const auto& text = actions.at(i + 1);
|
||||
auto* action = ai < this->mActions.length() ? this->mActions.at(ai) : nullptr;
|
||||
|
||||
if (action && identifier == action->identifier()) {
|
||||
action->setText(text);
|
||||
} else {
|
||||
auto* newAction = new NotificationAction(identifier, text, this);
|
||||
|
||||
if (action) {
|
||||
deletedActions.push_back(action);
|
||||
this->mActions.replace(ai, newAction);
|
||||
} else {
|
||||
this->mActions.push_back(newAction);
|
||||
}
|
||||
|
||||
actionsChanged = true;
|
||||
}
|
||||
|
||||
ai++;
|
||||
}
|
||||
|
||||
for (auto i = this->mActions.length(); i > ai; i--) {
|
||||
deletedActions.push_back(this->mActions.at(i - 1));
|
||||
this->mActions.remove(i - 1);
|
||||
actionsChanged = true;
|
||||
}
|
||||
} else {
|
||||
qCWarning(logNotifications) << this << '(' << appName << ')' << "sent an action set of an invalid length.";
|
||||
}
|
||||
|
||||
if (appNameChanged) emit this->appNameChanged();
|
||||
if (appIconChanged) emit this->appIconChanged();
|
||||
if (summaryChanged) emit this->summaryChanged();
|
||||
if (bodyChanged) emit this->bodyChanged();
|
||||
if (expireTimeoutChanged) emit this->expireTimeoutChanged();
|
||||
if (urgencyChanged) emit this->urgencyChanged();
|
||||
if (actionsChanged) emit this->actionsChanged();
|
||||
if (hasActionIconsChanged) emit this->hasActionIconsChanged();
|
||||
if (isResidentChanged) emit this->isResidentChanged();
|
||||
if (isTransientChanged) emit this->isTransientChanged();
|
||||
if (desktopEntryChanged) emit this->desktopEntryChanged();
|
||||
if (imageChanged) emit this->imageChanged();
|
||||
if (hintsChanged) emit this->hintsChanged();
|
||||
|
||||
for (auto* action: deletedActions) {
|
||||
delete action;
|
||||
}
|
||||
|
||||
delete oldImage;
|
||||
}
|
||||
|
||||
quint32 Notification::id() const { return this->mId; }
|
||||
bool Notification::isTracked() const { return this->mCloseReason == 0; }
|
||||
NotificationCloseReason::Enum Notification::closeReason() const { return this->mCloseReason; }
|
||||
|
||||
void Notification::setTracked(bool tracked) {
|
||||
this->close(
|
||||
tracked ? static_cast<NotificationCloseReason::Enum>(0) : NotificationCloseReason::Dismissed
|
||||
);
|
||||
}
|
||||
|
||||
bool Notification::isLastGeneration() const {
|
||||
return this->mLastGeneration;
|
||||
}
|
||||
|
||||
void Notification::setLastGeneration() {
|
||||
this->mLastGeneration = true;
|
||||
}
|
||||
|
||||
qreal Notification::expireTimeout() const { return this->mExpireTimeout; }
|
||||
QString Notification::appName() const { return this->mAppName; }
|
||||
QString Notification::appIcon() const { return this->mAppIcon; }
|
||||
QString Notification::summary() const { return this->mSummary; }
|
||||
QString Notification::body() const { return this->mBody; }
|
||||
NotificationUrgency::Enum Notification::urgency() const { return this->mUrgency; }
|
||||
QVector<NotificationAction*> Notification::actions() const { return this->mActions; }
|
||||
bool Notification::hasActionIcons() const { return this->mHasActionIcons; }
|
||||
bool Notification::isResident() const { return this->mIsResident; }
|
||||
bool Notification::isTransient() const { return this->mIsTransient; }
|
||||
QString Notification::desktopEntry() const { return this->mDesktopEntry; }
|
||||
|
||||
QString Notification::image() const {
|
||||
if (this->mImagePixmap) {
|
||||
return this->mImagePixmap->url();
|
||||
} else {
|
||||
return this->mImagePath;
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap Notification::hints() const { return this->mHints; }
|
||||
|
||||
}
|
221
src/services/notifications/notification.hpp
Normal file
221
src/services/notifications/notification.hpp
Normal file
|
@ -0,0 +1,221 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <QtCore/qtmetamacros.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
class NotificationImage;
|
||||
|
||||
class NotificationUrgency: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
Low = 0,
|
||||
Normal = 1,
|
||||
Critical = 2,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(NotificationUrgency::Enum value);
|
||||
};
|
||||
|
||||
class NotificationCloseReason: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
/// The notification expired due to a timeout.
|
||||
Expired = 1,
|
||||
/// The notification was explicitly dismissed by the user.
|
||||
Dismissed = 2,
|
||||
/// The remote application requested the notification be removed.
|
||||
CloseRequested = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(NotificationCloseReason::Enum value);
|
||||
};
|
||||
|
||||
class NotificationAction;
|
||||
|
||||
class Notification: public QObject {
|
||||
Q_OBJECT;
|
||||
Q_PROPERTY(quint32 id READ id CONSTANT);
|
||||
/// If the notification is tracked by the notification server.
|
||||
///
|
||||
/// Setting this property to false is equivalent to calling `dismiss()`.
|
||||
Q_PROPERTY(bool tracked READ isTracked WRITE setTracked NOTIFY trackedChanged);
|
||||
/// If this notification was carried over from the last generation
|
||||
/// when quickshell reloaded.
|
||||
///
|
||||
/// Notifications from the last generation will only be emitted if
|
||||
/// [NotificationServer.keepOnReload](../notificationserver#prop.keepOnReload) is true.
|
||||
Q_PROPERTY(bool lastGeneration READ isLastGeneration CONSTANT);
|
||||
/// Time in seconds the notification should be valid for
|
||||
Q_PROPERTY(qreal expireTimeout READ expireTimeout NOTIFY expireTimeoutChanged);
|
||||
/// The sending application's name.
|
||||
Q_PROPERTY(QString appName READ appName NOTIFY appNameChanged);
|
||||
/// The sending application's icon. If none was provided, then the icon from an associated
|
||||
/// desktop entry will be retrieved. If none was found then "".
|
||||
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged);
|
||||
/// The image associated with this notification, or "" if none.
|
||||
Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged);
|
||||
Q_PROPERTY(QString body READ body NOTIFY bodyChanged);
|
||||
Q_PROPERTY(NotificationUrgency::Enum urgency READ urgency NOTIFY urgencyChanged);
|
||||
/// Actions that can be taken for this notification.
|
||||
Q_PROPERTY(QVector<NotificationAction*> actions READ actions NOTIFY actionsChanged);
|
||||
/// If actions associated with this notification have icons available.
|
||||
///
|
||||
/// See [NotificationAction.identifier](../notificationaction#prop.identifier) for details.
|
||||
Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged);
|
||||
/// If true, the notification will not be destroyed after an action is invoked.
|
||||
Q_PROPERTY(bool resident READ isResident NOTIFY isResidentChanged);
|
||||
/// If true, the notification should skip any kind of persistence function like a notification area.
|
||||
Q_PROPERTY(bool transient READ isTransient NOTIFY isTransientChanged);
|
||||
/// The name of the sender's desktop entry or "" if none was supplied.
|
||||
Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged);
|
||||
/// An image associated with the notification.
|
||||
///
|
||||
/// This image is often something like a profile picture in instant messaging applications.
|
||||
Q_PROPERTY(QString image READ image NOTIFY imageChanged);
|
||||
/// All hints sent by the client application. Many common hints are exposed via other properties.
|
||||
Q_PROPERTY(QVariantMap hints READ hints NOTIFY hintsChanged);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("Notifications must be acquired from a NotificationServer");
|
||||
|
||||
public:
|
||||
explicit Notification(quint32 id, QObject* parent): QObject(parent), mId(id) {}
|
||||
|
||||
/// Destroy the notification and hint to the remote application that it has
|
||||
/// timed out an expired.
|
||||
Q_INVOKABLE void expire();
|
||||
/// Destroy the notification and hint to the remote application that it was
|
||||
/// explicitly closed by the user.
|
||||
Q_INVOKABLE void dismiss();
|
||||
|
||||
void updateProperties(
|
||||
const QString& appName,
|
||||
QString appIcon,
|
||||
const QString& summary,
|
||||
const QString& body,
|
||||
const QStringList& actions,
|
||||
QVariantMap hints,
|
||||
qint32 expireTimeout
|
||||
);
|
||||
|
||||
void close(NotificationCloseReason::Enum reason);
|
||||
|
||||
[[nodiscard]] quint32 id() const;
|
||||
|
||||
[[nodiscard]] bool isTracked() const;
|
||||
[[nodiscard]] NotificationCloseReason::Enum closeReason() const;
|
||||
void setTracked(bool tracked);
|
||||
|
||||
[[nodiscard]] bool isLastGeneration() const;
|
||||
void setLastGeneration();
|
||||
|
||||
[[nodiscard]] qreal expireTimeout() const;
|
||||
[[nodiscard]] QString appName() const;
|
||||
[[nodiscard]] QString appIcon() const;
|
||||
[[nodiscard]] QString summary() const;
|
||||
[[nodiscard]] QString body() const;
|
||||
[[nodiscard]] NotificationUrgency::Enum urgency() const;
|
||||
[[nodiscard]] QVector<NotificationAction*> actions() const;
|
||||
[[nodiscard]] bool hasActionIcons() const;
|
||||
[[nodiscard]] bool isResident() const;
|
||||
[[nodiscard]] bool isTransient() const;
|
||||
[[nodiscard]] QString desktopEntry() const;
|
||||
[[nodiscard]] QString image() const;
|
||||
[[nodiscard]] QVariantMap hints() const;
|
||||
|
||||
signals:
|
||||
/// Sent when a notification has been closed.
|
||||
///
|
||||
/// The notification object will be destroyed as soon as all signal handlers exit.
|
||||
void closed(NotificationCloseReason::Enum reason);
|
||||
|
||||
void trackedChanged();
|
||||
void expireTimeoutChanged();
|
||||
void appNameChanged();
|
||||
void appIconChanged();
|
||||
void summaryChanged();
|
||||
void bodyChanged();
|
||||
void urgencyChanged();
|
||||
void actionsChanged();
|
||||
void hasActionIconsChanged();
|
||||
void isResidentChanged();
|
||||
void isTransientChanged();
|
||||
void desktopEntryChanged();
|
||||
void imageChanged();
|
||||
void hintsChanged();
|
||||
|
||||
private:
|
||||
quint32 mId;
|
||||
NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed;
|
||||
bool mLastGeneration = false;
|
||||
qreal mExpireTimeout = 0;
|
||||
QString mAppName;
|
||||
QString mAppIcon;
|
||||
QString mSummary;
|
||||
QString mBody;
|
||||
NotificationUrgency::Enum mUrgency = NotificationUrgency::Normal;
|
||||
QVector<NotificationAction*> mActions;
|
||||
bool mHasActionIcons = false;
|
||||
bool mIsResident = false;
|
||||
bool mIsTransient = false;
|
||||
QString mImagePath;
|
||||
NotificationImage* mImagePixmap = nullptr;
|
||||
QString mDesktopEntry;
|
||||
QVariantMap mHints;
|
||||
};
|
||||
|
||||
class NotificationAction: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The identifier of the action.
|
||||
///
|
||||
/// When [Notification.hasActionIcons] is true, this property will be an icon name.
|
||||
/// When it is false, this property is irrelevant.
|
||||
///
|
||||
/// [Notification.hasActionIcons]: ../notification#prop.hasActionIcons
|
||||
Q_PROPERTY(QString identifier READ identifier CONSTANT);
|
||||
/// The localized text that should be displayed on a button.
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("NotificationActions must be acquired from a Notification");
|
||||
|
||||
public:
|
||||
explicit NotificationAction(QString identifier, QString text, Notification* notification)
|
||||
: QObject(notification)
|
||||
, notification(notification)
|
||||
, mIdentifier(std::move(identifier))
|
||||
, mText(std::move(text)) {}
|
||||
|
||||
/// Invoke the action. If [Notification.resident] is false it will be dismissed.
|
||||
///
|
||||
/// [Notification.resident]: ../notification#prop.resident
|
||||
Q_INVOKABLE void invoke();
|
||||
|
||||
[[nodiscard]] QString identifier() const;
|
||||
[[nodiscard]] QString text() const;
|
||||
void setText(const QString& text);
|
||||
|
||||
signals:
|
||||
void textChanged();
|
||||
|
||||
private:
|
||||
Notification* notification;
|
||||
QString mIdentifier;
|
||||
QString mText;
|
||||
};
|
||||
|
||||
}
|
46
src/services/notifications/org.freedesktop.Notifications.xml
Normal file
46
src/services/notifications/org.freedesktop.Notifications.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<node>
|
||||
<interface name="org.freedesktop.Notifications">
|
||||
<method name="GetCapabilities">
|
||||
<arg name="capabilities" type="as" direction="out"/>
|
||||
</method>
|
||||
|
||||
<method name="Notify">
|
||||
<arg name="appName" type="s" direction="in"/>
|
||||
<arg name="replacesId" type="u" direction="in"/>
|
||||
<arg name="appIcon" 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="expireTimeout" type="i" direction="in"/>
|
||||
<arg name="id" type="u" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
|
||||
</method>
|
||||
|
||||
<method name="CloseNotification">
|
||||
<arg name="id" type="u" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="GetServerInformation">
|
||||
<arg name="name" type="s" direction="out"/>
|
||||
<arg name="vendor" type="s" direction="out"/>
|
||||
<arg name="version" type="s" direction="out"/>
|
||||
<arg name="specVersion" type="s" direction="out"/>
|
||||
</method>
|
||||
|
||||
<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="actionKey" type="s" direction="out"/>
|
||||
</signal>
|
||||
|
||||
<signal name="ActivationToken">
|
||||
<arg name="id" type="u" direction="out"/>
|
||||
<arg name="activationToken" type="s" direction="out"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
139
src/services/notifications/qml.cpp
Normal file
139
src/services/notifications/qml.cpp
Normal file
|
@ -0,0 +1,139 @@
|
|||
#include "qml.hpp"
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include "notification.hpp"
|
||||
#include "server.hpp"
|
||||
#include "../../core/model.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
void NotificationServerQml::onPostReload() {
|
||||
auto* instance = NotificationServer::instance();
|
||||
instance->support = this->support;
|
||||
|
||||
QObject::connect(
|
||||
instance,
|
||||
&NotificationServer::notification,
|
||||
this,
|
||||
&NotificationServerQml::notification
|
||||
);
|
||||
|
||||
instance->switchGeneration(this->mKeepOnReload, [this]() {
|
||||
this->live = true;
|
||||
emit this->trackedNotificationsChanged();
|
||||
});
|
||||
}
|
||||
|
||||
bool NotificationServerQml::keepOnReload() const {
|
||||
return this->mKeepOnReload;
|
||||
}
|
||||
|
||||
void NotificationServerQml::setKeepOnReload(bool keepOnReload) {
|
||||
if (keepOnReload == this->mKeepOnReload) return;
|
||||
|
||||
if (this->live) {
|
||||
qCritical() << "Cannot set NotificationServer.keepOnReload after the server has been started.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->mKeepOnReload = keepOnReload;
|
||||
emit this->keepOnReloadChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::persistenceSupported() const { return this->support.persistence; }
|
||||
|
||||
void NotificationServerQml::setPersistenceSupported(bool persistenceSupported) {
|
||||
if (persistenceSupported == this->support.persistence) return;
|
||||
this->support.persistence = persistenceSupported;
|
||||
this->updateSupported();
|
||||
emit this->persistenceSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::bodySupported() const { return this->support.body; }
|
||||
|
||||
void NotificationServerQml::setBodySupported(bool bodySupported) {
|
||||
if (bodySupported == this->support.body) return;
|
||||
this->support.body = bodySupported;
|
||||
this->updateSupported();
|
||||
emit this->bodySupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::bodyMarkupSupported() const { return this->support.bodyMarkup; }
|
||||
|
||||
void NotificationServerQml::setBodyMarkupSupported(bool bodyMarkupSupported) {
|
||||
if (bodyMarkupSupported == this->support.bodyMarkup) return;
|
||||
this->support.bodyMarkup = bodyMarkupSupported;
|
||||
this->updateSupported();
|
||||
emit this->bodyMarkupSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::bodyHyperlinksSupported() const { return this->support.bodyHyperlinks; }
|
||||
|
||||
void NotificationServerQml::setBodyHyperlinksSupported(bool bodyHyperlinksSupported) {
|
||||
if (bodyHyperlinksSupported == this->support.bodyHyperlinks) return;
|
||||
this->support.bodyHyperlinks = bodyHyperlinksSupported;
|
||||
this->updateSupported();
|
||||
emit this->bodyHyperlinksSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::bodyImagesSupported() const { return this->support.bodyImages; }
|
||||
|
||||
void NotificationServerQml::setBodyImagesSupported(bool bodyImagesSupported) {
|
||||
if (bodyImagesSupported == this->support.bodyImages) return;
|
||||
this->support.bodyImages = bodyImagesSupported;
|
||||
this->updateSupported();
|
||||
emit this->bodyImagesSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::actionsSupported() const { return this->support.actions; }
|
||||
|
||||
void NotificationServerQml::setActionsSupported(bool actionsSupported) {
|
||||
if (actionsSupported == this->support.actions) return;
|
||||
this->support.actions = actionsSupported;
|
||||
this->updateSupported();
|
||||
emit this->actionsSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::actionIconsSupported() const { return this->support.actionIcons; }
|
||||
|
||||
void NotificationServerQml::setActionIconsSupported(bool actionIconsSupported) {
|
||||
if (actionIconsSupported == this->support.actionIcons) return;
|
||||
this->support.actionIcons = actionIconsSupported;
|
||||
this->updateSupported();
|
||||
emit this->actionIconsSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::imageSupported() const { return this->support.image; }
|
||||
|
||||
void NotificationServerQml::setImageSupported(bool imageSupported) {
|
||||
if (imageSupported == this->support.image) return;
|
||||
this->support.image = imageSupported;
|
||||
this->updateSupported();
|
||||
emit this->imageSupportedChanged();
|
||||
}
|
||||
|
||||
bool NotificationServerQml::soundSupported() const { return this->support.sound; }
|
||||
|
||||
void NotificationServerQml::setSoundSupported(bool soundSupported) {
|
||||
if (soundSupported == this->support.sound) return;
|
||||
this->support.sound = soundSupported;
|
||||
this->updateSupported();
|
||||
emit this->soundSupportedChanged();
|
||||
}
|
||||
|
||||
ObjectModel<Notification>* NotificationServerQml::trackedNotifications() const {
|
||||
if (this->live) {
|
||||
return NotificationServer::instance()->trackedNotifications();
|
||||
} else {
|
||||
return ObjectModel<Notification>::emptyInstance();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationServerQml::updateSupported() {
|
||||
if (this->live) {
|
||||
NotificationServer::instance()->support = this->support;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
133
src/services/notifications/qml.hpp
Normal file
133
src/services/notifications/qml.hpp
Normal file
|
@ -0,0 +1,133 @@
|
|||
#pragma once
|
||||
|
||||
#include <QtCore/qtmetamacros.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include "notification.hpp"
|
||||
#include "../../core/reload.hpp"
|
||||
#include "../../core/model.hpp"
|
||||
#include "server.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
///! Desktop Notifications Server
|
||||
/// An implementation of the [Desktop Notifications Specification] for receiving notifications
|
||||
/// from external applications.
|
||||
///
|
||||
/// The server does not advertise most capabilities by default. See the individual properties for details.
|
||||
class NotificationServerQml
|
||||
: public QObject
|
||||
, public PostReloadHook {
|
||||
Q_OBJECT;
|
||||
/// If notifications should be re-emitted when quickshell reloads. Defaults to true.
|
||||
///
|
||||
/// The [lastGeneration](../notification#prop.lastGeneration) flag will be
|
||||
/// set on notifications from the prior generation for further filtering/handling.
|
||||
Q_PROPERTY(bool keepOnReload READ keepOnReload WRITE setKeepOnReload NOTIFY keepOnReloadChanged);
|
||||
/// If the notification server should advertise that it can persist notifications in the background
|
||||
/// after going offscreen. Defaults to false.
|
||||
Q_PROPERTY(bool persistenceSupported READ persistenceSupported WRITE setPersistenceSupported NOTIFY persistenceSupportedChanged);
|
||||
/// If notification body text should be advertised as supported by the notification server.
|
||||
/// Defaults to true.
|
||||
///
|
||||
/// Note that returned notifications are likely to return body text even if this property is false,
|
||||
/// as it is only a hint.
|
||||
Q_PROPERTY(bool bodySupported READ bodySupported WRITE setBodySupported NOTIFY bodySupportedChanged);
|
||||
/// If notification body text should be advertised as supporting markup as described in [the specification]
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// Note that returned notifications may still contain markup if this property is false, as it is only a hint.
|
||||
/// By default Text objects will try to render markup. To avoid this if any is sent, change [Text.textFormat] to `PlainText`.
|
||||
///
|
||||
/// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup
|
||||
/// [Text.textFormat]: https://doc.qt.io/qt-6/qml-qtquick-text.html#textFormat-prop
|
||||
Q_PROPERTY(bool bodyMarkupSupported READ bodyMarkupSupported WRITE setBodyMarkupSupported NOTIFY bodyMarkupSupportedChanged);
|
||||
/// If notification body text should be advertised as supporting hyperlinks as described in [the specification]
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// Note that returned notifications may still contain hyperlinks if this property is false, as it is only a hint.
|
||||
///
|
||||
/// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#hyperlinks
|
||||
Q_PROPERTY(bool bodyHyperlinksSupported READ bodyHyperlinksSupported WRITE setBodyHyperlinksSupported NOTIFY bodyHyperlinksSupportedChanged);
|
||||
/// If notification body text should be advertised as supporting images as described in [the specification]
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// Note that returned notifications may still contain images if this property is false, as it is only a hint.
|
||||
///
|
||||
/// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#images
|
||||
Q_PROPERTY(bool bodyImagesSupported READ bodyImagesSupported WRITE setBodyImagesSupported NOTIFY bodyImagesSupportedChanged);
|
||||
/// If notification actions should be advertised as supported by the notification server. Defaults to false.
|
||||
Q_PROPERTY(bool actionsSupported READ actionsSupported WRITE setActionsSupported NOTIFY actionsSupportedChanged);
|
||||
/// If notification actions should be advertised as supporting the display of icons. Defaults to false.
|
||||
Q_PROPERTY(bool actionIconsSupported READ actionIconsSupported WRITE setActionIconsSupported NOTIFY actionIconsSupportedChanged);
|
||||
/// If the notification server should advertise that it supports images. Defaults to false.
|
||||
Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged);
|
||||
/// If the notification server should advertise that it supports playing sounds. Defaults to false.
|
||||
Q_PROPERTY(bool soundSupported READ soundSupported WRITE setSoundSupported NOTIFY soundSupportedChanged);
|
||||
/// All notifications currently tracked by the server.
|
||||
Q_PROPERTY(ObjectModel<Notification>* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged);
|
||||
QML_NAMED_ELEMENT(NotificationServer);
|
||||
|
||||
public:
|
||||
void onPostReload() override;
|
||||
|
||||
[[nodiscard]] bool keepOnReload() const;
|
||||
void setKeepOnReload(bool keepOnReload);
|
||||
|
||||
[[nodiscard]] bool persistenceSupported() const;
|
||||
void setPersistenceSupported(bool persistenceSupported);
|
||||
|
||||
[[nodiscard]] bool bodySupported() const;
|
||||
void setBodySupported(bool bodySupported);
|
||||
|
||||
[[nodiscard]] bool bodyMarkupSupported() const;
|
||||
void setBodyMarkupSupported(bool bodyMarkupSupported);
|
||||
|
||||
[[nodiscard]] bool bodyHyperlinksSupported() const;
|
||||
void setBodyHyperlinksSupported(bool bodyHyperlinksSupported);
|
||||
|
||||
[[nodiscard]] bool bodyImagesSupported() const;
|
||||
void setBodyImagesSupported(bool bodyImagesSupported);
|
||||
|
||||
[[nodiscard]] bool actionsSupported() const;
|
||||
void setActionsSupported(bool actionsSupported);
|
||||
|
||||
[[nodiscard]] bool actionIconsSupported() const;
|
||||
void setActionIconsSupported(bool actionIconsSupported);
|
||||
|
||||
[[nodiscard]] bool imageSupported() const;
|
||||
void setImageSupported(bool imageSupported);
|
||||
|
||||
[[nodiscard]] bool soundSupported() const;
|
||||
void setSoundSupported(bool soundSupported);
|
||||
|
||||
[[nodiscard]] ObjectModel<Notification>* trackedNotifications() const;
|
||||
|
||||
signals:
|
||||
/// Sent when a notification is received by the server.
|
||||
///
|
||||
/// If this notification should not be discarded, set its `tracked` property to true.
|
||||
void notification(Notification* notification);
|
||||
|
||||
void keepOnReloadChanged();
|
||||
void persistenceSupportedChanged();
|
||||
void bodySupportedChanged();
|
||||
void bodyMarkupSupportedChanged();
|
||||
void bodyHyperlinksSupportedChanged();
|
||||
void bodyImagesSupportedChanged();
|
||||
void actionsSupportedChanged();
|
||||
void actionIconsSupportedChanged();
|
||||
void imageSupportedChanged();
|
||||
void soundSupportedChanged();
|
||||
void trackedNotificationsChanged();
|
||||
|
||||
private:
|
||||
void updateSupported();
|
||||
|
||||
bool live = false;
|
||||
bool mKeepOnReload = true;
|
||||
NotificationServerSupport support;
|
||||
};
|
||||
|
||||
} // namespace qs::service::notifications
|
198
src/services/notifications/server.cpp
Normal file
198
src/services/notifications/server.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
#include "server.hpp"
|
||||
#include <functional>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusmetatype.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include "dbus_notifications.h"
|
||||
#include "dbusimage.hpp"
|
||||
#include "notification.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications");
|
||||
|
||||
NotificationServer::NotificationServer() {
|
||||
qDBusRegisterMetaType<DBusNotificationImage>();
|
||||
|
||||
new DBusNotificationServer(this);
|
||||
|
||||
qCInfo(logNotifications) << "Starting notification server";
|
||||
|
||||
auto bus = QDBusConnection::sessionBus();
|
||||
|
||||
if (!bus.isConnected()) {
|
||||
qCWarning(logNotifications)
|
||||
<< "Could not connect to DBus. Notification service will not work.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bus.registerObject("/org/freedesktop/Notifications", this)) {
|
||||
qCWarning(logNotifications) << "Could not register Notification server object with DBus. "
|
||||
"Notification server will not work.";
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
&this->serviceWatcher,
|
||||
&QDBusServiceWatcher::serviceUnregistered,
|
||||
this,
|
||||
&NotificationServer::onServiceUnregistered
|
||||
);
|
||||
|
||||
this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
|
||||
this->serviceWatcher.addWatchedService("org.freedesktop.Notifications");
|
||||
this->serviceWatcher.setConnection(bus);
|
||||
|
||||
NotificationServer::tryRegister();
|
||||
}
|
||||
|
||||
NotificationServer* NotificationServer::instance() {
|
||||
static auto* instance = new NotificationServer(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
void NotificationServer::switchGeneration(bool reEmit, const std::function<void()>& clearHook) {
|
||||
auto notifications = this->mNotifications.valueList();
|
||||
this->mNotifications.valueList().clear();
|
||||
this->idMap.clear();
|
||||
|
||||
clearHook();
|
||||
|
||||
if (reEmit) {
|
||||
for (auto* notification: notifications) {
|
||||
notification->setLastGeneration();
|
||||
notification->setTracked(false);
|
||||
emit this->notification(notification);
|
||||
|
||||
if (!notification->isTracked()) {
|
||||
emit this->NotificationClosed(notification->id(), notification->closeReason());
|
||||
delete notification;
|
||||
} else {
|
||||
this->idMap.insert(notification->id(), notification);
|
||||
this->mNotifications.insertObject(notification);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* notification: notifications) {
|
||||
emit this->NotificationClosed(notification->id(), NotificationCloseReason::Expired);
|
||||
delete notification;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectModel<Notification>* NotificationServer::trackedNotifications() {
|
||||
return &this->mNotifications;
|
||||
}
|
||||
|
||||
void NotificationServer::deleteNotification(Notification* notification, NotificationCloseReason::Enum reason) {
|
||||
if (!this->idMap.contains(notification->id())) return;
|
||||
|
||||
emit notification->closed(reason);
|
||||
|
||||
this->mNotifications.removeObject(notification);
|
||||
this->idMap.remove(notification->id());
|
||||
|
||||
emit this->NotificationClosed(notification->id(), reason);
|
||||
delete notification;
|
||||
}
|
||||
|
||||
void NotificationServer::tryRegister() {
|
||||
auto bus = QDBusConnection::sessionBus();
|
||||
auto success = bus.registerService("org.freedesktop.Notifications");
|
||||
|
||||
if (success) {
|
||||
qCInfo(logNotifications) << "Registered notification server with dbus.";
|
||||
} else {
|
||||
qCWarning(logNotifications)
|
||||
<< "Could not register notification server at org.freedesktop.Notifications, presumably "
|
||||
"because one is already registered.";
|
||||
qCWarning(logNotifications) << "Registration will be attempted again if the active service is unregistered.";
|
||||
}
|
||||
}
|
||||
void NotificationServer::onServiceUnregistered(const QString& /*unused*/) {
|
||||
qCDebug(logNotifications
|
||||
) << "Active notification server unregistered, attempting registration";
|
||||
this->tryRegister();
|
||||
}
|
||||
|
||||
void NotificationServer::CloseNotification(uint id) {
|
||||
auto* notification = this->idMap.value(id);
|
||||
|
||||
if (notification) {
|
||||
this->deleteNotification(notification, NotificationCloseReason::CloseRequested);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList NotificationServer::GetCapabilities() const {
|
||||
auto capabilities = QStringList();
|
||||
|
||||
if (this->support.persistence) capabilities += "persistence";
|
||||
|
||||
if (this->support.body) {
|
||||
capabilities += "body";
|
||||
if (this->support.bodyMarkup) capabilities += "body-markup";
|
||||
if (this->support.bodyHyperlinks) capabilities += "body-hyperlinks";
|
||||
if (this->support.bodyImages) capabilities += "body-images";
|
||||
}
|
||||
|
||||
if (this->support.actions) {
|
||||
capabilities += "actions";
|
||||
if (this->support.actionIcons) capabilities += "action-icons";
|
||||
}
|
||||
|
||||
if (this->support.image) capabilities += "icon-static";
|
||||
if (this->support.sound) capabilities += "sound";
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
QString NotificationServer::GetServerInformation(QString& vendor, QString& version, QString& specVersion) {
|
||||
vendor = "quickshell";
|
||||
version = QCoreApplication::applicationVersion();
|
||||
specVersion = "1.2";
|
||||
return "quickshell";
|
||||
}
|
||||
|
||||
uint NotificationServer::Notify(
|
||||
const QString& appName,
|
||||
uint replacesId,
|
||||
const QString& appIcon,
|
||||
const QString& summary,
|
||||
const QString& body,
|
||||
const QStringList& actions,
|
||||
const QVariantMap& hints,
|
||||
int expireTimeout
|
||||
) {
|
||||
qDebug() << "NOTIFY" << appName << replacesId << appIcon << summary << body << actions << hints << expireTimeout;
|
||||
auto* notification = replacesId == 0 ? nullptr : this->idMap.value(replacesId);
|
||||
auto old = notification != nullptr;
|
||||
|
||||
if (!notification) {
|
||||
notification = new Notification(this->nextId++, this);
|
||||
QQmlEngine::setObjectOwnership(notification, QQmlEngine::CppOwnership);
|
||||
}
|
||||
|
||||
notification->updateProperties(appName, appIcon, summary, body, actions, hints, expireTimeout);
|
||||
|
||||
if (!old) {
|
||||
emit this->notification(notification);
|
||||
|
||||
if (!notification->isTracked()) {
|
||||
emit this->NotificationClosed(notification->id(), notification->closeReason());
|
||||
delete notification;
|
||||
} else {
|
||||
this->idMap.insert(notification->id(), notification);
|
||||
this->mNotifications.insertObject(notification);
|
||||
}
|
||||
}
|
||||
|
||||
return notification->id();
|
||||
}
|
||||
|
||||
}
|
76
src/services/notifications/server.hpp
Normal file
76
src/services/notifications/server.hpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <qdbusservicewatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include "notification.hpp"
|
||||
#include "../../core/model.hpp"
|
||||
|
||||
namespace qs::service::notifications {
|
||||
|
||||
struct NotificationServerSupport {
|
||||
bool persistence = false;
|
||||
bool body = true;
|
||||
bool bodyMarkup = false;
|
||||
bool bodyHyperlinks = false;
|
||||
bool bodyImages = false;
|
||||
bool actions = false;
|
||||
bool actionIcons = false;
|
||||
bool image = false;
|
||||
bool sound = false;
|
||||
};
|
||||
|
||||
class NotificationServer: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
static NotificationServer* instance();
|
||||
|
||||
void switchGeneration(bool reEmit, const std::function<void()>& clearHook);
|
||||
ObjectModel<Notification>* trackedNotifications();
|
||||
void deleteNotification(Notification* notification, NotificationCloseReason::Enum reason);
|
||||
|
||||
// NOLINTBEGIN
|
||||
void CloseNotification(uint id);
|
||||
QStringList GetCapabilities() const;
|
||||
static QString GetServerInformation(QString& vendor, QString& version, QString& specVersion);
|
||||
uint Notify(
|
||||
const QString& appName,
|
||||
uint replacesId,
|
||||
const QString& appIcon,
|
||||
const QString& summary,
|
||||
const QString& body,
|
||||
const QStringList& actions,
|
||||
const QVariantMap& hints,
|
||||
int expireTimeout
|
||||
);
|
||||
// NOLINTEND
|
||||
|
||||
NotificationServerSupport support;
|
||||
|
||||
signals:
|
||||
void notification(Notification* notification);
|
||||
|
||||
// NOLINTBEGIN
|
||||
void NotificationClosed(quint32 id, quint32 reason);
|
||||
void ActionInvoked(quint32 id, QString action);
|
||||
// NOLINTEND
|
||||
|
||||
private slots:
|
||||
void onServiceUnregistered(const QString& service);
|
||||
|
||||
private:
|
||||
explicit NotificationServer();
|
||||
|
||||
static void tryRegister();
|
||||
|
||||
QDBusServiceWatcher serviceWatcher;
|
||||
quint32 nextId = 1;
|
||||
QHash<quint32, Notification*> idMap;
|
||||
ObjectModel<Notification> mNotifications {this};
|
||||
};
|
||||
|
||||
}
|
39
src/services/upower/CMakeLists.txt
Normal file
39
src/services/upower/CMakeLists.txt
Normal file
|
@ -0,0 +1,39 @@
|
|||
set_source_files_properties(org.freedesktop.UPower.xml PROPERTIES
|
||||
CLASSNAME DBusUPowerService
|
||||
NO_NAMESPACE TRUE
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.UPower.xml
|
||||
dbus_service
|
||||
)
|
||||
|
||||
set_source_files_properties(org.freedesktop.UPower.Device.xml PROPERTIES
|
||||
CLASSNAME DBusUPowerDevice
|
||||
NO_NAMESPACE TRUE
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.UPower.Device.xml
|
||||
dbus_device
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-service-upower STATIC
|
||||
core.cpp
|
||||
device.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
# dbus headers
|
||||
target_include_directories(quickshell-service-upower PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
qt_add_qml_module(quickshell-service-upower
|
||||
URI Quickshell.Services.UPower
|
||||
VERSION 0.1
|
||||
)
|
||||
|
||||
target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus)
|
||||
target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin)
|
||||
|
||||
qs_pch(quickshell-service-upower)
|
||||
qs_pch(quickshell-service-upowerplugin)
|
165
src/services/upower/core.cpp
Normal file
165
src/services/upower/core.cpp
Normal file
|
@ -0,0 +1,165 @@
|
|||
#include "core.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusconnectioninterface.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdbusservicewatcher.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../../core/model.hpp"
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_service.h"
|
||||
#include "device.hpp"
|
||||
|
||||
namespace qs::service::upower {
|
||||
|
||||
Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg);
|
||||
|
||||
UPower::UPower() {
|
||||
qCDebug(logUPower) << "Starting UPower";
|
||||
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
|
||||
if (!bus.isConnected()) {
|
||||
qCWarning(logUPower) << "Could not connect to DBus. UPower service will not work.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->service =
|
||||
new DBusUPowerService("org.freedesktop.UPower", "/org/freedesktop/UPower", bus, this);
|
||||
|
||||
if (!this->service->isValid()) {
|
||||
qCWarning(logUPower) << "Cannot connect to the UPower service.";
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
&this->pOnBattery,
|
||||
&dbus::AbstractDBusProperty::changed,
|
||||
this,
|
||||
&UPower::onBatteryChanged
|
||||
);
|
||||
|
||||
this->serviceProperties.setInterface(this->service);
|
||||
this->serviceProperties.updateAllViaGetAll();
|
||||
|
||||
this->registerExisting();
|
||||
}
|
||||
|
||||
void UPower::registerExisting() {
|
||||
this->registerDevice("/org/freedesktop/UPower/devices/DisplayDevice");
|
||||
|
||||
auto pending = this->service->EnumerateDevices();
|
||||
auto* call = new QDBusPendingCallWatcher(pending, this);
|
||||
|
||||
auto responseCallback = [this](QDBusPendingCallWatcher* call) {
|
||||
const QDBusPendingReply<QList<QDBusObjectPath>> reply = *call;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logUPower) << "Failed to enumerate devices:" << reply.error().message();
|
||||
} else {
|
||||
for (const QDBusObjectPath& devicePath: reply.value()) {
|
||||
this->registerDevice(devicePath.path());
|
||||
}
|
||||
}
|
||||
|
||||
delete call;
|
||||
};
|
||||
|
||||
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
|
||||
}
|
||||
|
||||
void UPower::onDeviceReady() {
|
||||
auto* device = qobject_cast<UPowerDevice*>(this->sender());
|
||||
|
||||
if (device->path() == "/org/freedesktop/UPower/devices/DisplayDevice") {
|
||||
this->mDisplayDevice = device;
|
||||
emit this->displayDeviceChanged();
|
||||
qCDebug(logUPower) << "Display UPowerDevice" << device->path() << "ready";
|
||||
return;
|
||||
}
|
||||
|
||||
this->readyDevices.insertObject(device);
|
||||
qCDebug(logUPower) << "UPowerDevice" << device->path() << "ready";
|
||||
}
|
||||
|
||||
void UPower::onDeviceDestroyed(QObject* object) {
|
||||
auto* device = static_cast<UPowerDevice*>(object); // NOLINT
|
||||
|
||||
this->mDevices.remove(device->path());
|
||||
|
||||
if (device == this->mDisplayDevice) {
|
||||
this->mDisplayDevice = nullptr;
|
||||
emit this->displayDeviceChanged();
|
||||
qCDebug(logUPower) << "Display UPowerDevice" << device->path() << "destroyed";
|
||||
return;
|
||||
}
|
||||
|
||||
this->readyDevices.removeObject(device);
|
||||
qCDebug(logUPower) << "UPowerDevice" << device->path() << "destroyed";
|
||||
}
|
||||
|
||||
void UPower::registerDevice(const QString& path) {
|
||||
if (this->mDevices.contains(path)) {
|
||||
qCDebug(logUPower) << "Skipping duplicate registration of UPowerDevice" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
auto* device = new UPowerDevice(path, this);
|
||||
if (!device->isValid()) {
|
||||
qCWarning(logUPower) << "Ignoring invalid UPowerDevice registration of" << path;
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
this->mDevices.insert(path, device);
|
||||
QObject::connect(device, &UPowerDevice::ready, this, &UPower::onDeviceReady);
|
||||
QObject::connect(device, &QObject::destroyed, this, &UPower::onDeviceDestroyed);
|
||||
|
||||
qCDebug(logUPower) << "Registered UPowerDevice" << path;
|
||||
}
|
||||
|
||||
UPowerDevice* UPower::displayDevice() { return this->mDisplayDevice; }
|
||||
|
||||
ObjectModel<UPowerDevice>* UPower::devices() { return &this->readyDevices; }
|
||||
|
||||
bool UPower::onBattery() const { return this->pOnBattery.get(); }
|
||||
|
||||
UPower* UPower::instance() {
|
||||
static UPower* instance = new UPower(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
UPowerQml::UPowerQml(QObject* parent): QObject(parent) {
|
||||
QObject::connect(
|
||||
UPower::instance(),
|
||||
&UPower::displayDeviceChanged,
|
||||
this,
|
||||
&UPowerQml::displayDeviceChanged
|
||||
);
|
||||
QObject::connect(
|
||||
UPower::instance(),
|
||||
&UPower::onBatteryChanged,
|
||||
this,
|
||||
&UPowerQml::onBatteryChanged
|
||||
);
|
||||
}
|
||||
|
||||
UPowerDevice* UPowerQml::displayDevice() { // NOLINT
|
||||
return UPower::instance()->displayDevice();
|
||||
}
|
||||
|
||||
ObjectModel<UPowerDevice>* UPowerQml::devices() { // NOLINT
|
||||
return UPower::instance()->devices();
|
||||
}
|
||||
|
||||
bool UPowerQml::onBattery() { return UPower::instance()->onBattery(); }
|
||||
|
||||
} // namespace qs::service::upower
|
77
src/services/upower/core.hpp
Normal file
77
src/services/upower/core.hpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdbusservicewatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../../core/model.hpp"
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_service.h"
|
||||
#include "device.hpp"
|
||||
|
||||
namespace qs::service::upower {
|
||||
|
||||
class UPower: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
[[nodiscard]] UPowerDevice* displayDevice();
|
||||
[[nodiscard]] ObjectModel<UPowerDevice>* devices();
|
||||
[[nodiscard]] bool onBattery() const;
|
||||
|
||||
static UPower* instance();
|
||||
|
||||
signals:
|
||||
void displayDeviceChanged();
|
||||
void onBatteryChanged();
|
||||
|
||||
private slots:
|
||||
void onDeviceReady();
|
||||
void onDeviceDestroyed(QObject* object);
|
||||
|
||||
private:
|
||||
explicit UPower();
|
||||
|
||||
void registerExisting();
|
||||
void registerDevice(const QString& path);
|
||||
|
||||
UPowerDevice* mDisplayDevice = nullptr;
|
||||
QHash<QString, UPowerDevice*> mDevices;
|
||||
ObjectModel<UPowerDevice> readyDevices {this};
|
||||
|
||||
dbus::DBusPropertyGroup serviceProperties;
|
||||
dbus::DBusProperty<bool> pOnBattery {this->serviceProperties, "OnBattery"};
|
||||
|
||||
DBusUPowerService* service = nullptr;
|
||||
};
|
||||
|
||||
class UPowerQml: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(UPower);
|
||||
QML_SINGLETON;
|
||||
/// UPower's DisplayDevice for your system. Can be `null`.
|
||||
///
|
||||
/// This is an aggregate device and not a physical one, meaning you will not find it in `devices`.
|
||||
/// It is typically the device that is used for displaying information in desktop environments.
|
||||
Q_PROPERTY(UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged);
|
||||
/// All connected UPower devices.
|
||||
Q_PROPERTY(ObjectModel<UPowerDevice>* devices READ devices CONSTANT);
|
||||
/// If the system is currently running on battery power, or discharging.
|
||||
Q_PROPERTY(bool onBattery READ onBattery NOTIFY onBatteryChanged);
|
||||
|
||||
public:
|
||||
explicit UPowerQml(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] UPowerDevice* displayDevice();
|
||||
[[nodiscard]] ObjectModel<UPowerDevice>* devices();
|
||||
[[nodiscard]] static bool onBattery();
|
||||
|
||||
signals:
|
||||
void displayDeviceChanged();
|
||||
void onBatteryChanged();
|
||||
};
|
||||
|
||||
} // namespace qs::service::upower
|
134
src/services/upower/device.cpp
Normal file
134
src/services/upower/device.cpp
Normal file
|
@ -0,0 +1,134 @@
|
|||
#include "device.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qstring.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
using namespace qs::dbus;
|
||||
|
||||
namespace qs::service::upower {
|
||||
|
||||
Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg);
|
||||
|
||||
QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) {
|
||||
switch (status) {
|
||||
case UPowerDeviceState::Unknown: return "Unknown";
|
||||
case UPowerDeviceState::Charging: return "Charging";
|
||||
case UPowerDeviceState::Discharging: return "Discharging";
|
||||
case UPowerDeviceState::Empty: return "Empty";
|
||||
case UPowerDeviceState::FullyCharged: return "Fully Charged";
|
||||
case UPowerDeviceState::PendingCharge: return "Pending Charge";
|
||||
case UPowerDeviceState::PendingDischarge: return "Pending Discharge";
|
||||
default: return "Invalid Status";
|
||||
}
|
||||
}
|
||||
|
||||
QString UPowerDeviceType::toString(UPowerDeviceType::Enum type) {
|
||||
switch (type) {
|
||||
case UPowerDeviceType::Unknown: return "Unknown";
|
||||
case UPowerDeviceType::LinePower: return "Line Power";
|
||||
case UPowerDeviceType::Battery: return "Battery";
|
||||
case UPowerDeviceType::Ups: return "Ups";
|
||||
case UPowerDeviceType::Monitor: return "Monitor";
|
||||
case UPowerDeviceType::Mouse: return "Mouse";
|
||||
case UPowerDeviceType::Keyboard: return "Keyboard";
|
||||
case UPowerDeviceType::Pda: return "Pda";
|
||||
case UPowerDeviceType::Phone: return "Phone";
|
||||
case UPowerDeviceType::MediaPlayer: return "Media Player";
|
||||
case UPowerDeviceType::Tablet: return "Tablet";
|
||||
case UPowerDeviceType::Computer: return "Computer";
|
||||
case UPowerDeviceType::GamingInput: return "Gaming Input";
|
||||
case UPowerDeviceType::Pen: return "Pen";
|
||||
case UPowerDeviceType::Touchpad: return "Touchpad";
|
||||
case UPowerDeviceType::Modem: return "Modem";
|
||||
case UPowerDeviceType::Network: return "Network";
|
||||
case UPowerDeviceType::Headset: return "Headset";
|
||||
case UPowerDeviceType::Speakers: return "Speakers";
|
||||
case UPowerDeviceType::Headphones: return "Headphones";
|
||||
case UPowerDeviceType::Video: return "Video";
|
||||
case UPowerDeviceType::OtherAudio: return "Other Audio";
|
||||
case UPowerDeviceType::RemoteControl: return "Remote Control";
|
||||
case UPowerDeviceType::Printer: return "Printer";
|
||||
case UPowerDeviceType::Scanner: return "Scanner";
|
||||
case UPowerDeviceType::Camera: return "Camera";
|
||||
case UPowerDeviceType::Wearable: return "Wearable";
|
||||
case UPowerDeviceType::Toy: return "Toy";
|
||||
case UPowerDeviceType::BluetoothGeneric: return "Bluetooth Generic";
|
||||
default: return "Invalid Type";
|
||||
}
|
||||
}
|
||||
|
||||
UPowerDevice::UPowerDevice(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->device =
|
||||
new DBusUPowerDevice("org.freedesktop.UPower", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->device->isValid()) {
|
||||
qCWarning(logUPowerDevice) << "Cannot create UPowerDevice for" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(&this->pType, &AbstractDBusProperty::changed, this, &UPowerDevice::typeChanged);
|
||||
QObject::connect(&this->pPowerSupply, &AbstractDBusProperty::changed, this, &UPowerDevice::powerSupplyChanged);
|
||||
QObject::connect(&this->pEnergy, &AbstractDBusProperty::changed, this, &UPowerDevice::energyChanged);
|
||||
QObject::connect(&this->pEnergyCapacity, &AbstractDBusProperty::changed, this, &UPowerDevice::energyCapacityChanged);
|
||||
QObject::connect(&this->pChangeRate, &AbstractDBusProperty::changed, this, &UPowerDevice::changeRateChanged);
|
||||
QObject::connect(&this->pTimeToEmpty, &AbstractDBusProperty::changed, this, &UPowerDevice::timeToEmptyChanged);
|
||||
QObject::connect(&this->pTimeToFull, &AbstractDBusProperty::changed, this, &UPowerDevice::timeToFullChanged);
|
||||
QObject::connect(&this->pPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::percentageChanged);
|
||||
QObject::connect(&this->pIsPresent, &AbstractDBusProperty::changed, this, &UPowerDevice::isPresentChanged);
|
||||
QObject::connect(&this->pState, &AbstractDBusProperty::changed, this, &UPowerDevice::stateChanged);
|
||||
QObject::connect(&this->pHealthPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::healthPercentageChanged);
|
||||
QObject::connect(&this->pHealthPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::healthSupportedChanged);
|
||||
QObject::connect(&this->pIconName, &AbstractDBusProperty::changed, this, &UPowerDevice::iconNameChanged);
|
||||
QObject::connect(&this->pType, &AbstractDBusProperty::changed, this, &UPowerDevice::isLaptopBatteryChanged);
|
||||
QObject::connect(&this->pNativePath, &AbstractDBusProperty::changed, this, &UPowerDevice::nativePathChanged);
|
||||
|
||||
QObject::connect(&this->deviceProperties, &DBusPropertyGroup::getAllFinished, this, &UPowerDevice::ready);
|
||||
// clang-format on
|
||||
|
||||
this->deviceProperties.setInterface(this->device);
|
||||
this->deviceProperties.updateAllViaGetAll();
|
||||
}
|
||||
|
||||
bool UPowerDevice::isValid() const { return this->device->isValid(); }
|
||||
QString UPowerDevice::address() const { return this->device->service(); }
|
||||
QString UPowerDevice::path() const { return this->device->path(); }
|
||||
|
||||
UPowerDeviceType::Enum UPowerDevice::type() const {
|
||||
return static_cast<UPowerDeviceType::Enum>(this->pType.get());
|
||||
}
|
||||
|
||||
bool UPowerDevice::powerSupply() const { return this->pPowerSupply.get(); }
|
||||
qreal UPowerDevice::energy() const { return this->pEnergy.get(); }
|
||||
qreal UPowerDevice::energyCapacity() const { return this->pEnergyCapacity.get(); }
|
||||
qreal UPowerDevice::changeRate() const { return this->pChangeRate.get(); }
|
||||
qlonglong UPowerDevice::timeToEmpty() const { return this->pTimeToEmpty.get(); }
|
||||
qlonglong UPowerDevice::timeToFull() const { return this->pTimeToFull.get(); }
|
||||
qreal UPowerDevice::percentage() const { return this->pPercentage.get(); }
|
||||
bool UPowerDevice::isPresent() const { return this->pIsPresent.get(); }
|
||||
|
||||
UPowerDeviceState::Enum UPowerDevice::state() const {
|
||||
return static_cast<UPowerDeviceState::Enum>(this->pState.get());
|
||||
}
|
||||
|
||||
qreal UPowerDevice::healthPercentage() const { return this->pHealthPercentage.get(); }
|
||||
|
||||
bool UPowerDevice::healthSupported() const { return this->healthPercentage() != 0; }
|
||||
|
||||
QString UPowerDevice::iconName() const { return this->pIconName.get(); }
|
||||
|
||||
bool UPowerDevice::isLaptopBattery() const {
|
||||
return this->pType.get() == UPowerDeviceType::Battery && this->pPowerSupply.get();
|
||||
}
|
||||
|
||||
QString UPowerDevice::nativePath() const { return this->pNativePath.get(); }
|
||||
|
||||
} // namespace qs::service::upower
|
187
src/services/upower/device.hpp
Normal file
187
src/services/upower/device.hpp
Normal file
|
@ -0,0 +1,187 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../../core/doc.hpp"
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::service::upower {
|
||||
|
||||
class UPowerDeviceState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
Unknown = 0,
|
||||
Charging = 1,
|
||||
Discharging = 2,
|
||||
Empty = 3,
|
||||
FullyCharged = 4,
|
||||
/// The device is waiting to be charged after it was plugged in.
|
||||
PendingCharge = 5,
|
||||
/// The device is waiting to be discharged after being unplugged.
|
||||
PendingDischarge = 6,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(UPowerDeviceState::Enum status);
|
||||
};
|
||||
|
||||
class UPowerDeviceType: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
Unknown = 0,
|
||||
LinePower = 1,
|
||||
Battery = 2,
|
||||
Ups = 3,
|
||||
Monitor = 4,
|
||||
Mouse = 5,
|
||||
Keyboard = 6,
|
||||
Pda = 7,
|
||||
Phone = 8,
|
||||
MediaPlayer = 9,
|
||||
Tablet = 10,
|
||||
Computer = 11,
|
||||
GamingInput = 12,
|
||||
Pen = 13,
|
||||
Touchpad = 14,
|
||||
Modem = 15,
|
||||
Network = 16,
|
||||
Headset = 17,
|
||||
Speakers = 18,
|
||||
Headphones = 19,
|
||||
Video = 20,
|
||||
OtherAudio = 21,
|
||||
RemoteControl = 22,
|
||||
Printer = 23,
|
||||
Scanner = 24,
|
||||
Camera = 25,
|
||||
Wearable = 26,
|
||||
Toy = 27,
|
||||
BluetoothGeneric = 28,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(UPowerDeviceType::Enum type);
|
||||
};
|
||||
|
||||
///! A device exposed through the UPower system service.
|
||||
class UPowerDevice: public QObject {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// The type of device.
|
||||
Q_PROPERTY(UPowerDeviceType::Enum type READ type NOTIFY typeChanged);
|
||||
/// If the device is a power supply for your computer and can provide charge.
|
||||
Q_PROPERTY(bool powerSupply READ powerSupply NOTIFY powerSupplyChanged);
|
||||
/// Current energy level of the device in watt-hours.
|
||||
Q_PROPERTY(qreal energy READ energy NOTIFY energyChanged);
|
||||
/// Maximum energy capacity of the device in watt-hours
|
||||
Q_PROPERTY(qreal energyCapacity READ energyCapacity NOTIFY energyCapacityChanged);
|
||||
/// Rate of energy change in watts (positive when charging, negative when discharging).
|
||||
Q_PROPERTY(qreal changeRate READ changeRate NOTIFY changeRateChanged);
|
||||
/// Estimated time until the device is fully discharged, in seconds.
|
||||
///
|
||||
/// Will be set to `0` if charging.
|
||||
Q_PROPERTY(qreal timeToEmpty READ timeToEmpty NOTIFY timeToEmptyChanged);
|
||||
/// Estimated time until the device is fully charged, in seconds.
|
||||
///
|
||||
/// Will be set to `0` if discharging.
|
||||
Q_PROPERTY(qreal timeToFull READ timeToFull NOTIFY timeToFullChanged);
|
||||
/// Current charge level as a percentage.
|
||||
///
|
||||
/// This would be equivalent to `energy / energyCapacity`.
|
||||
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged);
|
||||
/// If the power source is present in the bay or slot, useful for hot-removable batteries.
|
||||
///
|
||||
/// If the device `type` is not `Battery`, then the property will be invalid.
|
||||
Q_PROPERTY(bool isPresent READ isPresent NOTIFY isPresentChanged);
|
||||
/// Current state of the device.
|
||||
Q_PROPERTY(UPowerDeviceState::Enum state READ state NOTIFY stateChanged);
|
||||
/// Health of the device as a percentage of its original health.
|
||||
Q_PROPERTY(qreal healthPercentage READ healthPercentage NOTIFY healthPercentageChanged);
|
||||
Q_PROPERTY(bool healthSupported READ healthSupported NOTIFY healthSupportedChanged);
|
||||
/// Name of the icon representing the current state of the device, or an empty string if not provided.
|
||||
Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged);
|
||||
/// If the device is a laptop battery or not. Use this to check if your device is a valid battery.
|
||||
///
|
||||
/// This will be equivalent to `type == Battery && powerSupply == true`.
|
||||
Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged);
|
||||
/// Native path of the device specific to your OS.
|
||||
Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("UPowerDevices can only be acquired from UPower");
|
||||
|
||||
public:
|
||||
explicit UPowerDevice(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] QString address() const;
|
||||
[[nodiscard]] QString path() const;
|
||||
|
||||
[[nodiscard]] UPowerDeviceType::Enum type() const;
|
||||
[[nodiscard]] bool powerSupply() const;
|
||||
[[nodiscard]] qreal energy() const;
|
||||
[[nodiscard]] qreal energyCapacity() const;
|
||||
[[nodiscard]] qreal changeRate() const;
|
||||
[[nodiscard]] qlonglong timeToEmpty() const;
|
||||
[[nodiscard]] qlonglong timeToFull() const;
|
||||
[[nodiscard]] qreal percentage() const;
|
||||
[[nodiscard]] bool isPresent() const;
|
||||
[[nodiscard]] UPowerDeviceState::Enum state() const;
|
||||
[[nodiscard]] qreal healthPercentage() const;
|
||||
[[nodiscard]] bool healthSupported() const;
|
||||
[[nodiscard]] QString iconName() const;
|
||||
[[nodiscard]] bool isLaptopBattery() const;
|
||||
[[nodiscard]] QString nativePath() const;
|
||||
|
||||
signals:
|
||||
QSDOC_HIDE void ready();
|
||||
|
||||
void typeChanged();
|
||||
void powerSupplyChanged();
|
||||
void energyChanged();
|
||||
void energyCapacityChanged();
|
||||
void changeRateChanged();
|
||||
void timeToEmptyChanged();
|
||||
void timeToFullChanged();
|
||||
void percentageChanged();
|
||||
void isPresentChanged();
|
||||
void stateChanged();
|
||||
void healthPercentageChanged();
|
||||
void healthSupportedChanged();
|
||||
void iconNameChanged();
|
||||
void isLaptopBatteryChanged();
|
||||
void nativePathChanged();
|
||||
|
||||
private:
|
||||
dbus::DBusPropertyGroup deviceProperties;
|
||||
dbus::DBusProperty<quint32> pType {this->deviceProperties, "Type"};
|
||||
dbus::DBusProperty<bool> pPowerSupply {this->deviceProperties, "PowerSupply"};
|
||||
dbus::DBusProperty<qreal> pEnergy {this->deviceProperties, "Energy"};
|
||||
dbus::DBusProperty<qreal> pEnergyCapacity {this->deviceProperties, "EnergyFull"};
|
||||
dbus::DBusProperty<qreal> pChangeRate {this->deviceProperties, "EnergyRate"};
|
||||
dbus::DBusProperty<qlonglong> pTimeToEmpty {this->deviceProperties, "TimeToEmpty"};
|
||||
dbus::DBusProperty<qlonglong> pTimeToFull {this->deviceProperties, "TimeToFull"};
|
||||
dbus::DBusProperty<qreal> pPercentage {this->deviceProperties, "Percentage"};
|
||||
dbus::DBusProperty<bool> pIsPresent {this->deviceProperties, "IsPresent"};
|
||||
dbus::DBusProperty<quint32> pState {this->deviceProperties, "State"};
|
||||
dbus::DBusProperty<qreal> pHealthPercentage {this->deviceProperties, "Capacity"};
|
||||
dbus::DBusProperty<QString> pIconName {this->deviceProperties, "IconName"};
|
||||
dbus::DBusProperty<QString> pNativePath {this->deviceProperties, "NativePath"};
|
||||
|
||||
DBusUPowerDevice* device = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::service::upower
|
7
src/services/upower/module.md
Normal file
7
src/services/upower/module.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
name = "Quickshell.Services.UPower"
|
||||
description = "UPower Service"
|
||||
headers = [
|
||||
"core.hpp",
|
||||
"device.hpp",
|
||||
]
|
||||
-----
|
16
src/services/upower/org.freedesktop.UPower.Device.xml
Normal file
16
src/services/upower/org.freedesktop.UPower.Device.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<node>
|
||||
<interface name="org.freedesktop.UPower.Device">
|
||||
<property name="Type" type="u" access="read"/>
|
||||
<property name="PowerSupply" type="b" access="read"/>
|
||||
<property name="Energy" type="d" access="read"/>
|
||||
<property name="EnergyFull" type="d" access="read"/>
|
||||
<property name="EnergyRate" type="d" access="read"/>
|
||||
<property name="TimeToEmpty" type="x" access="read"/>
|
||||
<property name="TimeToFull" type="x" access="read"/>
|
||||
<property name="Percentage" type="d" access="read"/>
|
||||
<property name="IsPresent" type="b" access="read"/>
|
||||
<property name="State" type="u" access="read"/>
|
||||
<property name="Capacity" type = "d" access="read"/>
|
||||
<property name="IconName" type="s" access="read"/>
|
||||
</interface>
|
||||
</node>
|
7
src/services/upower/org.freedesktop.UPower.xml
Normal file
7
src/services/upower/org.freedesktop.UPower.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<node>
|
||||
<interface name="org.freedesktop.UPower">
|
||||
<method name="EnumerateDevices">
|
||||
<arg direction="out" type="ao" name="devices"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
Loading…
Reference in a new issue