Plasma-workspace

notification.cpp
1/*
2 SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
3 SPDX-FileCopyrightText: 2017 David Edmundson <davidedmundson@kde.org>
4 SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
5
6 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7*/
8
9#include "notification.h"
10#include "notification_p.h"
11
12#include <algorithm>
13
14#include <QDBusArgument>
15#include <QDebug>
16#include <QImageReader>
17#include <QRegularExpression>
18#include <QXmlStreamReader>
19
20#include <KApplicationTrader>
21#include <KConfig>
22#include <KConfigGroup>
23#include <KService>
24
25#include "debug.h"
26
27using namespace NotificationManager;
28using namespace Qt::StringLiterals;
29
30QCache<uint, QImage> Notification::Private::s_imageCache = QCache<uint, QImage>{};
31
32Notification::Private::Private()
33{
34}
35
36Notification::Private::~Private()
37{
38 // The image cache is cleared by AbstractNotificationsModel::pendingRemovalTimer
39}
40
41QString Notification::Private::sanitize(const QString &text)
42{
43 // replace all \ns with <br/>
44 QString t = text;
45
46 t.replace(QLatin1String("\n"), QStringLiteral("<br/>"));
47 // Now remove all inner whitespace (\ns are already <br/>s)
48 t = std::move(t).simplified();
49 // Replace <br></br> with just <br/> in case someone is mad enough to send that
50 static const QRegularExpression brOpenCloseExpr(QStringLiteral("<br>\\s*</br>"));
51 t.replace(brOpenCloseExpr, QLatin1String("<br/>"));
52 // Replace single <br> with <br/> as QXmlStreamReader requires it
53 t.replace(QStringLiteral("<br>"), QLatin1String("<br/>"));
54 // Finally, check if we don't have multiple <br/>s following,
55 // can happen for example when "\n \n" is sent, this replaces
56 // all <br/>s in succession with just one. Also remove extra whitespace after any newline
57 static const QRegularExpression brExpr(QStringLiteral("<br/>(\\s|<br/>)+"));
58 t.replace(brExpr, QLatin1String("<br/>"));
59 // This fancy RegExp escapes every occurrence of & since QtQuick Text will blatantly cut off
60 // text where it finds a stray ampersand.
61 // Only &{apos, quot, gt, lt, amp}; as well as &#123 character references will be allowed
62 static const QRegularExpression escapeExpr(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)"));
63 t.replace(escapeExpr, QLatin1String("&amp;"));
64
65 // Don't bother adding some HTML structure if the body is now empty
66 if (t.isEmpty()) {
67 return t;
68 }
69
70 t = u"<html>" + std::move(t) + u"</html>";
72 QString result;
73 QXmlStreamWriter out(&result);
74
75 static constexpr std::array<QStringView, 10> allowedTags{u"b", u"i", u"u", u"img", u"a", u"html", u"br", u"table", u"tr", u"td"};
76
77 out.writeStartDocument();
78 while (!r.atEnd()) {
79 r.readNext();
80
81 if (r.tokenType() == QXmlStreamReader::StartElement) {
82 const QStringView name = r.name();
83 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
84 continue;
85 }
86 out.writeStartElement(name);
87 if (name == QLatin1String("img")) {
88 const QString src = r.attributes().value("src").toString();
89 const QStringView alt = r.attributes().value("alt");
90
91 const QUrl url(src);
92 if (url.isLocalFile()) {
93 out.writeAttribute(QStringLiteral("src"), src);
94 } else {
95 // image denied for security reasons! Do not copy the image src here!
96 }
97
98 out.writeAttribute(u"alt", alt);
99 }
100 if (name == QLatin1Char('a')) {
101 out.writeAttribute(u"href", r.attributes().value("href"));
102 }
103 }
104
105 if (r.tokenType() == QXmlStreamReader::EndElement) {
106 const QStringView name = r.name();
107 if (std::ranges::find(allowedTags, name) == allowedTags.end()) {
108 continue;
109 }
110 out.writeEndElement();
111 }
112
113 if (r.tokenType() == QXmlStreamReader::Characters) {
114 out.writeCharacters(r.text()); // this auto escapes chars -> HTML entities
115 }
116 }
117 out.writeEndDocument();
118
119 if (r.hasError()) {
120 qCWarning(NOTIFICATIONMANAGER) << "Notification to send to backend contains invalid XML: " << r.errorString() << "line" << r.lineNumber() << "col"
121 << r.columnNumber();
122 }
123
124 // The Text.StyledText format handles only html3.2 stuff and &apos; is html4 stuff
125 // so we need to replace it here otherwise it will not render at all.
126 result.replace(QLatin1String("&apos;"), QChar(u'\''));
127
128 return result;
129}
130
131QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgument &arg)
132{
133 int width, height, rowStride, hasAlpha, bitsPerSample, channels;
134 QByteArray pixels;
135 char *ptr;
136 char *end;
137
139 return QImage();
140 }
141 arg.beginStructure();
142 arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels;
143 arg.endStructure();
144
145#define SANITY_CHECK(condition) \
146 if (!(condition)) { \
147 qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \
148 return QImage(); \
149 }
150
151 SANITY_CHECK(width > 0);
152 SANITY_CHECK(width < 2048);
153 SANITY_CHECK(height > 0);
154 SANITY_CHECK(height < 2048);
155 SANITY_CHECK(rowStride > 0);
156
157#undef SANITY_CHECK
158
159 auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) {
160 const char *end = src + width * 3;
161 for (; src != end; ++dst, src += 3) {
162 *dst = qRgb(src[0], src[1], src[2]);
163 }
164 };
165
166 auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) {
167 const char *end = src + width * 4;
168 for (; src != end; ++dst, src += 4) {
169 *dst = qRgba(src[0], src[1], src[2], src[3]);
170 }
171 };
172
174 void (*fcn)(QRgb *, const char *, int) = nullptr;
175 if (bitsPerSample == 8) {
176 if (channels == 4) {
177 format = QImage::Format_ARGB32;
178 fcn = copyLineARGB32;
179 } else if (channels == 3) {
180 format = QImage::Format_RGB32;
181 fcn = copyLineRGB32;
182 }
183 }
184 if (format == QImage::Format_Invalid) {
185 qCWarning(NOTIFICATIONMANAGER) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels
186 << ")";
187 return QImage();
188 }
189
190 QImage image(width, height, format);
191 ptr = pixels.data();
192 end = ptr + pixels.length();
193 for (int y = 0; y < height; ++y, ptr += rowStride) {
194 if (ptr + channels * width > end) {
195 qCWarning(NOTIFICATIONMANAGER) << "Image data is incomplete. y:" << y << "height:" << height;
196 break;
197 }
198 fcn((QRgb *)image.scanLine(y), ptr, width);
199 }
200
201 return image;
202}
203
204void Notification::Private::sanitizeImage(QImage &image)
205{
206 if (image.isNull()) {
207 return;
208 }
209
210 const QSize max = maximumImageSize();
211 if (image.size().width() > max.width() || image.size().height() > max.height()) {
213 }
214}
215
216void Notification::Private::loadImagePath(const QString &path)
217{
218 // image_path and appIcon should either be a URL with file scheme or the name of a themed icon.
219 // We're lenient and also allow local paths.
220
221 s_imageCache.remove(id); // clear
222 icon.clear();
223
224 QUrl imageUrl;
225 if (path.startsWith(QLatin1Char('/'))) {
226 imageUrl = QUrl::fromLocalFile(path);
227 } else if (path.contains(QLatin1Char('/'))) { // bad heuristic to detect a URL
228 imageUrl = QUrl(path);
229
230 if (!imageUrl.isLocalFile()) {
231 qCDebug(NOTIFICATIONMANAGER) << "Refused to load image from" << path << "which isn't a valid local location.";
232 return;
233 }
234 }
235
236 if (!imageUrl.isValid()) {
237 // try icon path instead;
238 icon = path;
239 return;
240 }
241
242 QImageReader reader(imageUrl.toLocalFile());
243 reader.setAutoTransform(true);
244
245 if (QSize imageSize = reader.size(); imageSize.isValid()) {
246 if (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height()) {
247 imageSize = imageSize.scaled(maximumImageSize(), Qt::KeepAspectRatio);
248 reader.setScaledSize(imageSize);
249 }
250 s_imageCache.insert(id, new QImage(reader.read()), imageSize.width() * imageSize.height());
251 }
252}
253
254QString Notification::Private::defaultComponentName()
255{
256 // NOTE Keep in sync with KNotification
257 return QStringLiteral("plasma_workspace");
258}
259
260constexpr QSize Notification::Private::maximumImageSize()
261{
262 return QSize(256, 256);
263}
264
265KService::Ptr Notification::Private::serviceForDesktopEntry(const QString &desktopEntry)
266{
267 if (desktopEntry.isEmpty()) {
268 return {};
269 }
270
271 KService::Ptr service;
272
273 if (desktopEntry.startsWith(QLatin1Char('/'))) {
274 service = KService::serviceByDesktopPath(desktopEntry);
275 } else {
276 service = KService::serviceByDesktopName(desktopEntry);
277 }
278
279 if (!service) {
280 const QString lowerDesktopEntry = desktopEntry.toLower();
281 service = KService::serviceByDesktopName(lowerDesktopEntry);
282 }
283
284 // Try if it's a renamed flatpak
285 if (!service) {
286 const QString desktopId = desktopEntry + QLatin1String(".desktop");
287
288 const auto services = KApplicationTrader::query([&desktopId](const KService::Ptr &app) -> bool {
289 const QStringList renamedFrom = app->property<QStringList>(QStringLiteral("X-Flatpak-RenamedFrom"));
290 return renamedFrom.contains(desktopId);
291 });
292
293 if (!services.isEmpty()) {
294 service = services.first();
295 }
296 }
297
298 // Try snap instance name.
299 if (!service) {
300 const auto services = KApplicationTrader::query([&desktopEntry](const KService::Ptr &app) -> bool {
301 const QString snapInstanceName = app->property<QString>(QStringLiteral("X-SnapInstanceName"));
302 return desktopEntry.compare(snapInstanceName, Qt::CaseInsensitive) == 0;
303 });
304
305 if (!services.isEmpty()) {
306 service = services.first();
307 }
308 }
309
310 return service;
311}
312
313void Notification::Private::setDesktopEntry(const QString &desktopEntry)
314{
315 QString serviceName;
316
317 configurableService = false;
318
319 KService::Ptr service = serviceForDesktopEntry(desktopEntry);
320 if (service) {
321 this->desktopEntry = service->desktopEntryName();
322 serviceName = service->name();
323 applicationIconName = service->icon();
324 configurableService = !service->noDisplay();
325 }
326
327 const bool isDefaultEvent = (notifyRcName == defaultComponentName());
328 configurableNotifyRc = false;
329 if (!notifyRcName.isEmpty()) {
330 // Check whether the application actually has notifications we can configure
331 KConfig config(notifyRcName + QStringLiteral(".notifyrc"), KConfig::NoGlobals);
332
333 QStringList configSources =
334 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications6/%1.notifyrc").arg(notifyRcName));
335 // Keep compatibility with KF5 applications
336 if (configSources.isEmpty()) {
337 configSources = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/%1.notifyrc").arg(notifyRcName));
338 }
339 // `QStandardPaths` follows the order of precedence given by `$XDG_DATA_DIRS
340 // (more priority goest first), but for `addConfigSources() it is the opposite
341 std::reverse(configSources.begin(), configSources.end());
342 config.addConfigSources(configSources);
343
344 KConfigGroup globalGroup(&config, u"Global"_s);
345
346 const QString iconName = globalGroup.readEntry("IconName");
347
348 // also only overwrite application icon name for non-default events (or if we don't have a service icon)
349 if (!iconName.isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) {
350 applicationIconName = iconName;
351 }
352
353 const QRegularExpression regexp(QStringLiteral("^Event/([^/]*)$"));
354 configurableNotifyRc = !config.groupList().filter(regexp).isEmpty();
355 }
356
357 // For default events we try to show the application name from the desktop entry if possible
358 // This will have us show e.g. "Dr Konqi" instead of generic "Plasma Desktop"
359 // The application may not send an applicationName. Use the name from the desktop entry then
360 if ((isDefaultEvent || applicationName.isEmpty()) && !serviceName.isEmpty()) {
361 applicationName = serviceName;
362 }
363}
364
365void Notification::Private::processHints(const QVariantMap &hints)
366{
367 auto end = hints.end();
368
369 notifyRcName = hints.value(QStringLiteral("x-kde-appname")).toString();
370
371 setDesktopEntry(hints.value(QStringLiteral("desktop-entry")).toString());
372
373 // Special override for KDE Connect since the notification is sent by kdeconnectd
374 // but actually comes from a different app on the phone
375 const QString applicationDisplayName = hints.value(QStringLiteral("x-kde-display-appname")).toString();
376 if (!applicationDisplayName.isEmpty()) {
377 applicationName = applicationDisplayName;
378 }
379
380 originName = hints.value(QStringLiteral("x-kde-origin-name")).toString();
381
382 eventId = hints.value(QStringLiteral("x-kde-eventId")).toString();
383 xdgTokenAppId = hints.value(QStringLiteral("x-kde-xdgTokenAppId")).toString();
384
385 bool ok;
386 const int urgency = hints.value(QStringLiteral("urgency")).toInt(&ok); // DBus type is actually "byte"
387 if (ok) {
388 // FIXME use separate enum again
389 switch (urgency) {
390 case 0:
391 setUrgency(Notifications::LowUrgency);
392 break;
393 case 1:
395 break;
396 case 2:
397 setUrgency(Notifications::CriticalUrgency);
398 break;
399 }
400 }
401
402 resident = hints.value(QStringLiteral("resident")).toBool();
403 transient = hints.value(QStringLiteral("transient")).toBool();
404
405 userActionFeedback = hints.value(QStringLiteral("x-kde-user-action-feedback")).toBool();
406 if (userActionFeedback) {
407 // A confirmation of an explicit user interaction is assumed to have been seen by the user.
408 read = true;
409 }
410
411 urls = QUrl::fromStringList(hints.value(QStringLiteral("x-kde-urls")).toStringList());
412
413 replyPlaceholderText = hints.value(QStringLiteral("x-kde-reply-placeholder-text")).toString();
414 replySubmitButtonText = hints.value(QStringLiteral("x-kde-reply-submit-button-text")).toString();
415 replySubmitButtonIconName = hints.value(QStringLiteral("x-kde-reply-submit-button-icon-name")).toString();
416
417 category = hints.value(QStringLiteral("category")).toString();
418
419 // Underscored hints was in use in version 1.1 of the spec but has been
420 // replaced by dashed hints in version 1.2. We need to support it for
421 // users of the 1.2 version of the spec.
422 auto it = hints.find(QStringLiteral("image-data"));
423 if (it == end) {
424 it = hints.find(QStringLiteral("image_data"));
425 }
426 if (it == end) {
427 // This hint was in use in version 1.0 of the spec but has been
428 // replaced by "image_data" in version 1.1. We need to support it for
429 // users of the 1.0 version of the spec.
430 it = hints.find(QStringLiteral("icon_data"));
431 }
432
433 QImage *image = nullptr;
434 if (it != end) {
435 QImage imageFromHint(decodeNotificationSpecImageHint(it->value<QDBusArgument>()));
436 if (!imageFromHint.isNull()) {
437 Q_ASSERT_X(imageFromHint.width() > 0 && imageFromHint.height() > 0,
438 Q_FUNC_INFO,
439 qPrintable(QStringLiteral("%1 %2").arg(QString::number(imageFromHint.width()), QString::number(imageFromHint.height()))));
440 sanitizeImage(imageFromHint);
441 image = new QImage(imageFromHint);
442 s_imageCache.insert(id, image, imageFromHint.width() * imageFromHint.height());
443 }
444 }
445
446 if (!image) {
447 it = hints.find(QStringLiteral("image-path"));
448 if (it == end) {
449 it = hints.find(QStringLiteral("image_path"));
450 }
451
452 if (it != end) {
453 loadImagePath(it->toString());
454 }
455 }
456
457 this->hints = hints;
458}
459
460void Notification::Private::setUrgency(Notifications::Urgency urgency)
461{
462 this->urgency = urgency;
463
464 // Critical notifications must not time out
465 // TODO should we really imply this here and not on the view side?
466 // are there usecases for critical but can expire?
467 // "critical updates available"?
468 if (urgency == Notifications::CriticalUrgency) {
469 timeout = 0;
470 }
471}
472
473Notification::Notification(uint id)
474 : d(new Private())
475{
476 d->id = id;
477 d->created = QDateTime::currentDateTimeUtc();
478}
479
480Notification::Notification(const Notification &other)
481 : d(new Private(*other.d))
482{
483}
484
485Notification::Notification(Notification &&other) noexcept
486 : d(other.d)
487{
488 other.d = nullptr;
489}
490
491Notification &Notification::operator=(const Notification &other)
492{
493 *d = *other.d;
494 return *this;
495}
496
497Notification &Notification::operator=(Notification &&other) noexcept
498{
499 d = other.d;
500 other.d = nullptr;
501 return *this;
502}
503
504Notification::~Notification()
505{
506 delete d;
507}
508
509uint Notification::id() const
510{
511 return d->id;
512}
513
514QString Notification::dBusService() const
515{
516 return d->dBusService;
517}
518
519void Notification::setDBusService(const QString &dBusService)
520{
521 d->dBusService = dBusService;
522}
523
524QDateTime Notification::created() const
525{
526 return d->created;
527}
528
529void Notification::setCreated(const QDateTime &created)
530{
531 d->created = created;
532}
533
534QDateTime Notification::updated() const
535{
536 return d->updated;
537}
538
539void Notification::resetUpdated()
540{
541 d->updated = QDateTime::currentDateTimeUtc();
542}
543
544bool Notification::read() const
545{
546 return d->read;
547}
548
549void Notification::setRead(bool read)
550{
551 d->read = read;
552}
553
554QString Notification::summary() const
555{
556 return d->summary;
557}
558
559void Notification::setSummary(const QString &summary)
560{
561 d->summary = summary;
562}
563
564QString Notification::body() const
565{
566 return d->body;
567}
568
569void Notification::setBody(const QString &body)
570{
571 d->rawBody = body;
572 d->body = Private::sanitize(body.trimmed());
573}
574
575QString Notification::rawBody() const
576{
577 return d->rawBody;
578}
579
580QString Notification::icon() const
581{
582 return d->icon;
583}
584
585void Notification::setIcon(const QString &icon)
586{
587 d->loadImagePath(icon);
588}
589
590QImage Notification::image() const
591{
592 if (d->s_imageCache.contains(d->id)) {
593 return *d->s_imageCache.object(d->id);
594 }
595 return QImage();
596}
597
598void Notification::setImage(const QImage &image)
599{
600 d->s_imageCache.insert(d->id, new QImage(image));
601}
602
603QString Notification::desktopEntry() const
604{
605 return d->desktopEntry;
606}
607
608void Notification::setDesktopEntry(const QString &desktopEntry)
609{
610 d->setDesktopEntry(desktopEntry);
611}
612
613QString Notification::notifyRcName() const
614{
615 return d->notifyRcName;
616}
617
618QString Notification::eventId() const
619{
620 return d->eventId;
621}
622
623QString Notification::applicationName() const
624{
625 return d->applicationName;
626}
627
628void Notification::setApplicationName(const QString &applicationName)
629{
630 d->applicationName = applicationName;
631}
632
633QString Notification::applicationIconName() const
634{
635 return d->applicationIconName;
636}
637
638void Notification::setApplicationIconName(const QString &applicationIconName)
639{
640 d->applicationIconName = applicationIconName;
641}
642
643QString Notification::originName() const
644{
645 return d->originName;
646}
647
648QStringList Notification::actionNames() const
649{
650 return d->actionNames;
651}
652
653QStringList Notification::actionLabels() const
654{
655 return d->actionLabels;
656}
657
658bool Notification::hasDefaultAction() const
659{
660 return d->hasDefaultAction;
661}
662
663QString Notification::defaultActionLabel() const
664{
665 // Most apps don't expect the default action be visible to the user.
666 // For KDE apps we can assume KNotification does something reasonable.
667 if (!d->notifyRcName.isEmpty() && d->notifyRcName != Private::defaultComponentName()) {
668 return d->defaultActionLabel;
669 } else {
670 return QString(); // Let the UI pick a sensible default.
671 }
672}
673
674void Notification::setActions(const QStringList &actions)
675{
676 if (actions.count() % 2 != 0) {
677 qCWarning(NOTIFICATIONMANAGER) << "List of actions must contain an even number of items, tried to set actions to" << actions;
678 return;
679 }
680
681 d->hasDefaultAction = false;
682 d->hasConfigureAction = false;
683 d->hasReplyAction = false;
684
685 QStringList names;
686 QStringList labels;
687
688 for (int i = 0; i < actions.count(); i += 2) {
689 const QString &name = actions.at(i);
690 const QString &label = actions.at(i + 1);
691
692 if (!d->hasDefaultAction && name == QLatin1String("default")) {
693 d->hasDefaultAction = true;
694 d->defaultActionLabel = label;
695 continue;
696 }
697
698 if (!d->hasConfigureAction && name == QLatin1String("settings")) {
699 d->hasConfigureAction = true;
700 d->configureActionLabel = label;
701 continue;
702 }
703
704 if (!d->hasReplyAction && name == QLatin1String("inline-reply")) {
705 d->hasReplyAction = true;
706 d->replyActionLabel = label;
707 continue;
708 }
709
710 names << name;
711 labels << label;
712 }
713
714 d->actionNames = names;
715 d->actionLabels = labels;
716}
717
718QList<QUrl> Notification::urls() const
719{
720 return d->urls;
721}
722
723void Notification::setUrls(const QList<QUrl> &urls)
724{
725 d->urls = urls;
726}
727
728Notifications::Urgency Notification::urgency() const
729{
730 return d->urgency;
731}
732
733bool Notification::userActionFeedback() const
734{
735 return d->userActionFeedback;
736}
737
738int Notification::timeout() const
739{
740 return d->timeout;
741}
742
743void Notification::setTimeout(int timeout)
744{
745 d->timeout = timeout;
746}
747
748bool Notification::configurable() const
749{
750 return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService;
751}
752
753QString Notification::configureActionLabel() const
754{
755 return d->configureActionLabel;
756}
757
758bool Notification::hasReplyAction() const
759{
760 return d->hasReplyAction;
761}
762
763QString Notification::replyActionLabel() const
764{
765 return d->replyActionLabel;
766}
767
768QString Notification::replyPlaceholderText() const
769{
770 return d->replyPlaceholderText;
771}
772
773QString Notification::replySubmitButtonText() const
774{
775 return d->replySubmitButtonText;
776}
777
778QString Notification::replySubmitButtonIconName() const
779{
780 return d->replySubmitButtonIconName;
781}
782
783QString Notification::category() const
784{
785 return d->category;
786}
787
788bool Notification::expired() const
789{
790 return d->expired;
791}
792
793void Notification::setExpired(bool expired)
794{
795 d->expired = expired;
796}
797
798bool Notification::dismissed() const
799{
800 return d->dismissed;
801}
802
803void Notification::setDismissed(bool dismissed)
804{
805 d->dismissed = dismissed;
806}
807
808bool Notification::resident() const
809{
810 return d->resident;
811}
812
813void Notification::setResident(bool resident)
814{
815 d->resident = resident;
816}
817
818bool Notification::transient() const
819{
820 return d->transient;
821}
822
823void Notification::setTransient(bool transient)
824{
825 d->transient = transient;
826}
827
828QVariantMap Notification::hints() const
829{
830 return d->hints;
831}
832
833void Notification::setHints(const QVariantMap &hints)
834{
835 d->hints = hints;
836}
837
838void Notification::processHints(const QVariantMap &hints)
839{
840 d->processHints(hints);
841}
842
843bool Notification::wasAddedDuringInhibition() const
844{
845 return d->wasAddedDuringInhibition;
846}
847
848void Notification::setWasAddedDuringInhibition(bool value)
849{
850 d->wasAddedDuringInhibition = value;
851}
static Ptr serviceByDesktopName(const QString &_name)
QExplicitlySharedDataPointer< KService > Ptr
static Ptr serviceByDesktopPath(const QString &_path)
Urgency
The notification urgency.
@ LowUrgency
The notification has low urgency, it is not important and may not be shown or added to a history.
@ NormalUrgency
The notification has normal urgency. This is also the default if no urgecny is supplied.
list< Action > actions
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QVariant read(const QByteArray &data, int versionOverride=0)
QString path(const QString &relativePath)
void transient(const QString &message, const QString &title)
QString name(StandardAction id)
Category category(StandardShortcut id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QDateTime currentDateTimeUtc()
void beginStructure()
ElementType currentType() const const
void endStructure()
bool isNull() const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QSize size() const const
int width() const const
iterator begin()
iterator end()
bool isEmpty() const const
int height() const const
bool isValid() const const
int width() const const
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
KeepAspectRatio
CaseInsensitive
SmoothTransformation
QUrl fromLocalFile(const QString &localFile)
QList< QUrl > fromStringList(const QStringList &urls, ParsingMode mode)
bool isLocalFile() const const
bool isValid() const const
QString toLocalFile() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:55:44 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.