Compare commits

...

5 commits

Author SHA1 Message Date
outfoxxed 1ef7a3d10f
notifs pushed early on request 2024-07-11 22:42:40 -07:00
outfoxxed 79cbfba48a
wayland/layershell: add warning that exclusive focus is not a lock
Apparently this needed to be said.
2024-07-11 22:32:21 -07:00
outfoxxed c758421af6
core/reloader: fix Reloadable::onReload being called multiple times
onReload was called multiple times due to Reloadable::reloadRecursive
calling onReload instead of reload, which didn't set reloadComplete.
This allowed the componentComplete fallback to call reload later.
2024-07-11 01:43:54 -07:00
outfoxxed 49b309247d
all: fix formatting 2024-07-11 00:16:44 -07:00
outfoxxed bb33c9a0c4
core/global: add Quickshell.iconPath
Replaces "image://icon/" in user facing code.
2024-07-11 00:09:34 -07:00
22 changed files with 1250 additions and 10 deletions

View file

@ -26,6 +26,7 @@ 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}")
@ -45,6 +46,7 @@ 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}")

View file

@ -344,12 +344,11 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
this->desktopEntries.insert(id, dentry);
if (this->lowercaseDesktopEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
this->lowercaseDesktopEntries.remove(lowerId);
}

View file

@ -55,7 +55,6 @@ public:
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QString& execString, const QString& workingDirectory);
public:
QString mId;
QString mName;

View file

@ -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;
}

View file

@ -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());
}
};

View file

@ -1,5 +1,6 @@
#include "proxywindow.hpp"
#include <private/qquickwindow_p.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
@ -11,7 +12,6 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include <private/qquickwindow_p.h>
#include "generation.hpp"
#include "qmlglobal.hpp"

View file

@ -19,6 +19,7 @@
#include <unistd.h>
#include "generation.hpp"
#include "iconimageprovider.hpp"
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
@ -188,6 +189,10 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
return qEnvironmentVariable(vstr.data());
}
QString QuickshellGlobal::iconPath(const QString& icon) {
return IconImageProvider::requestString(icon, "");
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine);

View file

@ -125,6 +125,11 @@ public:
/// Returns the string value of an environment variable or null if it is not set.
Q_INVOKABLE QVariant env(const QString& variable);
/// Returns a source string usable in an [Image] for a given system icon.
///
/// [Image]: https://doc.qt.io/qt-6/qml-qtquick-image.html
Q_INVOKABLE static QString iconPath(const QString& icon);
[[nodiscard]] QString workingDirectory() const;
void setWorkingDirectory(QString workingDirectory);

View file

@ -86,7 +86,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
// pass handling to the child's onReload, which should call back into reloadRecursive,
// with its oldInstance becoming the new oldRoot.
reloadable->onReload(oldInstance);
reloadable->reload(oldInstance);
} else if (newObj != nullptr) {
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
}

View file

@ -21,3 +21,7 @@ endif()
if (SERVICE_UPOWER)
add_subdirectory(upower)
endif()
if (SERVICE_NOTIFICATIONS)
add_subdirectory(notifications)
endif()

View 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)

View 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;
}
}

View 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;
};
}

View 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; }
}

View 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;
};
}

View 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>

View 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;
}
}
}

View 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

View 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();
}
}

View 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};
};
}

View file

@ -236,7 +236,8 @@ DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; }
void StatusNotifierItem::refMenu() {
this->menuRefcount++;
qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" << this->menuRefcount;
qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now"
<< this->menuRefcount;
if (this->menuRefcount == 1) {
this->onMenuPathChanged();
@ -249,7 +250,8 @@ void StatusNotifierItem::refMenu() {
void StatusNotifierItem::unrefMenu() {
this->menuRefcount--;
qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" << this->menuRefcount;
qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now"
<< this->menuRefcount;
if (this->menuRefcount == 0) {
this->onMenuPathChanged();
@ -258,7 +260,7 @@ void StatusNotifierItem::unrefMenu() {
void StatusNotifierItem::onMenuPathChanged() {
qCDebug(logSniMenu) << "Updating menu of" << this << "with refcount" << this->menuRefcount
<< "path" << this->menuPath.get().path();
<< "path" << this->menuPath.get().path();
if (this->mMenu) {
this->mMenu->deleteLater();

View file

@ -38,6 +38,10 @@ enum Enum {
/// No keyboard input will be accepted.
None = 0,
/// Exclusive access to the keyboard, locking out all other windows.
///
/// > [!WARNING] You **CANNOT** use this to make a secure lock screen.
/// >
/// > If you want to make a lock screen, use [WlSessionLock](../wlsessionlock).
Exclusive = 1,
/// Access to the keyboard as determined by the operating system.
///