Akonadi

resourcescheduler.cpp
1/*
2 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "resourcescheduler_p.h"
8
9#include "recursivemover_p.h"
10#include <QDBusConnection>
11
12#include "akonadiagentbase_debug.h"
13#include "private/instance_p.h"
14#include <KLocalizedString>
15
16#include <QDBusInterface>
17#include <QTimer>
18
19using namespace Akonadi;
20using namespace std::chrono_literals;
21qint64 ResourceScheduler::Task::latestSerial = 0;
22static QDBusAbstractInterface *s_resourcetracker = nullptr;
23
24/// @cond PRIVATE
25
26ResourceScheduler::ResourceScheduler(QObject *parent)
27 : QObject(parent)
28{
29}
30
31void ResourceScheduler::scheduleFullSync()
32{
33 Task t;
34 t.type = SyncAll;
35 TaskList &queue = queueForTaskType(t.type);
36 if (queue.contains(t) || mCurrentTask == t) {
37 return;
38 }
39 queue << t;
40 signalTaskToTracker(t, "SyncAll");
41 scheduleNext();
42}
43
44void ResourceScheduler::scheduleCollectionTreeSync()
45{
46 Task t;
47 t.type = SyncCollectionTree;
48 TaskList &queue = queueForTaskType(t.type);
49 if (queue.contains(t) || mCurrentTask == t) {
50 return;
51 }
52 queue << t;
53 signalTaskToTracker(t, "SyncCollectionTree");
54 scheduleNext();
55}
56
57void ResourceScheduler::scheduleTagSync()
58{
59 Task t;
60 t.type = SyncTags;
61 TaskList &queue = queueForTaskType(t.type);
62 if (queue.contains(t) || mCurrentTask == t) {
63 return;
64 }
65 queue << t;
66 signalTaskToTracker(t, "SyncTags");
67 scheduleNext();
68}
69
70void ResourceScheduler::scheduleRelationSync()
71{
72 Task t;
73 t.type = SyncRelations;
74 TaskList &queue = queueForTaskType(t.type);
75 if (queue.contains(t) || mCurrentTask == t) {
76 return;
77 }
78 queue << t;
79 signalTaskToTracker(t, "SyncRelations");
80 scheduleNext();
81}
82
83void ResourceScheduler::scheduleSync(const Collection &col)
84{
85 Task t;
86 t.type = SyncCollection;
87 t.collection = col;
88 TaskList &queue = queueForTaskType(t.type);
89 if (queue.contains(t) || mCurrentTask == t) {
90 return;
91 }
92 queue << t;
93 signalTaskToTracker(t, "SyncCollection", QString::number(col.id()));
94 scheduleNext();
95}
96
97void ResourceScheduler::scheduleAttributesSync(const Collection &collection)
98{
99 Task t;
100 t.type = SyncCollectionAttributes;
101 t.collection = collection;
102
103 TaskList &queue = queueForTaskType(t.type);
104 if (queue.contains(t) || mCurrentTask == t) {
105 return;
106 }
107 queue << t;
108 signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id()));
109 scheduleNext();
110}
111
112void ResourceScheduler::scheduleItemFetch(const Akonadi::Item &item, const QSet<QByteArray> &parts, const QList<QDBusMessage> &msgs, qint64 parentId)
113
114{
115 Task t;
116 t.type = FetchItem;
117 t.items << item;
118 t.itemParts = parts;
119 t.dbusMsgs = msgs;
120 t.argument = parentId;
121
122 TaskList &queue = queueForTaskType(t.type);
123 queue << t;
124
125 signalTaskToTracker(t, "FetchItem", QString::number(item.id()));
126 scheduleNext();
127}
128
129void ResourceScheduler::scheduleItemsFetch(const Item::List &items, const QSet<QByteArray> &parts, const QDBusMessage &msg)
130{
131 Task t;
132 t.type = FetchItems;
133 t.items = items;
134 t.itemParts = parts;
135
136 // if the current task does already fetch the requested item, break here but
137 // keep the dbus message, so we can send the reply later on
138 if (mCurrentTask == t) {
139 mCurrentTask.dbusMsgs << msg;
140 return;
141 }
142
143 // If this task is already in the queue, merge with it.
144 TaskList &queue = queueForTaskType(t.type);
145 const int idx = queue.indexOf(t);
146 if (idx != -1) {
147 queue[idx].dbusMsgs << msg;
148 return;
149 }
150
151 t.dbusMsgs << msg;
152 queue << t;
153
154 QStringList ids;
155 ids.reserve(items.size());
156 for (const auto &item : items) {
157 ids.push_back(QString::number(item.id()));
158 }
159 signalTaskToTracker(t, "FetchItems", ids.join(QLatin1StringView(", ")));
160 scheduleNext();
161}
162
163void ResourceScheduler::scheduleResourceCollectionDeletion()
164{
165 Task t;
166 t.type = DeleteResourceCollection;
167 TaskList &queue = queueForTaskType(t.type);
168 if (queue.contains(t) || mCurrentTask == t) {
169 return;
170 }
171 queue << t;
172 signalTaskToTracker(t, "DeleteResourceCollection");
173 scheduleNext();
174}
175
176void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection)
177{
178 Task t;
179 t.type = InvalideCacheForCollection;
180 t.collection = collection;
181 TaskList &queue = queueForTaskType(t.type);
182 if (queue.contains(t) || mCurrentTask == t) {
183 return;
184 }
185 queue << t;
186 signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id()));
187 scheduleNext();
188}
189
190void ResourceScheduler::scheduleChangeReplay()
191{
192 Task t;
193 t.type = ChangeReplay;
194 TaskList &queue = queueForTaskType(t.type);
195 // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks
196 if (queue.contains(t)) {
197 return;
198 }
199 queue << t;
200 signalTaskToTracker(t, "ChangeReplay");
201 scheduleNext();
202}
203
204void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover)
205{
206 Task t;
207 t.type = RecursiveMoveReplay;
208 t.collection = movedCollection;
209 t.argument = QVariant::fromValue(mover);
210 TaskList &queue = queueForTaskType(t.type);
211
212 if (queue.contains(t) || mCurrentTask == t) {
213 return;
214 }
215
216 queue << t;
217 signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id()));
218 scheduleNext();
219}
220
221void Akonadi::ResourceScheduler::scheduleFullSyncCompletion()
222{
223 Task t;
224 t.type = SyncAllDone;
225 TaskList &queue = queueForTaskType(t.type);
226 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
227 queue << t;
228 signalTaskToTracker(t, "SyncAllDone");
229 scheduleNext();
230}
231
232void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion()
233{
234 Task t;
235 t.type = SyncCollectionTreeDone;
236 TaskList &queue = queueForTaskType(t.type);
237 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
238 queue << t;
239 signalTaskToTracker(t, "SyncCollectionTreeDone");
240 scheduleNext();
241}
242
243void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver,
244 const char *methodName,
245 const QVariant &argument,
247{
248 Task t;
249 t.type = Custom;
250 t.receiver = receiver;
251 t.methodName = methodName;
252 t.argument = argument;
253 QueueType queueType = GenericTaskQueue;
254 if (priority == ResourceBase::AfterChangeReplay) {
255 queueType = AfterChangeReplayQueue;
256 } else if (priority == ResourceBase::Prepend) {
257 queueType = PrependTaskQueue;
258 }
259 TaskList &queue = mTaskList[queueType];
260
261 if (queue.contains(t)) {
262 return;
263 }
264
265 switch (priority) {
267 queue.prepend(t);
268 break;
269 default:
270 queue.append(t);
271 break;
272 }
273
274 signalTaskToTracker(t, "Custom-" + t.methodName);
275 scheduleNext();
276}
277
278void ResourceScheduler::taskDone()
279{
280 if (isEmpty()) {
281 Q_EMIT status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready"));
282 }
283
284 if (s_resourcetracker) {
285 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
286 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
287 }
288
289 mCurrentTask = Task();
290 mCurrentTasksQueue = -1;
291 scheduleNext();
292}
293
294void ResourceScheduler::itemFetchDone(const QString &msg)
295{
296 Q_ASSERT(mCurrentTask.type == FetchItem);
297
298 TaskList &queue = queueForTaskType(mCurrentTask.type);
299
300 const qint64 parentId = mCurrentTask.argument.toLongLong();
301 // msg is empty, there was no error
302 if (msg.isEmpty() && !queue.isEmpty()) {
303 Task &nextTask = queue[0];
304 // If the next task is FetchItem too...
305 if (nextTask.type != mCurrentTask.type || nextTask.argument.toLongLong() != parentId) {
306 // If the next task is not FetchItem or the next FetchItem task has
307 // different parentId then this was the last task in the series, so
308 // send the DBus replies.
309 mCurrentTask.sendDBusReplies(msg);
310 }
311 } else {
312 // msg was not empty, there was an error.
313 // remove all subsequent FetchItem tasks with the same parentId
314 auto iter = queue.begin();
315 while (iter != queue.end()) {
316 if (iter->type != mCurrentTask.type || iter->argument.toLongLong() == parentId) {
317 iter = queue.erase(iter);
318 continue;
319 } else {
320 break;
321 }
322 }
323
324 // ... and send DBus reply with the error message
325 mCurrentTask.sendDBusReplies(msg);
326 }
327
328 taskDone();
329}
330
331void ResourceScheduler::deferTask()
332{
333 if (mCurrentTask.type == Invalid) {
334 return;
335 }
336
337 if (s_resourcetracker) {
338 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
339 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
340 }
341
342 Task t = mCurrentTask;
343 mCurrentTask = Task();
344
345 Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount);
346 mTaskList[mCurrentTasksQueue].prepend(t);
347 mCurrentTasksQueue = -1;
348
349 signalTaskToTracker(t, "DeferedTask");
350
351 scheduleNext();
352}
353
354bool ResourceScheduler::isEmpty()
355{
356 for (int i = 0; i < NQueueCount; ++i) {
357 if (!mTaskList[i].isEmpty()) {
358 return false;
359 }
360 }
361 return true;
362}
363
364void ResourceScheduler::scheduleNext()
365{
366 if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) {
367 return;
368 }
369 QTimer::singleShot(0s, this, &ResourceScheduler::executeNext);
370}
371
372void ResourceScheduler::executeNext()
373{
374 if (mCurrentTask.type != Invalid || isEmpty()) {
375 return;
376 }
377
378 for (int i = 0; i < NQueueCount; ++i) {
379 if (!mTaskList[i].isEmpty()) {
380 mCurrentTask = mTaskList[i].takeFirst();
381 mCurrentTasksQueue = i;
382 break;
383 }
384 }
385
386 if (s_resourcetracker) {
387 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial)};
388 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList);
389 }
390
391 switch (mCurrentTask.type) {
392 case SyncAll:
393 Q_EMIT executeFullSync();
394 break;
395 case SyncCollectionTree:
396 Q_EMIT executeCollectionTreeSync();
397 break;
398 case SyncCollection:
399 Q_EMIT executeCollectionSync(mCurrentTask.collection);
400 break;
401 case SyncCollectionAttributes:
402 Q_EMIT executeCollectionAttributesSync(mCurrentTask.collection);
403 break;
404 case SyncTags:
405 Q_EMIT executeTagSync();
406 break;
407 case FetchItem:
408 Q_EMIT executeItemFetch(mCurrentTask.items.at(0), mCurrentTask.itemParts);
409 break;
410 case FetchItems:
411 Q_EMIT executeItemsFetch(mCurrentTask.items, mCurrentTask.itemParts);
412 break;
413 case DeleteResourceCollection:
414 Q_EMIT executeResourceCollectionDeletion();
415 break;
416 case InvalideCacheForCollection:
417 Q_EMIT executeCacheInvalidation(mCurrentTask.collection);
418 break;
419 case ChangeReplay:
420 Q_EMIT executeChangeReplay();
421 break;
422 case RecursiveMoveReplay:
423 Q_EMIT executeRecursiveMoveReplay(mCurrentTask.argument.value<RecursiveMover *>());
424 break;
425 case SyncAllDone:
426 Q_EMIT fullSyncComplete();
427 break;
428 case SyncCollectionTreeDone:
429 Q_EMIT collectionTreeSyncComplete();
430 break;
431 case SyncRelations:
432 Q_EMIT executeRelationSync();
433 break;
434 case Custom: {
435 const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)");
436 const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1;
437 bool success = false;
438 if (hasSlotWithVariant) {
439 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument));
440 Q_ASSERT_X(success || !mCurrentTask.argument.isValid(),
441 "ResourceScheduler::executeNext",
442 "Valid argument was provided but the method wasn't found");
443 }
444 if (!success) {
445 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData());
446 }
447
448 if (!success) {
449 qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument"
450 << mCurrentTask.argument;
451 }
452 break;
453 }
454 default: {
455 qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type;
456 dump();
457 Q_ASSERT(false);
458 }
459 }
460}
461
462ResourceScheduler::Task ResourceScheduler::currentTask() const
463{
464 return mCurrentTask;
465}
466
467ResourceScheduler::Task &ResourceScheduler::currentTask()
468{
469 return mCurrentTask;
470}
471
472void ResourceScheduler::setOnline(bool state)
473{
474 if (mOnline == state) {
475 return;
476 }
477 mOnline = state;
478 if (mOnline) {
479 scheduleNext();
480 } else {
481 if (mCurrentTask.type != Invalid) {
482 // abort running task
483 queueForTaskType(mCurrentTask.type).prepend(mCurrentTask);
484 mCurrentTask = Task();
485 mCurrentTasksQueue = -1;
486 }
487 // abort pending synchronous tasks, might take longer until the resource goes online again
488 TaskList &itemFetchQueue = queueForTaskType(FetchItem);
489 qint64 parentId = -1;
490 Task lastTask;
491 for (QList<Task>::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) {
492 if ((*it).type == FetchItem) {
493 qint64 idx = it->argument.toLongLong();
494 if (parentId == -1) {
495 parentId = idx;
496 }
497 if (idx != parentId) {
498 // Only emit the DBus reply once we reach the last taskwith the
499 // same "idx"
500 lastTask.sendDBusReplies(i18nc("@info", "Job canceled."));
501 parentId = idx;
502 }
503 lastTask = (*it);
504 it = itemFetchQueue.erase(it);
505 if (s_resourcetracker) {
506 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), i18nc("@info", "Job canceled.")};
507 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
508 }
509 } else {
510 ++it;
511 }
512 }
513 }
514}
515
516void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString)
517{
518 // if there's a job tracer running, tell it about the new job
519 if (!s_resourcetracker) {
520 const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier();
521 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) {
522 s_resourcetracker = new QDBusInterface(QLatin1StringView("org.kde.akonadiconsole") + suffix,
523 QStringLiteral("/resourcesJobtracker"),
524 QStringLiteral("org.freedesktop.Akonadi.JobTracker"),
526 nullptr);
527 }
528 }
529
530 if (s_resourcetracker) {
531 const QList<QVariant> argumentList = QList<QVariant>() << static_cast<AgentBase *>(parent())->identifier() // "session" (in our case resource)
532 << QString::number(task.serial) // "job"
533 << QString() // "parent job"
534 << QString::fromLatin1(taskType) // "job type"
535 << debugString // "job debugging string"
536 ;
537 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList);
538 }
539}
540
541void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection)
542{
543 if (!collection.isValid()) { // should not happen, but you never know...
544 return;
545 }
546 TaskList &queue = queueForTaskType(SyncCollection);
547 for (QList<Task>::iterator it = queue.begin(); it != queue.end();) {
548 if ((*it).type == SyncCollection && (*it).collection == collection) {
549 it = queue.erase(it);
550 qCDebug(AKONADIAGENTBASE_LOG) << " erasing";
551 } else {
552 ++it;
553 }
554 }
555}
556
557void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg)
558{
559 for (const QDBusMessage &msg : std::as_const(dbusMsgs)) {
560 qCDebug(AKONADIAGENTBASE_LOG) << "Sending dbus reply for method" << methodName << "with error" << errorMsg;
561 QDBusMessage reply;
562 if (!errorMsg.isEmpty()) {
563 reply = msg.createErrorReply(QDBusError::Failed, errorMsg);
564 } else if (msg.member() == QLatin1StringView("requestItemDelivery")) {
565 reply = msg.createReply();
566 } else if (msg.member().isEmpty()) {
567 continue; // unittest calls scheduleItemFetch with empty QDBusMessage
568 } else {
569 qCCritical(AKONADIAGENTBASE_LOG) << "ResourceScheduler: got unexpected method name :" << msg.member();
570 }
572 }
573}
574
575ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type)
576{
577 switch (type) {
578 case ChangeReplay:
579 case RecursiveMoveReplay:
580 return ChangeReplayQueue;
581 case FetchItem:
582 case FetchItems:
583 case SyncCollectionAttributes:
584 return UserActionQueue;
585 default:
586 return GenericTaskQueue;
587 }
588}
589
590ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type)
591{
592 const QueueType qt = queueTypeForTaskType(type);
593 return mTaskList[qt];
594}
595
596void ResourceScheduler::dump() const
597{
598 qCDebug(AKONADIAGENTBASE_LOG) << dumpToString();
599}
600
601QString ResourceScheduler::dumpToString() const
602{
603 QString ret;
604 QTextStream str(&ret);
605 str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << '\n';
606 str << " current task: " << mCurrentTask << '\n';
607 for (int i = 0; i < NQueueCount; ++i) {
608 const TaskList &queue = mTaskList[i];
609 if (queue.isEmpty()) {
610 str << " queue " << i << " is empty" << '\n';
611 } else {
612 str << " queue " << i << " " << queue.size() << " tasks:\n";
613 const QList<Task>::const_iterator queueEnd(queue.constEnd());
614 for (QList<Task>::const_iterator it = queue.constBegin(); it != queueEnd; ++it) {
615 str << " " << (*it) << '\n';
616 }
617 }
618 }
619 str.flush();
620 return ret;
621}
622
623void ResourceScheduler::clear()
624{
625 qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:";
626 for (int i = 0; i < NQueueCount; ++i) {
627 TaskList &queue = mTaskList[i];
628 queue.clear();
629 }
630 mCurrentTask = Task();
631 mCurrentTasksQueue = -1;
632}
633
634void Akonadi::ResourceScheduler::cancelQueues()
635{
636 for (int i = 0; i < NQueueCount; ++i) {
637 TaskList &queue = mTaskList[i];
638 if (s_resourcetracker) {
639 for (const Task &t : queue) {
640 QList<QVariant> argumentList{QString::number(t.serial), QString()};
641 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
642 }
643 }
644 queue.clear();
645 }
646}
647
648static const char s_taskTypes[][27] = {"Invalid (no task)",
649 "SyncAll",
650 "SyncCollectionTree",
651 "SyncCollection",
652 "SyncCollectionAttributes",
653 "SyncTags",
654 "FetchItem",
655 "FetchItems",
656 "ChangeReplay",
657 "RecursiveMoveReplay",
658 "DeleteResourceCollection",
659 "InvalideCacheForCollection",
660 "SyncAllDone",
661 "SyncCollectionTreeDone",
662 "SyncRelations",
663 "Custom"};
664
665QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task)
666{
667 d << task.serial << " " << s_taskTypes[task.type] << " ";
668 if (task.type != ResourceScheduler::Invalid) {
669 if (task.collection.isValid()) {
670 d << "collection " << task.collection.id() << " ";
671 }
672 if (!task.items.isEmpty()) {
673 QStringList ids;
674 ids.reserve(task.items.size());
675 for (const auto &item : std::as_const(task.items)) {
676 ids.push_back(QString::number(item.id()));
677 }
678 d << "items " << ids.join(QLatin1StringView(", ")) << " ";
679 }
680 if (!task.methodName.isEmpty()) {
681 d << task.methodName << " " << task.argument.toString();
682 }
683 }
684 return d;
685}
686
687QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task)
688{
689 QString s;
690 QTextStream str(&s);
691 str << task;
692 d << s;
693 return d;
694}
695
696/// @endcond
697
698#include "moc_resourcescheduler_p.cpp"
The base class for all Akonadi agents and resources.
Definition agentbase.h:73
@ Idle
The agent does currently nothing.
Definition agentbase.h:422
Represents a collection of PIM items.
Definition collection.h:62
Represents a PIM item stored in Akonadi storage.
Definition item.h:101
Id id() const
Returns the unique identifier of the item.
Definition item.cpp:63
SchedulePriority
Describes the scheduling priority of a task that has been queued for execution.
@ Prepend
The task will be executed as soon as the current task has finished.
@ AfterChangeReplay
The task is scheduled after the last ChangeReplay task in the queue.
Q_SCRIPTABLE CaptureState status()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Helper integration between Akonadi and Qt.
A glue between Qt and the standard library.
const char * constData() const const
QDBusPendingCall asyncCallWithArgumentList(const QString &method, const QList< QVariant > &args)
bool send(const QDBusMessage &message) const const
QDBusConnection sessionBus()
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
void clear()
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString join(QChar separator) const const
QTaskBuilder< Task > task(Task &&task)
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.