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

KDE's Doxygen guidelines are available online.