1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtCore module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qconcatenatetablesproxymodel.h"
41 #include <private/qabstractitemmodel_p.h>
42 #include "qsize.h"
43 #include "qdebug.h"
44 
45 QT_BEGIN_NAMESPACE
46 
47 class QConcatenateTablesProxyModelPrivate : public QAbstractItemModelPrivate
48 {
49     Q_DECLARE_PUBLIC(QConcatenateTablesProxyModel);
50 
51 public:
52     QConcatenateTablesProxyModelPrivate();
53 
54     int computeRowsPrior(const QAbstractItemModel *sourceModel) const;
55 
56     struct SourceModelForRowResult
57     {
SourceModelForRowResultQConcatenateTablesProxyModelPrivate::SourceModelForRowResult58         SourceModelForRowResult() : sourceModel(nullptr), sourceRow(-1) {}
59         QAbstractItemModel *sourceModel;
60         int sourceRow;
61     };
62     SourceModelForRowResult sourceModelForRow(int row) const;
63 
64     void _q_slotRowsAboutToBeInserted(const QModelIndex &, int start, int end);
65     void _q_slotRowsInserted(const QModelIndex &, int start, int end);
66     void _q_slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end);
67     void _q_slotRowsRemoved(const QModelIndex &, int start, int end);
68     void _q_slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end);
69     void _q_slotColumnsInserted(const QModelIndex &parent, int, int);
70     void _q_slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
71     void _q_slotColumnsRemoved(const QModelIndex &parent, int, int);
72     void _q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles);
73     void _q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
74     void _q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
75     void _q_slotModelAboutToBeReset();
76     void _q_slotModelReset();
77     int columnCountAfterChange(const QAbstractItemModel *model, int newCount) const;
78     int calculatedColumnCount() const;
79     void updateColumnCount();
80     bool mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
81                                     int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const;
82 
83     QVector<QAbstractItemModel *> m_models;
84     int m_rowCount; // have to maintain it here since we can't compute during model destruction
85     int m_columnCount;
86 
87     // for columns{AboutToBe,}{Inserted,Removed}
88     int m_newColumnCount;
89 
90     // for layoutAboutToBeChanged/layoutChanged
91     QVector<QPersistentModelIndex> layoutChangePersistentIndexes;
92     QVector<QModelIndex> layoutChangeProxyIndexes;
93 };
94 
QConcatenateTablesProxyModelPrivate()95 QConcatenateTablesProxyModelPrivate::QConcatenateTablesProxyModelPrivate()
96     : m_rowCount(0),
97       m_columnCount(0),
98       m_newColumnCount(0)
99 {
100 }
101 
102 /*!
103     \since 5.13
104     \class QConcatenateTablesProxyModel
105     \inmodule QtCore
106     \brief The QConcatenateTablesProxyModel class proxies multiple source models, concatenating their rows.
107 
108     \ingroup model-view
109 
110     QConcatenateTablesProxyModel takes multiple source models and concatenates their rows.
111 
112     In other words, the proxy will have all rows of the first source model,
113     followed by all rows of the second source model, and so on.
114 
115     If the source models don't have the same number of columns, the proxy will only
116     have as many columns as the source model with the smallest number of columns.
117     Additional columns in other source models will simply be ignored.
118 
119     Source models can be added and removed at runtime, and the column count is adjusted accordingly.
120 
121     This proxy does not inherit from QAbstractProxyModel because it uses multiple source
122     models, rather than a single one.
123 
124     Only flat models (lists and tables) are supported, tree models are not.
125 
126     \sa QAbstractProxyModel, {Model/View Programming}, QIdentityProxyModel, QAbstractItemModel
127  */
128 
129 
130 /*!
131     Constructs a concatenate-rows proxy model with the given \a parent.
132 */
QConcatenateTablesProxyModel(QObject * parent)133 QConcatenateTablesProxyModel::QConcatenateTablesProxyModel(QObject *parent)
134     : QAbstractItemModel(*new QConcatenateTablesProxyModelPrivate, parent)
135 {
136 }
137 
138 /*!
139     Destroys this proxy model.
140 */
~QConcatenateTablesProxyModel()141 QConcatenateTablesProxyModel::~QConcatenateTablesProxyModel()
142 {
143 }
144 
145 /*!
146     Returns the proxy index for a given \a sourceIndex, which can be from any of the source models.
147 */
mapFromSource(const QModelIndex & sourceIndex) const148 QModelIndex QConcatenateTablesProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
149 {
150     Q_D(const QConcatenateTablesProxyModel);
151     if (!sourceIndex.isValid())
152         return QModelIndex();
153     const QAbstractItemModel *sourceModel = sourceIndex.model();
154     if (!d->m_models.contains(const_cast<QAbstractItemModel *>(sourceModel))) {
155         qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
156         Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
157         return QModelIndex();
158     }
159     if (sourceIndex.column() >= d->m_columnCount)
160         return QModelIndex();
161     int rowsPrior = d_func()->computeRowsPrior(sourceModel);
162     return createIndex(rowsPrior + sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer());
163 }
164 
165 /*!
166     Returns the source index for a given \a proxyIndex.
167 */
mapToSource(const QModelIndex & proxyIndex) const168 QModelIndex QConcatenateTablesProxyModel::mapToSource(const QModelIndex &proxyIndex) const
169 {
170     Q_D(const QConcatenateTablesProxyModel);
171     Q_ASSERT(checkIndex(proxyIndex));
172     if (!proxyIndex.isValid())
173         return QModelIndex();
174     if (proxyIndex.model() != this) {
175         qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
176         Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
177         return QModelIndex();
178     }
179     const int row = proxyIndex.row();
180     const auto result = d->sourceModelForRow(row);
181     if (!result.sourceModel)
182         return QModelIndex();
183     return result.sourceModel->index(result.sourceRow, proxyIndex.column());
184 }
185 
186 /*!
187   \reimp
188 */
data(const QModelIndex & index,int role) const189 QVariant QConcatenateTablesProxyModel::data(const QModelIndex &index, int role) const
190 {
191     const QModelIndex sourceIndex = mapToSource(index);
192     Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
193     if (!sourceIndex.isValid())
194         return QVariant();
195     return sourceIndex.data(role);
196 }
197 
198 /*!
199   \reimp
200 */
setData(const QModelIndex & index,const QVariant & value,int role)201 bool QConcatenateTablesProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
202 {
203     Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
204     const QModelIndex sourceIndex = mapToSource(index);
205     Q_ASSERT(sourceIndex.isValid());
206     const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
207     return sourceModel->setData(sourceIndex, value, role);
208 }
209 
210 /*!
211   \reimp
212 */
itemData(const QModelIndex & proxyIndex) const213 QMap<int, QVariant> QConcatenateTablesProxyModel::itemData(const QModelIndex &proxyIndex) const
214 {
215     Q_ASSERT(checkIndex(proxyIndex));
216     const QModelIndex sourceIndex = mapToSource(proxyIndex);
217     Q_ASSERT(sourceIndex.isValid());
218     return sourceIndex.model()->itemData(sourceIndex);
219 }
220 
221 /*!
222   \reimp
223 */
setItemData(const QModelIndex & proxyIndex,const QMap<int,QVariant> & roles)224 bool QConcatenateTablesProxyModel::setItemData(const QModelIndex &proxyIndex, const QMap<int, QVariant> &roles)
225 {
226     Q_ASSERT(checkIndex(proxyIndex));
227     const QModelIndex sourceIndex = mapToSource(proxyIndex);
228     Q_ASSERT(sourceIndex.isValid());
229     const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
230     return sourceModel->setItemData(sourceIndex, roles);
231 }
232 
233 /*!
234   Returns the flags for the given index.
235   If the \a index is valid, the flags come from the source model for this \a index.
236   If the \a index is invalid (as used to determine if dropping onto an empty area
237   in the view is allowed, for instance), the flags from the first model are returned.
238 */
flags(const QModelIndex & index) const239 Qt::ItemFlags QConcatenateTablesProxyModel::flags(const QModelIndex &index) const
240 {
241     Q_D(const QConcatenateTablesProxyModel);
242     if (d->m_models.isEmpty())
243         return Qt::NoItemFlags;
244     Q_ASSERT(checkIndex(index));
245     if (!index.isValid())
246         return d->m_models.at(0)->flags(index);
247     const QModelIndex sourceIndex = mapToSource(index);
248     Q_ASSERT(sourceIndex.isValid());
249     return sourceIndex.model()->flags(sourceIndex);
250 }
251 
252 /*!
253     This method returns the horizontal header data for the first source model,
254     and the vertical header data for the source model corresponding to each row.
255     \reimp
256 */
headerData(int section,Qt::Orientation orientation,int role) const257 QVariant QConcatenateTablesProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
258 {
259     Q_D(const QConcatenateTablesProxyModel);
260     if (d->m_models.isEmpty())
261         return QVariant();
262     switch (orientation) {
263         case Qt::Horizontal:
264             return d->m_models.at(0)->headerData(section, orientation, role);
265         case Qt::Vertical: {
266             const auto result = d->sourceModelForRow(section);
267             Q_ASSERT(result.sourceModel);
268             return result.sourceModel->headerData(result.sourceRow, orientation, role);
269         }
270     }
271     return QVariant();
272 }
273 
274 /*!
275     This method returns the column count of the source model with the smallest number of columns.
276     \reimp
277 */
columnCount(const QModelIndex & parent) const278 int QConcatenateTablesProxyModel::columnCount(const QModelIndex &parent) const
279 {
280     Q_D(const QConcatenateTablesProxyModel);
281     if (parent.isValid())
282         return 0; // flat model
283     return d->m_columnCount;
284 }
285 
286 /*!
287   \reimp
288 */
index(int row,int column,const QModelIndex & parent) const289 QModelIndex QConcatenateTablesProxyModel::index(int row, int column, const QModelIndex &parent) const
290 {
291     Q_D(const QConcatenateTablesProxyModel);
292     Q_ASSERT(hasIndex(row, column, parent));
293     if (!hasIndex(row, column, parent))
294         return QModelIndex();
295     Q_ASSERT(checkIndex(parent, QAbstractItemModel::CheckIndexOption::ParentIsInvalid)); // flat model
296     const auto result = d->sourceModelForRow(row);
297     Q_ASSERT(result.sourceModel);
298     return mapFromSource(result.sourceModel->index(result.sourceRow, column));
299 }
300 
301 /*!
302   \reimp
303 */
parent(const QModelIndex & index) const304 QModelIndex QConcatenateTablesProxyModel::parent(const QModelIndex &index) const
305 {
306     Q_UNUSED(index);
307     return QModelIndex(); // flat model, no hierarchy
308 }
309 
310 /*!
311   \reimp
312 */
rowCount(const QModelIndex & parent) const313 int QConcatenateTablesProxyModel::rowCount(const QModelIndex &parent) const
314 {
315     Q_D(const QConcatenateTablesProxyModel);
316     if (parent.isValid())
317         return 0; // flat model
318     return d->m_rowCount;
319 }
320 
321 /*!
322     This method returns the mime types for the first source model.
323     \reimp
324 */
mimeTypes() const325 QStringList QConcatenateTablesProxyModel::mimeTypes() const
326 {
327     Q_D(const QConcatenateTablesProxyModel);
328     if (d->m_models.isEmpty())
329         return QStringList();
330     return d->m_models.at(0)->mimeTypes();
331 }
332 
333 /*!
334   The call is forwarded to the source model of the first index in the list of \a indexes.
335 
336   Important: please note that this proxy only supports dragging a single row.
337   It will assert if called with indexes from multiple rows, because dragging rows that
338   might come from different source models cannot be implemented generically by this proxy model.
339   Each piece of data in the QMimeData needs to be merged, which is data-type-specific.
340   Reimplement this method in a subclass if you want to support dragging multiple rows.
341 
342   \reimp
343 */
mimeData(const QModelIndexList & indexes) const344 QMimeData *QConcatenateTablesProxyModel::mimeData(const QModelIndexList &indexes) const
345 {
346     Q_D(const QConcatenateTablesProxyModel);
347     if (indexes.isEmpty())
348         return nullptr;
349     const QModelIndex firstIndex = indexes.first();
350     Q_ASSERT(checkIndex(firstIndex, CheckIndexOption::IndexIsValid));
351     const auto result = d->sourceModelForRow(firstIndex.row());
352     QModelIndexList sourceIndexes;
353     sourceIndexes.reserve(indexes.count());
354     for (const QModelIndex &index : indexes) {
355         const QModelIndex sourceIndex = mapToSource(index);
356         Q_ASSERT(sourceIndex.model() == result.sourceModel); // see documentation above
357         sourceIndexes.append(sourceIndex);
358     }
359     return result.sourceModel->mimeData(sourceIndexes);
360 }
361 
362 
mapDropCoordinatesToSource(int row,int column,const QModelIndex & parent,int * sourceRow,int * sourceColumn,QModelIndex * sourceParent,QAbstractItemModel ** sourceModel) const363 bool QConcatenateTablesProxyModelPrivate::mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
364                                                                      int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const
365 {
366     Q_Q(const QConcatenateTablesProxyModel);
367     *sourceColumn = column;
368     if (!parent.isValid()) {
369         // Drop after the last item
370         if (row == -1 || row == m_rowCount) {
371             *sourceRow = -1;
372             *sourceModel = m_models.constLast();
373             return true;
374         }
375         // Drop between toplevel items
376         const auto result = sourceModelForRow(row);
377         Q_ASSERT(result.sourceModel);
378         *sourceRow = result.sourceRow;
379         *sourceModel = result.sourceModel;
380         return true;
381     } else {
382         if (row > -1)
383             return false; // flat model, no dropping as new children of items
384         // Drop onto item
385         const int targetRow = parent.row();
386         const auto result = sourceModelForRow(targetRow);
387         Q_ASSERT(result.sourceModel);
388         const QModelIndex sourceIndex = q->mapToSource(parent);
389         *sourceRow = -1;
390         *sourceParent = sourceIndex;
391         *sourceModel = result.sourceModel;
392         return true;
393     }
394 }
395 
396 /*!
397   \reimp
398 */
canDropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent) const399 bool QConcatenateTablesProxyModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
400 {
401     Q_D(const QConcatenateTablesProxyModel);
402     if (d->m_models.isEmpty())
403         return false;
404 
405     int sourceRow, sourceColumn;
406     QModelIndex sourceParent;
407     QAbstractItemModel *sourceModel;
408     if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
409         return false;
410     return sourceModel->canDropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
411 }
412 
413 /*!
414   QConcatenateTablesProxyModel handles dropping onto an item, between items, and after the last item.
415   In all cases the call is forwarded to the underlying source model.
416   When dropping onto an item, the source model for this item is called.
417   When dropping between items, the source model immediately below the drop position is called.
418   When dropping after the last item, the last source model is called.
419 
420   \reimp
421 */
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)422 bool QConcatenateTablesProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
423 {
424     Q_D(const QConcatenateTablesProxyModel);
425     if (d->m_models.isEmpty())
426         return false;
427     int sourceRow, sourceColumn;
428     QModelIndex sourceParent;
429     QAbstractItemModel *sourceModel;
430     if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
431         return false;
432 
433     return sourceModel->dropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
434 }
435 
436 /*!
437     \reimp
438 */
span(const QModelIndex & index) const439 QSize QConcatenateTablesProxyModel::span(const QModelIndex &index) const
440 {
441     Q_D(const QConcatenateTablesProxyModel);
442     Q_ASSERT(checkIndex(index));
443     if (d->m_models.isEmpty() || !index.isValid())
444         return QSize();
445     const QModelIndex sourceIndex = mapToSource(index);
446     Q_ASSERT(sourceIndex.isValid());
447     return sourceIndex.model()->span(sourceIndex);
448 }
449 
450 /*!
451     Returns a list of models that were added as source models for this proxy model.
452 
453     \since 5.15
454 */
sourceModels() const455 QList<QAbstractItemModel *> QConcatenateTablesProxyModel::sourceModels() const
456 {
457     Q_D(const QConcatenateTablesProxyModel);
458     return d->m_models.toList();
459 }
460 
461 /*!
462     Adds a source model \a sourceModel, below all previously added source models.
463 
464     The ownership of \a sourceModel is not affected by this.
465 
466     The same source model cannot be added more than once.
467  */
addSourceModel(QAbstractItemModel * sourceModel)468 void QConcatenateTablesProxyModel::addSourceModel(QAbstractItemModel *sourceModel)
469 {
470     Q_D(QConcatenateTablesProxyModel);
471     Q_ASSERT(sourceModel);
472     Q_ASSERT(!d->m_models.contains(sourceModel));
473     connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), this, SLOT(_q_slotDataChanged(QModelIndex,QModelIndex,QVector<int>)));
474     connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(_q_slotRowsInserted(QModelIndex,int,int)));
475     connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(_q_slotRowsRemoved(QModelIndex,int,int)));
476     connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(_q_slotRowsAboutToBeInserted(QModelIndex,int,int)));
477     connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(_q_slotRowsAboutToBeRemoved(QModelIndex,int,int)));
478 
479     connect(sourceModel, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(_q_slotColumnsInserted(QModelIndex,int,int)));
480     connect(sourceModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(_q_slotColumnsRemoved(QModelIndex,int,int)));
481     connect(sourceModel, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(_q_slotColumnsAboutToBeInserted(QModelIndex,int,int)));
482     connect(sourceModel, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(_q_slotColumnsAboutToBeRemoved(QModelIndex,int,int)));
483 
484     connect(sourceModel, SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
485             this, SLOT(_q_slotSourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
486     connect(sourceModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
487             this, SLOT(_q_slotSourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
488     connect(sourceModel, SIGNAL(modelAboutToBeReset()), this, SLOT(_q_slotModelAboutToBeReset()));
489     connect(sourceModel, SIGNAL(modelReset()), this, SLOT(_q_slotModelReset()));
490 
491     const int newRows = sourceModel->rowCount();
492     if (newRows > 0)
493         beginInsertRows(QModelIndex(), d->m_rowCount, d->m_rowCount + newRows - 1);
494     d->m_rowCount += newRows;
495     d->m_models.append(sourceModel);
496     if (newRows > 0)
497         endInsertRows();
498 
499     d->updateColumnCount();
500 }
501 
502 /*!
503     Removes the source model \a sourceModel, which was previously added to this proxy.
504 
505     The ownership of \a sourceModel is not affected by this.
506 */
removeSourceModel(QAbstractItemModel * sourceModel)507 void QConcatenateTablesProxyModel::removeSourceModel(QAbstractItemModel *sourceModel)
508 {
509     Q_D(QConcatenateTablesProxyModel);
510     Q_ASSERT(d->m_models.contains(sourceModel));
511     disconnect(sourceModel, nullptr, this, nullptr);
512 
513     const int rowsRemoved = sourceModel->rowCount();
514     const int rowsPrior = d->computeRowsPrior(sourceModel);   // location of removed section
515 
516     if (rowsRemoved > 0)
517         beginRemoveRows(QModelIndex(), rowsPrior, rowsPrior + rowsRemoved - 1);
518     d->m_models.removeOne(sourceModel);
519     d->m_rowCount -= rowsRemoved;
520     if (rowsRemoved > 0)
521         endRemoveRows();
522 
523     d->updateColumnCount();
524 }
525 
_q_slotRowsAboutToBeInserted(const QModelIndex & parent,int start,int end)526 void QConcatenateTablesProxyModelPrivate::_q_slotRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
527 {
528     Q_Q(QConcatenateTablesProxyModel);
529     if (parent.isValid()) // not supported, the proxy is a flat model
530         return;
531     const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
532     const int rowsPrior = computeRowsPrior(model);
533     q->beginInsertRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
534 }
535 
_q_slotRowsInserted(const QModelIndex & parent,int start,int end)536 void QConcatenateTablesProxyModelPrivate::_q_slotRowsInserted(const QModelIndex &parent, int start, int end)
537 {
538     Q_Q(QConcatenateTablesProxyModel);
539     if (parent.isValid()) // flat model
540         return;
541     m_rowCount += end - start + 1;
542     q->endInsertRows();
543 }
544 
_q_slotRowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)545 void QConcatenateTablesProxyModelPrivate::_q_slotRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
546 {
547     Q_Q(QConcatenateTablesProxyModel);
548     if (parent.isValid()) // flat model
549         return;
550     const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
551     const int rowsPrior = computeRowsPrior(model);
552     q->beginRemoveRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
553 }
554 
_q_slotRowsRemoved(const QModelIndex & parent,int start,int end)555 void QConcatenateTablesProxyModelPrivate::_q_slotRowsRemoved(const QModelIndex &parent, int start, int end)
556 {
557     Q_Q(QConcatenateTablesProxyModel);
558     if (parent.isValid()) // flat model
559         return;
560     m_rowCount -= end - start + 1;
561     q->endRemoveRows();
562 }
563 
_q_slotColumnsAboutToBeInserted(const QModelIndex & parent,int start,int end)564 void QConcatenateTablesProxyModelPrivate::_q_slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
565 {
566     Q_Q(QConcatenateTablesProxyModel);
567     if (parent.isValid()) // flat model
568         return;
569     const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
570     const int oldColCount = model->columnCount();
571     const int newColCount = columnCountAfterChange(model, oldColCount + end - start + 1);
572     Q_ASSERT(newColCount >= oldColCount);
573     if (newColCount > oldColCount)
574         // If the underlying models have a different number of columns (example: 2 and 3), inserting 2 columns in
575         // the first model leads to inserting only one column in the proxy, since qMin(2+2,3) == 3.
576         q->beginInsertColumns(QModelIndex(), start, qMin(end, start + newColCount - oldColCount - 1));
577     m_newColumnCount = newColCount;
578 }
579 
_q_slotColumnsInserted(const QModelIndex & parent,int start,int end)580 void QConcatenateTablesProxyModelPrivate::_q_slotColumnsInserted(const QModelIndex &parent, int start, int end)
581 {
582     Q_UNUSED(start);
583     Q_UNUSED(end);
584     Q_Q(QConcatenateTablesProxyModel);
585     if (parent.isValid()) // flat model
586         return;
587     if (m_newColumnCount != m_columnCount) {
588         m_columnCount = m_newColumnCount;
589         q->endInsertColumns();
590     }
591 }
592 
_q_slotColumnsAboutToBeRemoved(const QModelIndex & parent,int start,int end)593 void QConcatenateTablesProxyModelPrivate::_q_slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
594 {
595     Q_Q(QConcatenateTablesProxyModel);
596     if (parent.isValid()) // flat model
597         return;
598     const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
599     const int oldColCount = model->columnCount();
600     const int newColCount = columnCountAfterChange(model, oldColCount - (end - start + 1));
601     Q_ASSERT(newColCount <= oldColCount);
602     if (newColCount < oldColCount)
603         q->beginRemoveColumns(QModelIndex(), start, qMax(end, start + oldColCount - newColCount - 1));
604     m_newColumnCount = newColCount;
605 }
606 
_q_slotColumnsRemoved(const QModelIndex & parent,int start,int end)607 void QConcatenateTablesProxyModelPrivate::_q_slotColumnsRemoved(const QModelIndex &parent, int start, int end)
608 {
609     Q_Q(QConcatenateTablesProxyModel);
610     Q_UNUSED(start);
611     Q_UNUSED(end);
612     if (parent.isValid()) // flat model
613         return;
614     if (m_newColumnCount != m_columnCount) {
615         m_columnCount = m_newColumnCount;
616         q->endRemoveColumns();
617     }
618 }
619 
_q_slotDataChanged(const QModelIndex & from,const QModelIndex & to,const QVector<int> & roles)620 void QConcatenateTablesProxyModelPrivate::_q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles)
621 {
622     Q_Q(QConcatenateTablesProxyModel);
623     Q_ASSERT(from.isValid());
624     Q_ASSERT(to.isValid());
625     if (from.column() >= m_columnCount)
626         return;
627     QModelIndex adjustedTo = to;
628     if (to.column() >= m_columnCount)
629         adjustedTo = to.siblingAtColumn(m_columnCount - 1);
630     const QModelIndex myFrom = q->mapFromSource(from);
631     Q_ASSERT(q->checkIndex(myFrom, QAbstractItemModel::CheckIndexOption::IndexIsValid));
632     const QModelIndex myTo = q->mapFromSource(adjustedTo);
633     Q_ASSERT(q->checkIndex(myTo, QAbstractItemModel::CheckIndexOption::IndexIsValid));
634     emit q->dataChanged(myFrom, myTo, roles);
635 }
636 
_q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> & sourceParents,QAbstractItemModel::LayoutChangeHint hint)637 void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
638 {
639     Q_Q(QConcatenateTablesProxyModel);
640 
641     if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
642         return;
643 
644     emit q->layoutAboutToBeChanged({}, hint);
645 
646     const QModelIndexList persistentIndexList = q->persistentIndexList();
647     layoutChangePersistentIndexes.reserve(persistentIndexList.size());
648     layoutChangeProxyIndexes.reserve(persistentIndexList.size());
649 
650     for (const QModelIndex &proxyPersistentIndex : persistentIndexList) {
651         layoutChangeProxyIndexes.append(proxyPersistentIndex);
652         Q_ASSERT(proxyPersistentIndex.isValid());
653         const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
654         Q_ASSERT(srcPersistentIndex.isValid());
655         layoutChangePersistentIndexes << srcPersistentIndex;
656     }
657 }
658 
_q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> & sourceParents,QAbstractItemModel::LayoutChangeHint hint)659 void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
660 {
661     Q_Q(QConcatenateTablesProxyModel);
662     if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
663         return;
664     for (int i = 0; i < layoutChangeProxyIndexes.size(); ++i) {
665         const QModelIndex proxyIdx = layoutChangeProxyIndexes.at(i);
666         const QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
667         q->changePersistentIndex(proxyIdx, newProxyIdx);
668     }
669 
670     layoutChangePersistentIndexes.clear();
671     layoutChangeProxyIndexes.clear();
672 
673     emit q->layoutChanged({}, hint);
674 }
675 
_q_slotModelAboutToBeReset()676 void QConcatenateTablesProxyModelPrivate::_q_slotModelAboutToBeReset()
677 {
678     Q_Q(QConcatenateTablesProxyModel);
679     Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
680     q->beginResetModel();
681     // A reset might reduce both rowCount and columnCount, and we can't notify of both at the same time,
682     // and notifying of one after the other leaves an intermediary invalid situation.
683     // So the only safe choice is to forward it as a full reset.
684 }
685 
_q_slotModelReset()686 void QConcatenateTablesProxyModelPrivate::_q_slotModelReset()
687 {
688     Q_Q(QConcatenateTablesProxyModel);
689     Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
690     m_columnCount = calculatedColumnCount();
691     m_rowCount = computeRowsPrior(nullptr);
692     q->endResetModel();
693 }
694 
calculatedColumnCount() const695 int QConcatenateTablesProxyModelPrivate::calculatedColumnCount() const
696 {
697     if (m_models.isEmpty())
698         return 0;
699 
700     const auto it = std::min_element(m_models.begin(), m_models.end(), [](const QAbstractItemModel* model1, const QAbstractItemModel* model2) {
701                         return model1->columnCount() < model2->columnCount();
702                     });
703     return (*it)->columnCount();
704 }
705 
updateColumnCount()706 void QConcatenateTablesProxyModelPrivate::updateColumnCount()
707 {
708     Q_Q(QConcatenateTablesProxyModel);
709     const int newColumnCount = calculatedColumnCount();
710     const int columnDiff = newColumnCount - m_columnCount;
711     if (columnDiff > 0) {
712         q->beginInsertColumns(QModelIndex(), m_columnCount, m_columnCount + columnDiff - 1);
713         m_columnCount = newColumnCount;
714         q->endInsertColumns();
715     } else if (columnDiff < 0) {
716         const int lastColumn = m_columnCount - 1;
717         q->beginRemoveColumns(QModelIndex(), lastColumn + columnDiff + 1, lastColumn);
718         m_columnCount = newColumnCount;
719         q->endRemoveColumns();
720     }
721 }
722 
columnCountAfterChange(const QAbstractItemModel * model,int newCount) const723 int QConcatenateTablesProxyModelPrivate::columnCountAfterChange(const QAbstractItemModel *model, int newCount) const
724 {
725     int newColumnCount = 0;
726     for (int i = 0; i < m_models.count(); ++i) {
727         const QAbstractItemModel *mod = m_models.at(i);
728         const int colCount = mod == model ? newCount : mod->columnCount();
729         if (i == 0)
730             newColumnCount = colCount;
731         else
732             newColumnCount = qMin(colCount, newColumnCount);
733     }
734     return newColumnCount;
735 }
736 
computeRowsPrior(const QAbstractItemModel * sourceModel) const737 int QConcatenateTablesProxyModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const
738 {
739     int rowsPrior = 0;
740     for (const QAbstractItemModel *model : m_models) {
741         if (model == sourceModel)
742             break;
743         rowsPrior += model->rowCount();
744     }
745     return rowsPrior;
746 }
747 
sourceModelForRow(int row) const748 QConcatenateTablesProxyModelPrivate::SourceModelForRowResult QConcatenateTablesProxyModelPrivate::sourceModelForRow(int row) const
749 {
750     QConcatenateTablesProxyModelPrivate::SourceModelForRowResult result;
751     int rowCount = 0;
752     for (QAbstractItemModel *model : m_models) {
753         const int subRowCount = model->rowCount();
754         if (rowCount + subRowCount > row) {
755             result.sourceModel = model;
756             break;
757         }
758         rowCount += subRowCount;
759     }
760     result.sourceRow = row - rowCount;
761     return result;
762 }
763 
764 QT_END_NAMESPACE
765 
766 #include "moc_qconcatenatetablesproxymodel.cpp"
767