Messagelib

modelinvariantrowmapper.cpp
1/******************************************************************************
2 *
3 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 *
7 *******************************************************************************/
8
9#include "core/modelinvariantrowmapper.h"
10#include "core/modelinvariantindex_p.h"
11#include "core/modelinvariantrowmapper_p.h"
12
13#include <QTime>
14#include <QTimer>
15
16#include "messagelist_debug.h"
17
18namespace MessageList
19{
20namespace Core
21{
22class RowShift
23{
24public:
25 int mMinimumRowIndex;
26 int mShift;
28
29public:
30 RowShift(int minRowIndex, int shift, QHash<int, ModelInvariantIndex *> *invariantHash)
31 : mMinimumRowIndex(minRowIndex)
32 , mShift(shift)
33 , mInvariantHash(invariantHash)
34 {
35 }
36
37 ~RowShift()
38 {
39 for (const auto idx : std::as_const(*mInvariantHash)) {
40 idx->d->setRowMapper(nullptr);
41 }
42 delete mInvariantHash;
43 }
44};
45} // namespace Core
46} // namespace MessageList
47
48using namespace MessageList::Core;
49
50ModelInvariantRowMapper::ModelInvariantRowMapper()
51 : d(new ModelInvariantRowMapperPrivate(this))
52{
53 d->mRowShiftList = new QList<RowShift *>();
54 d->mCurrentShiftSerial = 0;
55 d->mCurrentInvariantHash = new QHash<int, ModelInvariantIndex *>();
56 d->mUpdateTimer = new QTimer(this);
57 d->mUpdateTimer->setSingleShot(true);
58 d->mLazyUpdateChunkInterval = 50;
59 d->mLazyUpdateIdleInterval = 50;
60
61 connect(d->mUpdateTimer, &QTimer::timeout, this, [this]() {
62 d->slotPerformLazyUpdate();
63 });
64}
65
66ModelInvariantRowMapper::~ModelInvariantRowMapper()
67{
68 if (d->mUpdateTimer->isActive()) {
69 d->mUpdateTimer->stop();
70 }
71
72 // FIXME: optimize this (it CAN be optimized)
73 for (const auto idx : std::as_const(*d->mCurrentInvariantHash)) {
74 idx->d->setRowMapper(nullptr);
75 }
76 delete d->mCurrentInvariantHash;
77
78 if (d->mRowShiftList) {
79 while (!d->mRowShiftList->isEmpty()) {
80 delete d->mRowShiftList->takeFirst();
81 }
82
83 delete d->mRowShiftList;
84 }
85}
86
87void ModelInvariantRowMapperPrivate::killFirstRowShift()
88{
89 RowShift *shift = mRowShiftList->at(0);
90
91 Q_ASSERT(shift->mInvariantHash->isEmpty());
92
93 delete shift;
94 mRowShiftList->removeAt(0);
95 mRemovedShiftCount++;
96 if (mRowShiftList->isEmpty()) {
97 delete mRowShiftList;
98 mRowShiftList = nullptr;
99 }
100}
101
102void ModelInvariantRowMapperPrivate::indexDead(ModelInvariantIndex *invariant)
103{
104 Q_ASSERT(invariant->d->rowMapper() == q);
105
106 if (invariant->d->rowMapperSerial() == mCurrentShiftSerial) {
107 mCurrentInvariantHash->remove(invariant->d->modelIndexRow());
108 return;
109 }
110
111 Q_ASSERT(invariant->d->rowMapperSerial() < mCurrentShiftSerial);
112
113 if (!mRowShiftList) {
114 return; // not found (not requested yet or invalid index at all)
115 }
116
117 uint invariantShiftIndex = invariant->d->rowMapperSerial() - mRemovedShiftCount;
118
119 Q_ASSERT(invariantShiftIndex < static_cast<uint>(mRowShiftList->count()));
120
121 RowShift *shift = mRowShiftList->at(invariantShiftIndex);
122
123 Q_ASSERT(shift);
124
125 shift->mInvariantHash->remove(invariant->d->modelIndexRow());
126
127 if ((shift->mInvariantHash->isEmpty()) && (invariantShiftIndex == 0)) {
128 // no more invariants with serial <= invariant->d->rowMapperSerial()
129 killFirstRowShift();
130 }
131}
132
133void ModelInvariantRowMapperPrivate::updateModelInvariantIndex(int modelIndexRow, ModelInvariantIndex *invariantToFill)
134{
135 // Here the invariant already belongs to this mapper. We ASSUME that it's somewhere
136 // in the history and not in the hash belonging to the current serial.
137 // modelIndexRow is the CURRENT model index row.
138 Q_ASSERT(invariantToFill->d->rowMapper() == q);
139
140 uint invariantShiftIndex = invariantToFill->d->rowMapperSerial() - mRemovedShiftCount;
141
142 Q_ASSERT(invariantShiftIndex < static_cast<uint>(mRowShiftList->count()));
143
144 RowShift *shift = mRowShiftList->at(invariantShiftIndex);
145
146 int count = shift->mInvariantHash->remove(invariantToFill->d->modelIndexRow());
147
148 Q_ASSERT(count > 0);
149 Q_UNUSED(count)
150
151 // update and make it belong to the current serial
152 invariantToFill->d->setModelIndexRowAndRowMapperSerial(modelIndexRow, mCurrentShiftSerial);
153
154 Q_ASSERT(!mCurrentInvariantHash->contains(invariantToFill->d->modelIndexRow()));
155
156 mCurrentInvariantHash->insert(invariantToFill->d->modelIndexRow(), invariantToFill);
157
158 if ((shift->mInvariantHash->isEmpty()) && (invariantShiftIndex == 0)) {
159 // no more invariants with serial <= invariantToFill->rowMapperSerial()
160 killFirstRowShift();
161 }
162}
163
164ModelInvariantIndex *ModelInvariantRowMapperPrivate::modelIndexRowToModelInvariantIndexInternal(int modelIndexRow, bool updateIfNeeded)
165{
166 // First of all look it up in the current hash
167 ModelInvariantIndex *invariant = mCurrentInvariantHash->value(modelIndexRow, nullptr);
168 if (invariant) {
169 return invariant; // found: was up to date
170 }
171
172 // Go backward in history by unapplying changes
173 if (!mRowShiftList) {
174 return nullptr; // not found (not requested yet or invalid index at all)
175 }
176
177 int idx = mRowShiftList->count();
178 if (idx == 0) {
179 Q_ASSERT(false);
180 return nullptr; // should never happen (mRowShiftList should have been 0), but well...
181 }
182 idx--;
183
184 int previousIndexRow = modelIndexRow;
185
186 while (idx >= 0) {
187 RowShift *shift = mRowShiftList->at(idx);
188
189 // this shift has taken "previousModelIndexRow" in the historic state
190 // and has executed:
191 //
192 // if ( previousIndexRow >= shift->mMinimumRowIndex )
193 // previousIndexRow += shift->mShift;
194 //
195 // so inverting it
196 //
197 // int potentialPreviousModelIndexRow = modelIndexRow - shift->mShift;
198 // if ( potentialPreviousModelIndexRow >= shift->mMinimumRowIndex )
199 // previousIndexRow = potentialPreviousModelIndexRow;
200 //
201 // or by simplifying...
202
203 int potentialPreviousModelIndexRow = previousIndexRow - shift->mShift;
204 if (potentialPreviousModelIndexRow >= shift->mMinimumRowIndex) {
205 previousIndexRow = potentialPreviousModelIndexRow;
206 }
207
208 invariant = shift->mInvariantHash->value(previousIndexRow, nullptr);
209 if (invariant) {
210 // found at this level in history
211 if (updateIfNeeded) { // update it too
212 updateModelInvariantIndex(modelIndexRow, invariant);
213 }
214 return invariant;
215 }
216
217 idx--;
218 }
219
220 qCWarning(MESSAGELIST_LOG) << "Requested invariant for storage row index " << modelIndexRow << " not found in history";
221 return nullptr; // not found in history
222}
223
225{
226 d->mLazyUpdateChunkInterval = chunkInterval;
227}
228
230{
231 d->mLazyUpdateIdleInterval = idleInterval;
232}
233
235{
236 // the invariant shift serial is the serial this mapper
237 // had at the time it emitted the invariant.
238 // mRowShiftList at that time had at most invariantShiftSerial items.
240
241 if (invariant->d->rowMapper() != this) {
242 return -1;
243 }
244
245 if (invariant->d->rowMapperSerial() == d->mCurrentShiftSerial) {
246 Q_ASSERT(d->mCurrentInvariantHash->value(invariant->d->modelIndexRow()) == invariant);
247 return invariant->d->modelIndexRow(); // this invariant was emitted very recently and isn't affected by any change
248 }
249
250 // If RowShift elements weren't removed from the list then
251 // we should have mCurrentShiftSerial items in the list.
252 // But RowShifts ARE removed sequentially from the beginning of the list
253 // as the invariants are updated in the user's data.
254 // We are making sure that if a RowShift belonging to a certain
255 // serial is removed from the list then there are no more
256 // ModelInvariantIndexinstances with that (or a lower) serial around.
257 // Thus invariantShiftSerial is >= mRemovedShiftCount.
258
259 // Example:
260 // Initial state, no shifts, current serial 0, removed shifts 0
261 // Emit ModelInvariantIndexfor model index row 6, with serial 0.
262 // User asks for model index row of invariant that has row index 10 and serial 0.
263 // The serial is equal to the current serial and we return the row index unchanged.
264 // A row arrives at position 4
265 // We add a RowShift with start index 5 and offset +1
266 // We increase current serial to 1
267 // User asks for model index row of invariant that has row index 6 with serial 0.
268 // We compute the first RowShift index as serial 0 - removed 0 = 0
269 // We apply the row shifts starting at that index.
270 // That is, since the requested row index is 6 >= 5
271 // We apply +1 shift and return row index 7 serial 1
272 // User asks for model index row of invariant that has row index 7 with serial 1
273 // The serial is equal to the current serial and we return the row index unchanged still with serial 1
274 // We update all the invariants in the user's data so that
275 // there are no more invariants with serial 0.
276 // We remove the RowShift and increase removed shift count to 1
277 // User asks for model index row of invariant that has row index 7
278 // The ModelInvariantIndex MUST have at least serial 1 because of the removal step above.
279 // The serial is equal to the current serial and we return the row index unchanged still with serial 1
280 // A row arrives at position 2
281 // We add a RowShift with start index 3 and offset +1
282 // We increase current serial to 2
283 // User asks for model index row of invariant that has row index 7 with serial 1.
284 // We compute the first RowShift index as serial 1 - removed 1 = 0
285 // We apply the row shifts starting at that index.
286 // That is, since the requested row index is 7 >= 3
287 // We apply +1 shift and return row index 8 serial 2
288 // User asks for model index row of invariant that has row index 8 and serial 2
289 // The serial is equal to the current serial and we return the row index unchanged still with serial 2
290 // Etc...
291
292 // So if we can trust that the user doesn't mess up with serials
293 // and the requested serial is not equal to the current serial
294 // then we can be 100% sure that mRowShiftList is not null (it contains at least one item).
295 // The requested serial is surely >= than mRemovedShiftCount too.
296
297 // To find the starting index of the RowShifts that apply to this
298 // serial we need to offset them by the removed rows.
299
300 uint invariantShiftIndex = invariant->d->rowMapperSerial() - d->mRemovedShiftCount;
301
302 Q_ASSERT(d->mRowShiftList);
303
304 // For the reasoning above invariantShiftIndex is surely < than mRowShiftList.count()
305
306 const uint count = static_cast<uint>(d->mRowShiftList->count());
307
309
310 int modelIndexRow = invariant->d->modelIndexRow();
311
312 // apply shifts
313 for (uint idx = invariantShiftIndex; idx < count; idx++) {
314 RowShift *shift = d->mRowShiftList->at(idx);
315 if (modelIndexRow >= shift->mMinimumRowIndex) {
316 modelIndexRow += shift->mShift;
317 }
318 }
319
320 // Update the invariant on-the-fly too...
321 d->updateModelInvariantIndex(modelIndexRow, invariant);
322
323 return modelIndexRow;
324}
325
327{
328 // The user is athemeg for the invariant of the item that is at the CURRENT modelIndexRow.
329 Q_ASSERT(invariantToFill->d->rowMapper() == nullptr);
330
331 // Plain new invariant. Fill it and add to the current hash.
332 invariantToFill->d->setModelIndexRowAndRowMapperSerial(modelIndexRow, d->mCurrentShiftSerial);
333 invariantToFill->d->setRowMapper(this);
334
335 Q_ASSERT(!d->mCurrentInvariantHash->contains(modelIndexRow));
336
337 d->mCurrentInvariantHash->insert(modelIndexRow, invariantToFill);
338}
339
341{
342 return d->modelIndexRowToModelInvariantIndexInternal(modelIndexRow, false);
343}
344
346{
347 if (!d->mRowShiftList) {
348 if (d->mCurrentInvariantHash->isEmpty()) {
349 return nullptr; // no invariants emitted, even if rows are changed, no invariant is affected.
350 }
351 }
352
353 // Find the invariants in range.
354 // It's somewhat impossible to split this in chunks.
355
357
358 const int end = startIndexRow + count;
359 for (int idx = startIndexRow; idx < end; idx++) {
360 ModelInvariantIndex *invariant = d->modelIndexRowToModelInvariantIndexInternal(idx, true);
361 if (invariant) {
362 invariantList->append(invariant);
363 }
364 }
365
366 if (invariantList->isEmpty()) {
367 delete invariantList;
368 return nullptr;
369 }
370
371 return invariantList;
372}
373
374void ModelInvariantRowMapper::modelRowsInserted(int modelIndexRowPosition, int count)
375{
376 // Some rows were added to the model at modelIndexRowPosition.
377
378 // FIXME: If rows are added at the end then we don't need any mapping.
379 // The fact is that we don't know which is the model's end...
380 // But maybe we can consider the end being the greatest row
381 // index emitted until now...
382
383 if (!d->mRowShiftList) {
384 if (d->mCurrentInvariantHash->isEmpty()) {
385 return; // no invariants emitted, even if rows are changed, no invariant is affected.
386 }
387 // some invariants might be affected
388 d->mRowShiftList = new QList<RowShift *>();
389 }
390
391 RowShift *shift;
392
393 if (d->mCurrentInvariantHash->isEmpty()) {
394 // No invariants updated (all existing are outdated)
395
396 Q_ASSERT(d->mRowShiftList->count() > 0); // must be true since it's not null
397
398 // Check if we can attach to the last existing shift (very common for consecutive row additions)
399 shift = d->mRowShiftList->at(d->mRowShiftList->count() - 1);
401
402 if (shift->mShift > 0) { // the shift was positive (addition)
403 if ((shift->mMinimumRowIndex + shift->mShift) == modelIndexRowPosition) {
404 // Inserting contiguous blocks of rows, just extend this shift
405 shift->mShift += count;
406 Q_ASSERT(d->mUpdateTimer->isActive());
407 return;
408 }
409 }
410 }
411
412 // FIXME: If we have few items, we can just shift the indexes now.
413
414 shift = new RowShift(modelIndexRowPosition, count, d->mCurrentInvariantHash);
415 d->mRowShiftList->append(shift);
416
417 d->mCurrentShiftSerial++;
418 d->mCurrentInvariantHash = new QHash<int, ModelInvariantIndex *>();
419
420 if (d->mRowShiftList->count() > 7) { // 7 is heuristic
421 // We start losing performance as the stack is growing too much.
422 // Start updating NOW and hope we can get it in few sweeps.
423
424 if (d->mUpdateTimer->isActive()) {
425 d->mUpdateTimer->stop();
426 }
427
428 d->slotPerformLazyUpdate();
429 } else {
430 // Make sure we'll get a lazy update somewhere in the future
431 if (!d->mUpdateTimer->isActive()) {
432 d->mUpdateTimer->start(d->mLazyUpdateIdleInterval);
433 }
434 }
435}
436
438{
439 // Some rows were added from the model at modelIndexRowPosition.
440
441 // FIXME: If rows are removed from the end, we don't need any mapping.
442 // The fact is that we don't know which is the model's end...
443 // But maybe we can consider the end being the greatest row
444 // index emitted until now...
445
446 if (!d->mRowShiftList) {
447 if (d->mCurrentInvariantHash->isEmpty()) {
448 return nullptr; // no invariants emitted, even if rows are changed, no invariant is affected.
449 }
450 // some invariants might be affected
451 }
452
453 // FIXME: If we have few items, we can just shift the indexes now.
454
455 // FIXME: Find a way to "merge" the shifts, if possible
456 // It OFTEN happens that we remove a lot of items at once (as opposed
457 // to item addition which is usually an incremental operation).
458
459 // FIXME: HUGE PROBLEM
460 // When the items aren't contiguous or are just out of order it's
461 // impossible to merge the shifts. Deleting many messages
462 // generates then a very deep delta stack. Since to delete the
463 // next message you need to traverse the whole stack, this method
464 // becomes very slow (maybe not as slow as updating all the indexes
465 // in the general case, but still *slow*).
466 //
467 // So one needs to perform updates while rows are being removed
468 // but that tends to void all your efforts to not update the
469 // whole list of items every time...
470 //
471 // Also deletions don't seem to be asynchronous (or at least
472 // they eat all the CPU power available for KMail) so the timers
473 // don't fire and we're not actually processing the model jobs...
474 //
475 // It turns out that deleting many items is just slower than
476 // reloading the view...
477
478 // Invalidate the invariants affected by the change
479 // In most cases it's a relatively small sweep (and it's done once).
480 // It's somewhat impossible to split this in chunks.
481
483
484 const int end = modelIndexRowPosition + count;
485 for (int idx = modelIndexRowPosition; idx < end; idx++) {
486 // FIXME: One could optimize this by joining the retrieval and destruction functions
487 // that is by making a special indexDead( int modelIndex )..
488 ModelInvariantIndex *dyingInvariant = d->modelIndexRowToModelInvariantIndexInternal(idx, false);
489 if (dyingInvariant) {
490 d->indexDead(dyingInvariant); // will remove from this mapper hashes
491 dyingInvariant->d->setRowMapper(nullptr); // invalidate!
493 } else {
494 // got no dying invariant
495 qCWarning(MESSAGELIST_LOG) << "Could not find invariant to invalidate at current row " << idx;
496 }
497 }
498
499 if (!d->mRowShiftList) {
500 // have no pending shifts, look if we are keeping other invariants
501 if (d->mCurrentInvariantHash->isEmpty()) {
502 // no more invariants in this mapper, even if rows are changed, no invariant is affected.
503 if (deadInvariants->isEmpty()) {
504 // should never happen, but well...
505 delete deadInvariants;
506 return nullptr;
507 }
508 return deadInvariants;
509 }
510 // still have some invariants inside, must add a shift for them
511 d->mRowShiftList = new QList<RowShift *>();
512 } // else already have shifts
513
514 // add a shift for this row removal
515 auto shift = new RowShift(modelIndexRowPosition + count, -count, d->mCurrentInvariantHash);
516 d->mRowShiftList->append(shift);
517
518 d->mCurrentShiftSerial++;
519 d->mCurrentInvariantHash = new QHash<int, ModelInvariantIndex *>();
520
521 // trigger updates
522 if (d->mRowShiftList->count() > 7) { // 7 is heuristic
523 // We start losing performance as the stack is growing too much.
524 // Start updating NOW and hope we can get it in few sweeps.
525
526 if (d->mUpdateTimer->isActive()) {
527 d->mUpdateTimer->stop();
528 }
529
530 d->slotPerformLazyUpdate();
531 } else {
532 // Make sure we'll get a lazy update somewhere in the future
533 if (!d->mUpdateTimer->isActive()) {
534 d->mUpdateTimer->start(d->mLazyUpdateIdleInterval);
535 }
536 }
537
538 if (deadInvariants->isEmpty()) {
539 // should never happen, but well...
540 delete deadInvariants;
541 return nullptr;
542 }
543
544 return deadInvariants;
545}
546
548{
549 // FIXME: optimize this (it probably can be optimized by providing a more complex user interface)
550
551 for (const auto idx : std::as_const(*d->mCurrentInvariantHash)) {
552 idx->d->setRowMapper(nullptr);
553 }
554 d->mCurrentInvariantHash->clear();
555
556 if (d->mRowShiftList) {
557 while (!d->mRowShiftList->isEmpty()) {
558 delete d->mRowShiftList->takeFirst();
559 }
560
561 delete d->mRowShiftList;
562 d->mRowShiftList = nullptr;
563 }
564
565 d->mCurrentShiftSerial = 0;
566 d->mRemovedShiftCount = 0;
567}
568
569void ModelInvariantRowMapperPrivate::slotPerformLazyUpdate()
570{
571 // The drawback here is that when one row is removed from the middle (say position 500 of 1000)
572 // then we require ALL the items to be updated...but:
573 //
574 // - We can do it very lazily in the background
575 // - Optimizing this would mean to ALSO keep the indexes in lists or in a large array
576 // - The list approach would require to keep the indexes sorted
577 // so it would cost at least N log (N) / 2.. which is worse than N.
578 // - We could keep a single (or multiple) array as large as the model
579 // but then we'd have a large memory consumption and large overhead
580 // when inserting / removing items from the middle.
581 //
582 // So finally I think that the multiple hash approach is a "minimum loss" approach.
583
584 QTime startTime = QTime::currentTime();
585
586 int curIndex = 0;
587
588 while (mRowShiftList) {
589 // Have at least one row shift
590 uint count = static_cast<uint>(mRowShiftList->count());
591
592 // Grab it
593 RowShift *shift = mRowShiftList->at(0);
594
595 // and update the invariants that belong to it
596 auto it = shift->mInvariantHash->begin();
597 auto end = shift->mInvariantHash->end();
598
599 while (it != end) {
600 ModelInvariantIndex *invariant = *it;
601
602 it = shift->mInvariantHash->erase(it);
603
604 // apply shifts
605 int modelIndexRow = invariant->d->modelIndexRow();
606
607 for (uint idx = 0; idx < count; ++idx) {
608 RowShift *thatShift = mRowShiftList->at(idx);
609 if (modelIndexRow >= thatShift->mMinimumRowIndex) {
610 modelIndexRow += thatShift->mShift;
611 }
612 }
613
614 // update and make it belong to the current serial
615 invariant->d->setModelIndexRowAndRowMapperSerial(modelIndexRow, mCurrentShiftSerial);
616
617 mCurrentInvariantHash->insert(modelIndexRow, invariant);
618
619 // once in a while check if we ran out of time
620 if ((curIndex % 15) == 0) { // 15 is heuristic
621 int elapsed = startTime.msecsTo(QTime::currentTime());
622 if ((elapsed > mLazyUpdateChunkInterval) || (elapsed < 0)) {
623 // interrupt
624 // qCDebug(MESSAGELIST_LOG) << "Lazy update fixed " << curIndex << " invariants ";
625 mUpdateTimer->start(mLazyUpdateIdleInterval);
626 return;
627 }
628 }
629
630 curIndex++;
631 }
632
633 // no more invariants with serial <= invariantToFill->rowMapperSerial()
634 killFirstRowShift();
635 }
636
637 // qCDebug(MESSAGELIST_LOG) << "Lazy update fixed " << curIndex << " invariants ";
638
639 // if we're here then no more work needs to be done.
640}
641
642#include "moc_modelinvariantrowmapper.cpp"
An invariant index that can be ALWAYS used to reference an item inside a QAbstractItemModel.
QList< ModelInvariantIndex * > * modelIndexRowRangeToModelInvariantIndexList(int startIndexRow, int count)
This basically applies modelIndexRowToModelInvariantIndex() to a range of elements.
void createModelInvariantIndex(int modelIndexRow, ModelInvariantIndex *invariantToFill)
Binds a ModelInvariantIndex structure to the specified CURRENT modelIndexRow.
void modelRowsInserted(int modelIndexRowPosition, int count)
Call this function when rows are inserted to the underlying model BEFORE scanning the model for the n...
void modelReset()
Call this function from your handlers of reset() and layoutChanged() AFTER you ve last accessed the m...
QList< ModelInvariantIndex * > * modelRowsRemoved(int modelIndexRowPosition, int count)
Call this function when rows are removed from the underlying model AFTER accessing the removed rows f...
void setLazyUpdateChunkInterval(int chunkInterval)
Sets the maximum time we can spend inside a single lazy update step.
ModelInvariantIndex * modelIndexRowToModelInvariantIndex(int modelIndexRow)
Finds the existing ModelInvariantIndex that belongs to the specified CURRENT modelIndexRow.
void setLazyUpdateIdleInterval(int idleInterval)
Sets the idle time between two lazy updates in milliseconds.
int modelInvariantIndexToModelIndexRow(ModelInvariantIndex *invariant)
Maps a ModelInvariantIndex to the CURRENT associated row index in the model.
The implementation independent part of the MessageList library.
Definition aggregation.h:22
T qobject_cast(QObject *object)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QTime currentTime()
int msecsTo(QTime t) const const
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.