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
45QT_BEGIN_NAMESPACE
46
47class QConcatenateTablesProxyModelPrivate : public QAbstractItemModelPrivate
48{
49 Q_DECLARE_PUBLIC(QConcatenateTablesProxyModel);
50
51public:
52 QConcatenateTablesProxyModelPrivate();
53
54 int computeRowsPrior(const QAbstractItemModel *sourceModel) const;
55
56 struct SourceModelForRowResult
57 {
58 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 QList<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 QList<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 QList<QPersistentModelIndex> layoutChangePersistentIndexes;
92 QList<QModelIndex> layoutChangeProxyIndexes;
93};
94
95QConcatenateTablesProxyModelPrivate::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*/
133QConcatenateTablesProxyModel::QConcatenateTablesProxyModel(QObject *parent)
134 : QAbstractItemModel(*new QConcatenateTablesProxyModelPrivate, parent)
135{
136}
137
138/*!
139 Destroys this proxy model.
140*/
141QConcatenateTablesProxyModel::~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*/
148QModelIndex 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*/
168QModelIndex 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*/
189QVariant 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*/
201bool 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*/
213QMap<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*/
224bool 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*/
239Qt::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*/
257QVariant 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*/
278int 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*/
289QModelIndex 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*/
304QModelIndex QConcatenateTablesProxyModel::parent(const QModelIndex &index) const
305{
306 Q_UNUSED(index);
307 return QModelIndex(); // flat model, no hierarchy
308}
309
310/*!
311 \reimp
312*/
313int QConcatenateTablesProxyModel::rowCount(const QModelIndex &parent) const
314{
315 Q_D(const QConcatenateTablesProxyModel);
316 Q_ASSERT(checkIndex(parent, QAbstractItemModel::CheckIndexOption::ParentIsInvalid)); // flat model
317 Q_UNUSED(parent);
318 return d->m_rowCount;
319}
320
321/*!
322 This method returns the mime types for the first source model.
323 \reimp
324*/
325QStringList 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*/
344QMimeData *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
363bool 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*/
399bool 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*/
422bool 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*/
439QSize 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*/
455QList<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 */
468void 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,QList<int>)), this, SLOT(_q_slotDataChanged(QModelIndex,QModelIndex,QList<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*/
507void 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
526void 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
536void 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
545void 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
555void 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
564void 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
580void 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
593void 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
607void 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
620void QConcatenateTablesProxyModelPrivate::_q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QList<int> &roles)
621{
622 Q_Q(QConcatenateTablesProxyModel);
623 Q_ASSERT(from.isValid());
624 Q_ASSERT(to.isValid());
625 const QModelIndex myFrom = q->mapFromSource(from);
626 Q_ASSERT(q->checkIndex(myFrom, QAbstractItemModel::CheckIndexOption::IndexIsValid));
627 const QModelIndex myTo = q->mapFromSource(to);
628 Q_ASSERT(q->checkIndex(myTo, QAbstractItemModel::CheckIndexOption::IndexIsValid));
629 emit q->dataChanged(myFrom, myTo, roles);
630}
631
632void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
633{
634 Q_Q(QConcatenateTablesProxyModel);
635
636 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
637 return;
638
639 emit q->layoutAboutToBeChanged({}, hint);
640
641 const QModelIndexList persistentIndexList = q->persistentIndexList();
642 layoutChangePersistentIndexes.reserve(persistentIndexList.size());
643 layoutChangeProxyIndexes.reserve(persistentIndexList.size());
644
645 for (const QModelIndex &proxyPersistentIndex : persistentIndexList) {
646 layoutChangeProxyIndexes.append(proxyPersistentIndex);
647 Q_ASSERT(proxyPersistentIndex.isValid());
648 const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
649 Q_ASSERT(srcPersistentIndex.isValid());
650 layoutChangePersistentIndexes << srcPersistentIndex;
651 }
652}
653
654void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
655{
656 Q_Q(QConcatenateTablesProxyModel);
657 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
658 return;
659 for (int i = 0; i < layoutChangeProxyIndexes.size(); ++i) {
660 const QModelIndex proxyIdx = layoutChangeProxyIndexes.at(i);
661 const QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
662 q->changePersistentIndex(proxyIdx, newProxyIdx);
663 }
664
665 layoutChangePersistentIndexes.clear();
666 layoutChangeProxyIndexes.clear();
667
668 emit q->layoutChanged({}, hint);
669}
670
671void QConcatenateTablesProxyModelPrivate::_q_slotModelAboutToBeReset()
672{
673 Q_Q(QConcatenateTablesProxyModel);
674 Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
675 q->beginResetModel();
676 // A reset might reduce both rowCount and columnCount, and we can't notify of both at the same time,
677 // and notifying of one after the other leaves an intermediary invalid situation.
678 // So the only safe choice is to forward it as a full reset.
679}
680
681void QConcatenateTablesProxyModelPrivate::_q_slotModelReset()
682{
683 Q_Q(QConcatenateTablesProxyModel);
684 Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
685 m_columnCount = calculatedColumnCount();
686 m_rowCount = computeRowsPrior(nullptr);
687 q->endResetModel();
688}
689
690int QConcatenateTablesProxyModelPrivate::calculatedColumnCount() const
691{
692 if (m_models.isEmpty())
693 return 0;
694
695 const auto it = std::min_element(m_models.begin(), m_models.end(), [](const QAbstractItemModel* model1, const QAbstractItemModel* model2) {
696 return model1->columnCount() < model2->columnCount();
697 });
698 return (*it)->columnCount();
699}
700
701void QConcatenateTablesProxyModelPrivate::updateColumnCount()
702{
703 Q_Q(QConcatenateTablesProxyModel);
704 const int newColumnCount = calculatedColumnCount();
705 const int columnDiff = newColumnCount - m_columnCount;
706 if (columnDiff > 0) {
707 q->beginInsertColumns(QModelIndex(), m_columnCount, m_columnCount + columnDiff - 1);
708 m_columnCount = newColumnCount;
709 q->endInsertColumns();
710 } else if (columnDiff < 0) {
711 const int lastColumn = m_columnCount - 1;
712 q->beginRemoveColumns(QModelIndex(), lastColumn + columnDiff + 1, lastColumn);
713 m_columnCount = newColumnCount;
714 q->endRemoveColumns();
715 }
716}
717
718int QConcatenateTablesProxyModelPrivate::columnCountAfterChange(const QAbstractItemModel *model, int newCount) const
719{
720 int newColumnCount = 0;
721 for (int i = 0; i < m_models.count(); ++i) {
722 const QAbstractItemModel *mod = m_models.at(i);
723 const int colCount = mod == model ? newCount : mod->columnCount();
724 if (i == 0)
725 newColumnCount = colCount;
726 else
727 newColumnCount = qMin(colCount, newColumnCount);
728 }
729 return newColumnCount;
730}
731
732int QConcatenateTablesProxyModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const
733{
734 int rowsPrior = 0;
735 for (const QAbstractItemModel *model : m_models) {
736 if (model == sourceModel)
737 break;
738 rowsPrior += model->rowCount();
739 }
740 return rowsPrior;
741}
742
743QConcatenateTablesProxyModelPrivate::SourceModelForRowResult QConcatenateTablesProxyModelPrivate::sourceModelForRow(int row) const
744{
745 QConcatenateTablesProxyModelPrivate::SourceModelForRowResult result;
746 int rowCount = 0;
747 for (QAbstractItemModel *model : m_models) {
748 const int subRowCount = model->rowCount();
749 if (rowCount + subRowCount > row) {
750 result.sourceModel = model;
751 break;
752 }
753 rowCount += subRowCount;
754 }
755 result.sourceRow = row - rowCount;
756 return result;
757}
758
759QT_END_NAMESPACE
760
761#include "moc_qconcatenatetablesproxymodel.cpp"
762