quickshell/src/services/notifications/notification.cpp
outfoxxed b67f92bc13
all: use BINDABLE only with trivial setters
Fixes various bugs caused by the QML engine bypassing setters
when BINDABLE is specified (even if the bindable is const).
Also restructures all properties using BINDABLE to have
a default READ and WRITE to ensure this doesn't happen again.
2025-05-29 16:08:39 -07:00

211 lines
6.1 KiB
C++

#include "notification.hpp"
#include <qcontainerfwd.h>
#include <qdbusargument.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/desktopentry.hpp"
#include "../../core/iconimageprovider.hpp"
#include "dbusimage.hpp"
#include "server.hpp"
namespace qs::service::notifications {
// NOLINTNEXTLINE(misc-use-internal-linkage)
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() {
if (this->notification->isRetained()) {
qCritical() << "Cannot invoke destroyed notification" << this;
return;
}
NotificationServer::instance()->ActionInvoked(this->notification->id(), this->mIdentifier);
if (!this->notification->bindableResident().value()) {
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) {
if (this->isRetained()) {
qCritical() << "Cannot close destroyed notification" << this;
return;
}
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
) {
Qt::beginPropertyUpdateGroup();
this->bExpireTimeout = expireTimeout;
this->bAppName = appName;
this->bSummary = summary;
this->bBody = body;
this->bHasActionIcons = hints.value("action-icons").toBool();
this->bResident = hints.value("resident").toBool();
this->bTransient = hints.value("transient").toBool();
this->bDesktopEntry = hints.value("desktop-entry").toString();
this->bUrgency = hints.contains("urgency")
? hints.value("urgency").value<NotificationUrgency::Enum>()
: NotificationUrgency::Normal;
if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) {
if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) {
appIcon = entry->mIcon;
}
}
this->bAppIcon = appIcon;
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";
QString imagePath;
if (imageDataName.isEmpty()) {
this->mImagePixmap.clear();
} else {
auto value = hints.value(imageDataName).value<QDBusArgument>();
value >> this->mImagePixmap.writeImage();
imagePath = this->mImagePixmap.url();
}
// don't store giant byte arrays longer than necessary
hints.remove("image-data");
hints.remove("image_data");
hints.remove("icon_data");
if (!this->mImagePixmap.hasData()) {
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, "");
}
}
}
this->bImage = imagePath;
this->bHints = hints;
Qt::endPropertyUpdateGroup();
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 (actionsChanged) emit this->actionsChanged();
for (auto* action: deletedActions) {
delete action;
}
}
quint32 Notification::id() const { return this->mId; }
bool Notification::isTracked() const { return this->mCloseReason == 0; }
QList<NotificationAction*> Notification::actions() const { return this->mActions; }
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; }
} // namespace qs::service::notifications