Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qconcatenatetablesproxymodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <private/qabstractitemmodel_p.h>
8
9#include "qsize.h"
10#include "qmap.h"
11#include <QtCore/qvarlengtharray.h>
12#include "qdebug.h"
13
14#include <array>
15#include <type_traits>
16
18
20{
21 Q_DECLARE_PUBLIC(QConcatenateTablesProxyModel);
22
23public:
25
26 int computeRowsPrior(const QAbstractItemModel *sourceModel) const;
27
35
36 void slotRowsAboutToBeInserted(const QModelIndex &, int start, int end);
37 void slotRowsInserted(const QModelIndex &, int start, int end);
38 void slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end);
39 void slotRowsRemoved(const QModelIndex &, int start, int end);
40 void slotRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
41 const QModelIndex &destinationParent, int destinationRow);
42 void slotRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
43 const QModelIndex &destinationParent, int destinationRow);
44 void slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end);
45 void slotColumnsInserted(const QModelIndex &parent, int, int);
46 void slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
47 void slotColumnsRemoved(const QModelIndex &parent, int, int);
48 void slotColumnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
49 const QModelIndex &destinationParent, int destination);
50 void slotColumnsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
51 const QModelIndex &destinationParent, int destination);
52 void slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QList<int> &roles);
53 void slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
54 void slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
57 int columnCountAfterChange(const QAbstractItemModel *model, int newCount) const;
60 bool mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
61 int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const;
62
63 struct ModelInfo {
65 template <typename...Args>
67 sizeof...(Args) == std::tuple_size_v<ConnArray> &&
69 // only non-references (= rvalues), only non-const
71 >,
72 bool>;
74 ModelInfo(ModelInfo &&) = default;
75 ModelInfo &operator=(ModelInfo &&) = default;
76 template <typename...Args, if_compatible<Args...> = true>
77 explicit ModelInfo(QAbstractItemModel *m, Args&&...args)
78 : model(m), connections{std::forward<Args>(args)...} {}
81 };
84
85 const ModelInfo *findSourceModel(const QAbstractItemModel *m) const
86 {
87 auto byModelPtr = [m](const auto &modInfo) { return modInfo.model == m; };
88 return std::find_if(m_models.cbegin(), m_models.cend(), byModelPtr);
89 }
90
91 ModelInfo *findSourceModel(const QAbstractItemModel *m)
92 {
93 return const_cast<ModelInfo *>(std::as_const(*this).findSourceModel(m));
94 }
95
96 bool containsSourceModel(const QAbstractItemModel *m) const
97 { return findSourceModel(m) != m_models.cend(); }
98
99 int m_rowCount; // have to maintain it here since we can't compute during model destruction
101
102 // for columns{AboutToBe,}{Inserted,Removed}
104
105 mutable uint m_roleNamesDirty : 1;
106 uint m_reserved : 31;
107
108 // for layoutAboutToBeChanged/layoutChanged
111};
112
121
122/*!
123 \since 5.13
124 \class QConcatenateTablesProxyModel
125 \inmodule QtCore
126 \brief The QConcatenateTablesProxyModel class proxies multiple source models, concatenating their rows.
127
128 \ingroup model-view
129
130 QConcatenateTablesProxyModel takes multiple source models and concatenates their rows.
131
132 In other words, the proxy will have all rows of the first source model,
133 followed by all rows of the second source model, and so on.
134
135 If the source models don't have the same number of columns, the proxy will only
136 have as many columns as the source model with the smallest number of columns.
137 Additional columns in other source models will simply be ignored.
138
139 Source models can be added and removed at runtime, and the column count is adjusted accordingly.
140
141 This proxy does not inherit from QAbstractProxyModel because it uses multiple source
142 models, rather than a single one.
143
144 Only flat models (lists and tables) are supported, tree models are not.
145
146 \sa QAbstractProxyModel, {Model/View Programming}, QIdentityProxyModel, QAbstractItemModel
147 */
148
149
150/*!
151 Constructs a concatenate-rows proxy model with the given \a parent.
152*/
153QConcatenateTablesProxyModel::QConcatenateTablesProxyModel(QObject *parent)
154 : QAbstractItemModel(*new QConcatenateTablesProxyModelPrivate, parent)
155{
156}
157
158/*!
159 Destroys this proxy model.
160*/
161QConcatenateTablesProxyModel::~QConcatenateTablesProxyModel()
162{
163}
164
165/*!
166 Returns the proxy index for a given \a sourceIndex, which can be from any of the source models.
167*/
168QModelIndex QConcatenateTablesProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
169{
170 Q_D(const QConcatenateTablesProxyModel);
171 if (!sourceIndex.isValid())
172 return QModelIndex();
173 const QAbstractItemModel *sourceModel = sourceIndex.model();
174 if (!d->containsSourceModel(sourceModel)) {
175 qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
176 Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
177 return QModelIndex();
178 }
179 if (sourceIndex.column() >= d->m_columnCount)
180 return QModelIndex();
181 int rowsPrior = d_func()->computeRowsPrior(sourceModel);
182 return createIndex(rowsPrior + sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer());
183}
184
185/*!
186 Returns the source index for a given \a proxyIndex.
187*/
188QModelIndex QConcatenateTablesProxyModel::mapToSource(const QModelIndex &proxyIndex) const
189{
190 Q_D(const QConcatenateTablesProxyModel);
191 Q_ASSERT(checkIndex(proxyIndex));
192 if (!proxyIndex.isValid())
193 return QModelIndex();
194 if (proxyIndex.model() != this) {
195 qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
196 Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
197 return QModelIndex();
198 }
199 const int row = proxyIndex.row();
200 const auto result = d->sourceModelForRow(row);
201 if (!result.sourceModel)
202 return QModelIndex();
203 return result.sourceModel->index(result.sourceRow, proxyIndex.column());
204}
205
206/*!
207 \reimp
208*/
209QVariant QConcatenateTablesProxyModel::data(const QModelIndex &index, int role) const
210{
211 const QModelIndex sourceIndex = mapToSource(index);
212 Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
213 if (!sourceIndex.isValid())
214 return QVariant();
215 return sourceIndex.data(role);
216}
217
218/*!
219 \reimp
220*/
221bool QConcatenateTablesProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
222{
223 Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
224 const QModelIndex sourceIndex = mapToSource(index);
225 Q_ASSERT(sourceIndex.isValid());
226 const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
227 return sourceModel->setData(sourceIndex, value, role);
228}
229
230/*!
231 \reimp
232*/
233QMap<int, QVariant> QConcatenateTablesProxyModel::itemData(const QModelIndex &proxyIndex) const
234{
235 Q_ASSERT(checkIndex(proxyIndex));
236 const QModelIndex sourceIndex = mapToSource(proxyIndex);
237 Q_ASSERT(sourceIndex.isValid());
238 return sourceIndex.model()->itemData(sourceIndex);
239}
240
241/*!
242 \reimp
243*/
244bool QConcatenateTablesProxyModel::setItemData(const QModelIndex &proxyIndex, const QMap<int, QVariant> &roles)
245{
246 Q_ASSERT(checkIndex(proxyIndex));
247 const QModelIndex sourceIndex = mapToSource(proxyIndex);
248 Q_ASSERT(sourceIndex.isValid());
249 const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
250 return sourceModel->setItemData(sourceIndex, roles);
251}
252
253/*!
254 Returns the flags for the given index.
255 If the \a index is valid, the flags come from the source model for this \a index.
256 If the \a index is invalid (as used to determine if dropping onto an empty area
257 in the view is allowed, for instance), the flags from the first model are returned.
258*/
259Qt::ItemFlags QConcatenateTablesProxyModel::flags(const QModelIndex &index) const
260{
261 Q_D(const QConcatenateTablesProxyModel);
262 if (d->m_models.isEmpty())
263 return Qt::NoItemFlags;
264 Q_ASSERT(checkIndex(index));
265 if (!index.isValid())
266 return d->m_models.at(0).model->flags(index);
267 const QModelIndex sourceIndex = mapToSource(index);
268 Q_ASSERT(sourceIndex.isValid());
269 return sourceIndex.model()->flags(sourceIndex);
270}
271
272/*!
273 This method returns the horizontal header data for the first source model,
274 and the vertical header data for the source model corresponding to each row.
275 \reimp
276*/
277QVariant QConcatenateTablesProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
278{
279 Q_D(const QConcatenateTablesProxyModel);
280 if (d->m_models.isEmpty())
281 return QVariant();
282 switch (orientation) {
283 case Qt::Horizontal:
284 return d->m_models.at(0).model->headerData(section, orientation, role);
285 case Qt::Vertical: {
286 const auto result = d->sourceModelForRow(section);
287 Q_ASSERT(result.sourceModel);
288 return result.sourceModel->headerData(result.sourceRow, orientation, role);
289 }
290 }
291 return QVariant();
292}
293
294/*!
295 This method returns the column count of the source model with the smallest number of columns.
296 \reimp
297*/
298int QConcatenateTablesProxyModel::columnCount(const QModelIndex &parent) const
299{
300 Q_D(const QConcatenateTablesProxyModel);
301 if (parent.isValid())
302 return 0; // flat model
303 return d->m_columnCount;
304}
305
306/*!
307 \reimp
308*/
309QModelIndex QConcatenateTablesProxyModel::index(int row, int column, const QModelIndex &parent) const
310{
311 Q_D(const QConcatenateTablesProxyModel);
312 Q_ASSERT(hasIndex(row, column, parent));
313 if (!hasIndex(row, column, parent))
314 return QModelIndex();
315 Q_ASSERT(checkIndex(parent, QAbstractItemModel::CheckIndexOption::ParentIsInvalid)); // flat model
316 const auto result = d->sourceModelForRow(row);
317 Q_ASSERT(result.sourceModel);
318 return mapFromSource(result.sourceModel->index(result.sourceRow, column));
319}
320
321/*!
322 \reimp
323*/
324QModelIndex QConcatenateTablesProxyModel::parent(const QModelIndex &index) const
325{
326 Q_UNUSED(index);
327 return QModelIndex(); // flat model, no hierarchy
328}
329
330/*!
331 \reimp
332*/
333int QConcatenateTablesProxyModel::rowCount(const QModelIndex &parent) const
334{
335 Q_D(const QConcatenateTablesProxyModel);
336 if (parent.isValid())
337 return 0; // flat model
338 return d->m_rowCount;
339}
340
341/*!
342 This method returns the mime types for the first source model.
343 \reimp
344*/
345QStringList QConcatenateTablesProxyModel::mimeTypes() const
346{
347 Q_D(const QConcatenateTablesProxyModel);
348 if (d->m_models.isEmpty())
349 return QStringList();
350 return d->m_models.at(0).model->mimeTypes();
351}
352
353/*!
354 The call is forwarded to the source model of the first index in the list of \a indexes.
355
356 Important: please note that this proxy only supports dragging a single row.
357 It will assert if called with indexes from multiple rows, because dragging rows that
358 might come from different source models cannot be implemented generically by this proxy model.
359 Each piece of data in the QMimeData needs to be merged, which is data-type-specific.
360 Reimplement this method in a subclass if you want to support dragging multiple rows.
361
362 \reimp
363*/
364QMimeData *QConcatenateTablesProxyModel::mimeData(const QModelIndexList &indexes) const
365{
366 Q_D(const QConcatenateTablesProxyModel);
367 if (indexes.isEmpty())
368 return nullptr;
369 const QModelIndex firstIndex = indexes.first();
370 Q_ASSERT(checkIndex(firstIndex, CheckIndexOption::IndexIsValid));
371 const auto result = d->sourceModelForRow(firstIndex.row());
372 QModelIndexList sourceIndexes;
373 sourceIndexes.reserve(indexes.size());
374 for (const QModelIndex &index : indexes) {
375 const QModelIndex sourceIndex = mapToSource(index);
376 Q_ASSERT(sourceIndex.model() == result.sourceModel); // see documentation above
377 sourceIndexes.append(sourceIndex);
378 }
379 return result.sourceModel->mimeData(sourceIndexes);
380}
381
382
383bool QConcatenateTablesProxyModelPrivate::mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
384 int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const
385{
386 Q_Q(const QConcatenateTablesProxyModel);
387 *sourceColumn = column;
388 if (!parent.isValid()) {
389 // Drop after the last item
390 if (row == -1 || row == m_rowCount) {
391 *sourceRow = -1;
392 *sourceModel = m_models.back().model;
393 return true;
394 }
395 // Drop between toplevel items
396 const auto result = sourceModelForRow(row);
397 Q_ASSERT(result.sourceModel);
398 *sourceRow = result.sourceRow;
399 *sourceModel = result.sourceModel;
400 return true;
401 } else {
402 if (row > -1)
403 return false; // flat model, no dropping as new children of items
404 // Drop onto item
405 const int targetRow = parent.row();
406 const auto result = sourceModelForRow(targetRow);
407 Q_ASSERT(result.sourceModel);
408 const QModelIndex sourceIndex = q->mapToSource(parent);
409 *sourceRow = -1;
410 *sourceParent = sourceIndex;
411 *sourceModel = result.sourceModel;
412 return true;
413 }
414}
415
416/*!
417 \reimp
418*/
419bool QConcatenateTablesProxyModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
420{
421 Q_D(const QConcatenateTablesProxyModel);
422 if (d->m_models.isEmpty())
423 return false;
424
425 int sourceRow, sourceColumn;
426 QModelIndex sourceParent;
427 QAbstractItemModel *sourceModel;
428 if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
429 return false;
430 return sourceModel->canDropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
431}
432
433/*!
434 QConcatenateTablesProxyModel handles dropping onto an item, between items, and after the last item.
435 In all cases the call is forwarded to the underlying source model.
436 When dropping onto an item, the source model for this item is called.
437 When dropping between items, the source model immediately below the drop position is called.
438 When dropping after the last item, the last source model is called.
439
440 \reimp
441*/
442bool QConcatenateTablesProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
443{
444 Q_D(const QConcatenateTablesProxyModel);
445 if (d->m_models.isEmpty())
446 return false;
447 int sourceRow, sourceColumn;
448 QModelIndex sourceParent;
449 QAbstractItemModel *sourceModel;
450 if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
451 return false;
452
453 return sourceModel->dropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
454}
455
456/*!
457 \reimp
458*/
459QSize QConcatenateTablesProxyModel::span(const QModelIndex &index) const
460{
461 Q_D(const QConcatenateTablesProxyModel);
462 Q_ASSERT(checkIndex(index));
463 if (d->m_models.isEmpty() || !index.isValid())
464 return QSize();
465 const QModelIndex sourceIndex = mapToSource(index);
466 Q_ASSERT(sourceIndex.isValid());
467 return sourceIndex.model()->span(sourceIndex);
468}
469
470/*!
471 Returns a list of models that were added as source models for this proxy model.
472
473 \since 5.15
474*/
475QList<QAbstractItemModel *> QConcatenateTablesProxyModel::sourceModels() const
476{
477 Q_D(const QConcatenateTablesProxyModel);
478 QList<QAbstractItemModel *> ret;
479 ret.reserve(d->m_models.size());
480 for (const auto &info : d->m_models)
481 ret.push_back(info.model);
482 return ret;
483}
484
485/*!
486 Adds a source model \a sourceModel, below all previously added source models.
487
488 The ownership of \a sourceModel is not affected by this.
489
490 The same source model cannot be added more than once.
491 */
492void QConcatenateTablesProxyModel::addSourceModel(QAbstractItemModel *sourceModel)
493{
494 Q_D(QConcatenateTablesProxyModel);
495 Q_ASSERT(sourceModel);
496 Q_ASSERT(!d->containsSourceModel(sourceModel));
497
498 const int newRows = sourceModel->rowCount();
499 if (newRows > 0)
500 beginInsertRows(QModelIndex(), d->m_rowCount, d->m_rowCount + newRows - 1);
501 d->m_rowCount += newRows;
502 d->m_models.emplace_back(sourceModel,
503 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::dataChanged,
504 d, &QConcatenateTablesProxyModelPrivate::slotDataChanged),
505 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsInserted,
506 d, &QConcatenateTablesProxyModelPrivate::slotRowsInserted),
507 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsRemoved,
508 d, &QConcatenateTablesProxyModelPrivate::slotRowsRemoved),
509 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsAboutToBeInserted,
510 d, &QConcatenateTablesProxyModelPrivate::slotRowsAboutToBeInserted),
511 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved,
512 d, &QConcatenateTablesProxyModelPrivate::slotRowsAboutToBeRemoved),
513 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsMoved,
514 d, &QConcatenateTablesProxyModelPrivate::slotRowsMoved),
515 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::rowsAboutToBeMoved,
516 d, &QConcatenateTablesProxyModelPrivate::slotRowsAboutToBeMoved),
517
518 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsInserted,
519 d, &QConcatenateTablesProxyModelPrivate::slotColumnsInserted),
520 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsRemoved,
521 d, &QConcatenateTablesProxyModelPrivate::slotColumnsRemoved),
522 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsAboutToBeInserted,
523 d, &QConcatenateTablesProxyModelPrivate::slotColumnsAboutToBeInserted),
524 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsAboutToBeRemoved,
525 d, &QConcatenateTablesProxyModelPrivate::slotColumnsAboutToBeRemoved),
526 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsMoved,
527 d, &QConcatenateTablesProxyModelPrivate::slotColumnsMoved),
528 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::columnsAboutToBeMoved,
529 d, &QConcatenateTablesProxyModelPrivate::slotColumnsAboutToBeMoved),
530
531 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::layoutAboutToBeChanged,
532 d, &QConcatenateTablesProxyModelPrivate::slotSourceLayoutAboutToBeChanged),
533 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::layoutChanged,
534 d, &QConcatenateTablesProxyModelPrivate::slotSourceLayoutChanged),
535 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset,
536 d, &QConcatenateTablesProxyModelPrivate::slotModelAboutToBeReset),
537 QObjectPrivate::connect(sourceModel, &QAbstractItemModel::modelReset,
538 d, &QConcatenateTablesProxyModelPrivate::slotModelReset)
539 );
540 if (!d->m_roleNamesDirty) {
541 // do update immediately, since append() is a simple update:
542 const auto newRoleNames = sourceModel->roleNames();
543 for (const auto &[k, v] : newRoleNames.asKeyValueRange())
544 d->m_roleNames.insert_or_assign(k, v);
545 }
546 if (newRows > 0)
547 endInsertRows();
548
549 d->updateColumnCount();
550}
551
552/*!
553 Removes the source model \a sourceModel, which was previously added to this proxy.
554
555 The ownership of \a sourceModel is not affected by this.
556*/
557void QConcatenateTablesProxyModel::removeSourceModel(QAbstractItemModel *sourceModel)
558{
559 Q_D(QConcatenateTablesProxyModel);
560
561 auto it = d->findSourceModel(sourceModel);
562 Q_ASSERT(it != d->m_models.end());
563 for (auto &c : it->connections)
564 disconnect(c);
565
566 const int rowsRemoved = sourceModel->rowCount();
567 const int rowsPrior = d->computeRowsPrior(sourceModel); // location of removed section
568
569 if (rowsRemoved > 0)
570 beginRemoveRows(QModelIndex(), rowsPrior, rowsPrior + rowsRemoved - 1);
571 d->m_models.erase(it);
572 d->m_roleNamesDirty = true;
573 d->m_rowCount -= rowsRemoved;
574 if (rowsRemoved > 0)
575 endRemoveRows();
576
577 d->updateColumnCount();
578}
579
580/*!
581 \since 6.9.0
582 \reimp
583 Returns the union of the roleNames() of the underlying models.
584
585 In case source models associate different names to the same role,
586 the name used in last source model overrides the name used in earlier models.
587*/
588QHash<int, QByteArray> QConcatenateTablesProxyModel::roleNames() const
589{
590 Q_D(const QConcatenateTablesProxyModel);
591 if (d->m_roleNamesDirty) {
592 d->m_roleNames = QAbstractItemModelPrivate::defaultRoleNames();
593 for (const auto &[model, _] : d->m_models)
594 d->m_roleNames.insert(model->roleNames());
595 d->m_roleNamesDirty = false;
596 }
597 return d->m_roleNames;
598}
599
601 int start, int end)
602{
603 Q_Q(QConcatenateTablesProxyModel);
604 if (parent.isValid()) // not supported, the proxy is a flat model
605 return;
606 const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
607 const int rowsPrior = computeRowsPrior(model);
608 q->beginInsertRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
609}
610
611void QConcatenateTablesProxyModelPrivate::slotRowsInserted(const QModelIndex &parent, int start,
612 int end)
613{
614 Q_Q(QConcatenateTablesProxyModel);
615 if (parent.isValid()) // flat model
616 return;
617 m_rowCount += end - start + 1;
618 q->endInsertRows();
619}
620
622 int start, int end)
623{
624 Q_Q(QConcatenateTablesProxyModel);
625 if (parent.isValid()) // flat model
626 return;
627 const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
628 const int rowsPrior = computeRowsPrior(model);
629 q->beginRemoveRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
630}
631
632void QConcatenateTablesProxyModelPrivate::slotRowsRemoved(const QModelIndex &parent, int start, int end)
633{
634 Q_Q(QConcatenateTablesProxyModel);
635 if (parent.isValid()) // flat model
636 return;
637 m_rowCount -= end - start + 1;
638 q->endRemoveRows();
639}
640
642 const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
643 const QModelIndex &destinationParent, int destinationRow)
644{
645 Q_Q(QConcatenateTablesProxyModel);
646 if (sourceParent.isValid() || destinationParent.isValid())
647 return;
648 const QAbstractItemModel *const model = static_cast<QAbstractItemModel *>(q->sender());
649 const int rowsPrior = computeRowsPrior(model);
650 q->beginMoveRows(sourceParent, rowsPrior + sourceStart, rowsPrior + sourceEnd,
651 destinationParent, rowsPrior + destinationRow);
652}
653
654void QConcatenateTablesProxyModelPrivate::slotRowsMoved(const QModelIndex &sourceParent,
655 int sourceStart, int sourceEnd,
656 const QModelIndex &destinationParent,
657 int destinationRow)
658{
659 Q_Q(QConcatenateTablesProxyModel);
660 Q_UNUSED(sourceStart)
661 Q_UNUSED(sourceEnd)
662 Q_UNUSED(destinationRow)
663 if (sourceParent.isValid() || destinationParent.isValid())
664 return;
665 q->endMoveRows();
666}
667
669 int start, int end)
670{
671 Q_Q(QConcatenateTablesProxyModel);
672 if (parent.isValid()) // flat model
673 return;
674 const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
675 const int oldColCount = model->columnCount();
676 const int newColCount = columnCountAfterChange(model, oldColCount + end - start + 1);
677 Q_ASSERT(newColCount >= oldColCount);
678 if (newColCount > oldColCount)
679 // If the underlying models have a different number of columns (example: 2 and 3), inserting 2 columns in
680 // the first model leads to inserting only one column in the proxy, since qMin(2+2,3) == 3.
681 q->beginInsertColumns(QModelIndex(), start, qMin(end, start + newColCount - oldColCount - 1));
682 m_newColumnCount = newColCount;
683}
684
685void QConcatenateTablesProxyModelPrivate::slotColumnsInserted(const QModelIndex &parent, int start,
686 int end)
687{
688 Q_UNUSED(start);
689 Q_UNUSED(end);
690 Q_Q(QConcatenateTablesProxyModel);
691 if (parent.isValid()) // flat model
692 return;
695 q->endInsertColumns();
696 }
697}
698
700 int start, int end)
701{
702 Q_Q(QConcatenateTablesProxyModel);
703 if (parent.isValid()) // flat model
704 return;
705 const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
706 const int oldColCount = model->columnCount();
707 const int newColCount = columnCountAfterChange(model, oldColCount - (end - start + 1));
708 Q_ASSERT(newColCount <= oldColCount);
709 if (newColCount < oldColCount)
710 q->beginRemoveColumns(QModelIndex(), start, qMax(end, start + oldColCount - newColCount - 1));
711 m_newColumnCount = newColCount;
712}
713
714void QConcatenateTablesProxyModelPrivate::slotColumnsRemoved(const QModelIndex &parent, int start,
715 int end)
716{
717 Q_Q(QConcatenateTablesProxyModel);
718 Q_UNUSED(start);
719 Q_UNUSED(end);
720 if (parent.isValid()) // flat model
721 return;
724 q->endRemoveColumns();
725 }
726}
727
729 const QModelIndex &sourceParent, int sourceStart, int sourceEnd,
730 const QModelIndex &destinationParent, int destination)
731{
732 Q_UNUSED(sourceStart)
733 Q_UNUSED(sourceEnd)
734 Q_UNUSED(destination)
735 if (sourceParent.isValid() || destinationParent.isValid())
736 return;
737 slotSourceLayoutAboutToBeChanged({}, QAbstractItemModel::HorizontalSortHint);
738}
739
740void QConcatenateTablesProxyModelPrivate::slotColumnsMoved(const QModelIndex &sourceParent,
741 int sourceStart, int sourceEnd,
742 const QModelIndex &destinationParent,
743 int destination)
744{
745 Q_UNUSED(sourceStart)
746 Q_UNUSED(sourceEnd)
747 Q_UNUSED(destination)
748 if (sourceParent.isValid() || destinationParent.isValid())
749 return;
750 slotSourceLayoutChanged({}, QAbstractItemModel::HorizontalSortHint);
751}
752
754 const QModelIndex &to,
755 const QList<int> &roles)
756{
757 Q_Q(QConcatenateTablesProxyModel);
758 Q_ASSERT(from.isValid());
759 Q_ASSERT(to.isValid());
760 if (from.column() >= m_columnCount)
761 return;
762 QModelIndex adjustedTo = to;
763 if (to.column() >= m_columnCount)
764 adjustedTo = to.siblingAtColumn(m_columnCount - 1);
765 const QModelIndex myFrom = q->mapFromSource(from);
766 Q_ASSERT(q->checkIndex(myFrom, QAbstractItemModel::CheckIndexOption::IndexIsValid));
767 const QModelIndex myTo = q->mapFromSource(adjustedTo);
768 Q_ASSERT(q->checkIndex(myTo, QAbstractItemModel::CheckIndexOption::IndexIsValid));
769 emit q->dataChanged(myFrom, myTo, roles);
770}
771
773 const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
774{
775 Q_Q(QConcatenateTablesProxyModel);
776
777 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
778 return;
779
780 emit q->layoutAboutToBeChanged({}, hint);
781
782 const QModelIndexList persistentIndexList = q->persistentIndexList();
783 layoutChangePersistentIndexes.reserve(persistentIndexList.size());
784 layoutChangeProxyIndexes.reserve(persistentIndexList.size());
785
786 for (const QModelIndex &proxyPersistentIndex : persistentIndexList) {
787 layoutChangeProxyIndexes.append(proxyPersistentIndex);
788 Q_ASSERT(proxyPersistentIndex.isValid());
789 const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
790 Q_ASSERT(srcPersistentIndex.isValid());
791 layoutChangePersistentIndexes << srcPersistentIndex;
792 }
793}
794
796 const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
797{
798 Q_Q(QConcatenateTablesProxyModel);
799 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
800 return;
801 for (int i = 0; i < layoutChangeProxyIndexes.size(); ++i) {
802 const QModelIndex proxyIdx = layoutChangeProxyIndexes.at(i);
803 const QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
804 q->changePersistentIndex(proxyIdx, newProxyIdx);
805 }
806
807 layoutChangePersistentIndexes.clear();
808 layoutChangeProxyIndexes.clear();
809
810 emit q->layoutChanged({}, hint);
811}
812
814{
815 Q_Q(QConcatenateTablesProxyModel);
816 Q_ASSERT(containsSourceModel(static_cast<QAbstractItemModel *>(q->sender())));
817 q->beginResetModel();
818 // A reset might reduce both rowCount and columnCount, and we can't notify of both at the same time,
819 // and notifying of one after the other leaves an intermediary invalid situation.
820 // So the only safe choice is to forward it as a full reset.
821}
822
824{
825 Q_Q(QConcatenateTablesProxyModel);
826 Q_ASSERT(containsSourceModel(static_cast<QAbstractItemModel *>(q->sender())));
828 m_rowCount = computeRowsPrior(nullptr);
829 q->endResetModel();
830}
831
833{
834 if (m_models.isEmpty())
835 return 0;
836
837 auto byColumnCount = [](const auto &a, const auto &b) {
838 return a.model->columnCount() < b.model->columnCount();
839 };
840 const auto it = std::min_element(m_models.begin(), m_models.end(), byColumnCount);
841 return it->model->columnCount();
842}
843
845{
846 Q_Q(QConcatenateTablesProxyModel);
847 const int newColumnCount = calculatedColumnCount();
848 const int columnDiff = newColumnCount - m_columnCount;
849 if (columnDiff > 0) {
850 q->beginInsertColumns(QModelIndex(), m_columnCount, m_columnCount + columnDiff - 1);
851 m_columnCount = newColumnCount;
852 q->endInsertColumns();
853 } else if (columnDiff < 0) {
854 const int lastColumn = m_columnCount - 1;
855 q->beginRemoveColumns(QModelIndex(), lastColumn + columnDiff + 1, lastColumn);
856 m_columnCount = newColumnCount;
857 q->endRemoveColumns();
858 }
859}
860
861int QConcatenateTablesProxyModelPrivate::columnCountAfterChange(const QAbstractItemModel *model, int newCount) const
862{
863 int newColumnCount = 0;
864 for (qsizetype i = 0; i < m_models.size(); ++i) {
865 const QAbstractItemModel *mod = m_models.at(i).model;
866 const int colCount = mod == model ? newCount : mod->columnCount();
867 if (i == 0)
868 newColumnCount = colCount;
869 else
870 newColumnCount = qMin(colCount, newColumnCount);
871 }
872 return newColumnCount;
873}
874
875int QConcatenateTablesProxyModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const
876{
877 int rowsPrior = 0;
878 for (const auto &[model, _] : m_models) {
879 if (model == sourceModel)
880 break;
881 rowsPrior += model->rowCount();
882 }
883 return rowsPrior;
884}
885
887{
889 int rowCount = 0;
890 for (const auto &[model, _] : m_models) {
891 const int subRowCount = model->rowCount();
892 if (rowCount + subRowCount > row) {
893 result.sourceModel = model;
894 break;
895 }
896 rowCount += subRowCount;
897 }
898 result.sourceRow = row - rowCount;
899 return result;
900}
901
902QT_END_NAMESPACE
903
904#include "moc_qconcatenatetablesproxymodel.cpp"
void slotSourceLayoutAboutToBeChanged(const QList< QPersistentModelIndex > &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
bool containsSourceModel(const QAbstractItemModel *m) const
void slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end)
ModelInfo * findSourceModel(const QAbstractItemModel *m)
void slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QList< int > &roles)
SourceModelForRowResult sourceModelForRow(int row) const
void slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
void slotColumnsRemoved(const QModelIndex &parent, int, int)
int columnCountAfterChange(const QAbstractItemModel *model, int newCount) const
void slotRowsAboutToBeInserted(const QModelIndex &, int start, int end)
void slotColumnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destination)
bool mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent, int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const
void slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
const ModelInfo * findSourceModel(const QAbstractItemModel *m) const
void slotRowsInserted(const QModelIndex &, int start, int end)
void slotColumnsInserted(const QModelIndex &parent, int, int)
void slotRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void slotColumnsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destination)
void slotSourceLayoutChanged(const QList< QPersistentModelIndex > &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
void slotRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
int computeRowsPrior(const QAbstractItemModel *sourceModel) const
QList< QPersistentModelIndex > layoutChangePersistentIndexes
void slotRowsRemoved(const QModelIndex &, int start, int end)
Combined button and popup list for selecting options.
ModelInfo & operator=(ModelInfo &&)=default