| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include <math.h> |
| 5 | #include <QtCore/qstack.h> |
| 6 | #include <QtCore/qdebug.h> |
| 7 | |
| 8 | #include "qqmltreemodeltotablemodel_p_p.h" |
| 9 | |
| 10 | QT_BEGIN_NAMESPACE |
| 11 | |
| 12 | //#define QQMLTREEMODELADAPTOR_DEBUG |
| 13 | #if defined(QQMLTREEMODELADAPTOR_DEBUG) && !defined(QT_TESTLIB_LIB) |
| 14 | # define ASSERT_CONSISTENCY() Q_ASSERT_X(testConsistency(true /* dumpOnFail */), Q_FUNC_INFO, "Consistency test failed") |
| 15 | #else |
| 16 | # define ASSERT_CONSISTENCY qt_noop |
| 17 | #endif |
| 18 | |
| 19 | QQmlTreeModelToTableModel::QQmlTreeModelToTableModel(QObject *parent) |
| 20 | : QAbstractItemModel(parent) |
| 21 | { |
| 22 | } |
| 23 | |
| 24 | QAbstractItemModel *QQmlTreeModelToTableModel::model() const |
| 25 | { |
| 26 | return m_model; |
| 27 | } |
| 28 | |
| 29 | void QQmlTreeModelToTableModel::connectToModel() |
| 30 | { |
| 31 | m_connections = { |
| 32 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::destroyed, |
| 33 | context: this, slot: &QQmlTreeModelToTableModel::modelHasBeenDestroyed), |
| 34 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::modelReset, |
| 35 | context: this, slot: &QQmlTreeModelToTableModel::modelHasBeenReset), |
| 36 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::dataChanged, |
| 37 | context: this, slot: &QQmlTreeModelToTableModel::modelDataChanged), |
| 38 | |
| 39 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::layoutAboutToBeChanged, |
| 40 | context: this, slot: &QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged), |
| 41 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::layoutChanged, |
| 42 | context: this, slot: &QQmlTreeModelToTableModel::modelLayoutChanged), |
| 43 | |
| 44 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeInserted, |
| 45 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeInserted), |
| 46 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsInserted, |
| 47 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsInserted), |
| 48 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeRemoved, |
| 49 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved), |
| 50 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsRemoved, |
| 51 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsRemoved), |
| 52 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeMoved, |
| 53 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeMoved), |
| 54 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsMoved, |
| 55 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsMoved), |
| 56 | |
| 57 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsAboutToBeInserted, |
| 58 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted), |
| 59 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsAboutToBeRemoved, |
| 60 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved), |
| 61 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsInserted, |
| 62 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsInserted), |
| 63 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsRemoved, |
| 64 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsRemoved) |
| 65 | }; |
| 66 | } |
| 67 | |
| 68 | void QQmlTreeModelToTableModel::setModel(QAbstractItemModel *arg) |
| 69 | { |
| 70 | if (m_model != arg) { |
| 71 | if (m_model) { |
| 72 | for (const auto &c : m_connections) |
| 73 | QObject::disconnect(c); |
| 74 | m_connections.fill(u: {}); |
| 75 | } |
| 76 | |
| 77 | clearModelData(); |
| 78 | m_model = arg; |
| 79 | |
| 80 | if (m_rootIndex.isValid() && m_rootIndex.model() != m_model) |
| 81 | m_rootIndex = QModelIndex(); |
| 82 | |
| 83 | if (m_model) { |
| 84 | connectToModel(); |
| 85 | showModelTopLevelItems(); |
| 86 | } |
| 87 | |
| 88 | emit modelChanged(model: arg); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | void QQmlTreeModelToTableModel::clearModelData() |
| 93 | { |
| 94 | beginResetModel(); |
| 95 | m_items.clear(); |
| 96 | m_expandedItems.clear(); |
| 97 | endResetModel(); |
| 98 | } |
| 99 | |
| 100 | QModelIndex QQmlTreeModelToTableModel::rootIndex() const |
| 101 | { |
| 102 | return m_rootIndex; |
| 103 | } |
| 104 | |
| 105 | void QQmlTreeModelToTableModel::setRootIndex(const QModelIndex &idx) |
| 106 | { |
| 107 | if (m_rootIndex == idx) |
| 108 | return; |
| 109 | |
| 110 | if (m_model) |
| 111 | clearModelData(); |
| 112 | m_rootIndex = idx; |
| 113 | if (m_model) |
| 114 | showModelTopLevelItems(); |
| 115 | emit rootIndexChanged(); |
| 116 | } |
| 117 | |
| 118 | void QQmlTreeModelToTableModel::resetRootIndex() |
| 119 | { |
| 120 | setRootIndex(QModelIndex()); |
| 121 | } |
| 122 | |
| 123 | QModelIndex QQmlTreeModelToTableModel::index(int row, int column, const QModelIndex &parent) const |
| 124 | { |
| 125 | return hasIndex(row, column, parent) ? createIndex(arow: row, acolumn: column) : QModelIndex(); |
| 126 | } |
| 127 | |
| 128 | QModelIndex QQmlTreeModelToTableModel::parent(const QModelIndex &child) const |
| 129 | { |
| 130 | Q_UNUSED(child) |
| 131 | return QModelIndex(); |
| 132 | } |
| 133 | |
| 134 | QHash<int, QByteArray> QQmlTreeModelToTableModel::roleNames() const |
| 135 | { |
| 136 | if (!m_model) |
| 137 | return QHash<int, QByteArray>(); |
| 138 | return m_model->roleNames(); |
| 139 | } |
| 140 | |
| 141 | int QQmlTreeModelToTableModel::rowCount(const QModelIndex &) const |
| 142 | { |
| 143 | if (!m_model) |
| 144 | return 0; |
| 145 | return m_items.size(); |
| 146 | } |
| 147 | |
| 148 | int QQmlTreeModelToTableModel::columnCount(const QModelIndex &parent) const |
| 149 | { |
| 150 | if (!m_model) |
| 151 | return 0; |
| 152 | return m_model->columnCount(parent); |
| 153 | } |
| 154 | |
| 155 | QVariant QQmlTreeModelToTableModel::data(const QModelIndex &index, int role) const |
| 156 | { |
| 157 | if (!m_model) |
| 158 | return QVariant(); |
| 159 | |
| 160 | return m_model->data(index: mapToModel(index), role); |
| 161 | } |
| 162 | |
| 163 | bool QQmlTreeModelToTableModel::setData(const QModelIndex &index, const QVariant &value, int role) |
| 164 | { |
| 165 | if (!m_model) |
| 166 | return false; |
| 167 | |
| 168 | return m_model->setData(index: mapToModel(index), value, role); |
| 169 | } |
| 170 | |
| 171 | QVariant QQmlTreeModelToTableModel::(int section, Qt::Orientation orientation, int role) const |
| 172 | { |
| 173 | return m_model->headerData(section, orientation, role); |
| 174 | } |
| 175 | |
| 176 | Qt::ItemFlags QQmlTreeModelToTableModel::flags(const QModelIndex &index) const |
| 177 | { |
| 178 | return m_model->flags(index: mapToModel(index)); |
| 179 | } |
| 180 | |
| 181 | int QQmlTreeModelToTableModel::depthAtRow(int row) const |
| 182 | { |
| 183 | if (row < 0 || row >= m_items.size()) |
| 184 | return 0; |
| 185 | return m_items.at(i: row).depth; |
| 186 | } |
| 187 | |
| 188 | int QQmlTreeModelToTableModel::itemIndex(const QModelIndex &index) const |
| 189 | { |
| 190 | // This is basically a plagiarism of QTreeViewPrivate::viewIndex() |
| 191 | if (!index.isValid() || index == m_rootIndex || m_items.isEmpty()) |
| 192 | return -1; |
| 193 | |
| 194 | const int totalCount = m_items.size(); |
| 195 | |
| 196 | // We start nearest to the lastViewedItem |
| 197 | int localCount = qMin(a: m_lastItemIndex - 1, b: totalCount - m_lastItemIndex); |
| 198 | |
| 199 | for (int i = 0; i < localCount; ++i) { |
| 200 | const TreeItem &item1 = m_items.at(i: m_lastItemIndex + i); |
| 201 | if (item1.index == index) { |
| 202 | m_lastItemIndex = m_lastItemIndex + i; |
| 203 | return m_lastItemIndex; |
| 204 | } |
| 205 | const TreeItem &item2 = m_items.at(i: m_lastItemIndex - i - 1); |
| 206 | if (item2.index == index) { |
| 207 | m_lastItemIndex = m_lastItemIndex - i - 1; |
| 208 | return m_lastItemIndex; |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | for (int j = qMax(a: 0, b: m_lastItemIndex + localCount); j < totalCount; ++j) { |
| 213 | const TreeItem &item = m_items.at(i: j); |
| 214 | if (item.index == index) { |
| 215 | m_lastItemIndex = j; |
| 216 | return j; |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | for (int j = qMin(a: totalCount, b: m_lastItemIndex - localCount) - 1; j >= 0; --j) { |
| 221 | const TreeItem &item = m_items.at(i: j); |
| 222 | if (item.index == index) { |
| 223 | m_lastItemIndex = j; |
| 224 | return j; |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | // nothing found |
| 229 | return -1; |
| 230 | } |
| 231 | |
| 232 | bool QQmlTreeModelToTableModel::isVisible(const QModelIndex &index) |
| 233 | { |
| 234 | return itemIndex(index) != -1; |
| 235 | } |
| 236 | |
| 237 | bool QQmlTreeModelToTableModel::childrenVisible(const QModelIndex &index) |
| 238 | { |
| 239 | return (index == m_rootIndex && !m_items.isEmpty()) |
| 240 | || (m_expandedItems.contains(value: index) && isVisible(index)); |
| 241 | } |
| 242 | |
| 243 | QModelIndex QQmlTreeModelToTableModel::mapToModel(const QModelIndex &index) const |
| 244 | { |
| 245 | if (!index.isValid()) |
| 246 | return QModelIndex(); |
| 247 | |
| 248 | const int row = index.row(); |
| 249 | if (row < 0 || row > m_items.size() - 1) |
| 250 | return QModelIndex(); |
| 251 | |
| 252 | const QModelIndex sourceIndex = m_items.at(i: row).index; |
| 253 | return m_model->index(row: sourceIndex.row(), column: index.column(), parent: sourceIndex.parent()); |
| 254 | } |
| 255 | |
| 256 | QModelIndex QQmlTreeModelToTableModel::mapFromModel(const QModelIndex &index) const |
| 257 | { |
| 258 | if (!index.isValid()) |
| 259 | return QModelIndex(); |
| 260 | |
| 261 | int row = -1; |
| 262 | for (int i = 0; i < m_items.size(); ++i) { |
| 263 | const QModelIndex proxyIndex = m_items[i].index; |
| 264 | if (proxyIndex.row() == index.row() && proxyIndex.parent() == index.parent()) { |
| 265 | row = i; |
| 266 | break; |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | if (row == -1) |
| 271 | return QModelIndex(); |
| 272 | |
| 273 | return this->index(row, column: index.column()); |
| 274 | } |
| 275 | |
| 276 | QModelIndex QQmlTreeModelToTableModel::mapToModel(int row) const |
| 277 | { |
| 278 | if (row < 0 || row >= m_items.size()) |
| 279 | return QModelIndex(); |
| 280 | return m_items.at(i: row).index; |
| 281 | } |
| 282 | |
| 283 | QItemSelection QQmlTreeModelToTableModel::selectionForRowRange(const QModelIndex &fromIndex, const QModelIndex &toIndex) const |
| 284 | { |
| 285 | int from = itemIndex(index: fromIndex); |
| 286 | int to = itemIndex(index: toIndex); |
| 287 | if (from == -1) { |
| 288 | if (to == -1) |
| 289 | return QItemSelection(); |
| 290 | return QItemSelection(toIndex, toIndex); |
| 291 | } |
| 292 | |
| 293 | to = qMax(a: to, b: 0); |
| 294 | if (from > to) |
| 295 | qSwap(value1&: from, value2&: to); |
| 296 | |
| 297 | typedef QPair<QModelIndex, QModelIndex> MIPair; |
| 298 | typedef QHash<QModelIndex, MIPair> MI2MIPairHash; |
| 299 | MI2MIPairHash ranges; |
| 300 | QModelIndex firstIndex = m_items.at(i: from).index; |
| 301 | QModelIndex lastIndex = firstIndex; |
| 302 | QModelIndex previousParent = firstIndex.parent(); |
| 303 | bool selectLastRow = false; |
| 304 | for (int i = from + 1; i <= to || (selectLastRow = true); i++) { |
| 305 | // We run an extra iteration to make sure the last row is |
| 306 | // added to the selection. (And also to avoid duplicating |
| 307 | // the insertion code.) |
| 308 | QModelIndex index; |
| 309 | QModelIndex parent; |
| 310 | if (!selectLastRow) { |
| 311 | index = m_items.at(i).index; |
| 312 | parent = index.parent(); |
| 313 | } |
| 314 | if (selectLastRow || previousParent != parent) { |
| 315 | const MI2MIPairHash::iterator &it = ranges.find(key: previousParent); |
| 316 | if (it == ranges.end()) |
| 317 | ranges.insert(key: previousParent, value: MIPair(firstIndex, lastIndex)); |
| 318 | else |
| 319 | it->second = lastIndex; |
| 320 | |
| 321 | if (selectLastRow) |
| 322 | break; |
| 323 | |
| 324 | firstIndex = index; |
| 325 | previousParent = parent; |
| 326 | } |
| 327 | lastIndex = index; |
| 328 | } |
| 329 | |
| 330 | QItemSelection sel; |
| 331 | sel.reserve(asize: ranges.size()); |
| 332 | for (const MIPair &pair : std::as_const(t&: ranges)) |
| 333 | sel.append(t: QItemSelectionRange(pair.first, pair.second)); |
| 334 | |
| 335 | return sel; |
| 336 | } |
| 337 | |
| 338 | void QQmlTreeModelToTableModel::showModelTopLevelItems(bool doInsertRows) |
| 339 | { |
| 340 | if (!m_model) |
| 341 | return; |
| 342 | |
| 343 | if (m_model->hasChildren(parent: m_rootIndex) && m_model->canFetchMore(parent: m_rootIndex)) |
| 344 | m_model->fetchMore(parent: m_rootIndex); |
| 345 | const long topLevelRowCount = m_model->rowCount(parent: m_rootIndex); |
| 346 | if (topLevelRowCount == 0) |
| 347 | return; |
| 348 | |
| 349 | showModelChildItems(parent: TreeItem(m_rootIndex), start: 0, end: topLevelRowCount - 1, doInsertRows); |
| 350 | } |
| 351 | |
| 352 | void QQmlTreeModelToTableModel::showModelChildItems(const TreeItem &parentItem, int start, int end, bool doInsertRows, bool doExpandPendingRows) |
| 353 | { |
| 354 | const QModelIndex &parentIndex = parentItem.index; |
| 355 | int rowIdx = parentIndex.isValid() && parentIndex != m_rootIndex ? itemIndex(index: parentIndex) + 1 : 0; |
| 356 | Q_ASSERT(rowIdx == 0 || parentItem.expanded); |
| 357 | if (parentIndex.isValid() && parentIndex != m_rootIndex && (rowIdx == 0 || !parentItem.expanded)) |
| 358 | return; |
| 359 | |
| 360 | if (m_model->rowCount(parent: parentIndex) == 0) { |
| 361 | if (m_model->hasChildren(parent: parentIndex) && m_model->canFetchMore(parent: parentIndex)) |
| 362 | m_model->fetchMore(parent: parentIndex); |
| 363 | return; |
| 364 | } |
| 365 | |
| 366 | int insertCount = end - start + 1; |
| 367 | int startIdx; |
| 368 | if (start == 0) { |
| 369 | startIdx = rowIdx; |
| 370 | } else { |
| 371 | // Prefer to insert before next sibling instead of after last child of previous, as |
| 372 | // the latter is potentially buggy, see QTBUG-66062 |
| 373 | const QModelIndex &nextSiblingIdx = m_model->index(row: end + 1, column: 0, parent: parentIndex); |
| 374 | if (nextSiblingIdx.isValid()) { |
| 375 | startIdx = itemIndex(index: nextSiblingIdx); |
| 376 | } else { |
| 377 | const QModelIndex &prevSiblingIdx = m_model->index(row: start - 1, column: 0, parent: parentIndex); |
| 378 | startIdx = lastChildIndex(index: prevSiblingIdx) + 1; |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | int rowDepth = rowIdx == 0 ? 0 : parentItem.depth + 1; |
| 383 | if (doInsertRows) |
| 384 | beginInsertRows(parent: QModelIndex(), first: startIdx, last: startIdx + insertCount - 1); |
| 385 | m_items.reserve(asize: m_items.size() + insertCount); |
| 386 | |
| 387 | for (int i = 0; i < insertCount; i++) { |
| 388 | const QModelIndex &cmi = m_model->index(row: start + i, column: 0, parent: parentIndex); |
| 389 | const bool expanded = m_expandedItems.contains(value: cmi); |
| 390 | const TreeItem treeItem(cmi, rowDepth, expanded); |
| 391 | m_items.insert(i: startIdx + i, t: treeItem); |
| 392 | |
| 393 | if (expanded) |
| 394 | m_itemsToExpand.append(t: treeItem); |
| 395 | } |
| 396 | |
| 397 | if (doInsertRows) |
| 398 | endInsertRows(); |
| 399 | |
| 400 | if (doExpandPendingRows) |
| 401 | expandPendingRows(doInsertRows); |
| 402 | } |
| 403 | |
| 404 | |
| 405 | void QQmlTreeModelToTableModel::expand(const QModelIndex &idx) |
| 406 | { |
| 407 | ASSERT_CONSISTENCY(); |
| 408 | if (!m_model) |
| 409 | return; |
| 410 | |
| 411 | Q_ASSERT(!idx.isValid() || idx.model() == m_model); |
| 412 | |
| 413 | if (!idx.isValid() || !m_model->hasChildren(parent: idx)) |
| 414 | return; |
| 415 | if (m_expandedItems.contains(value: idx)) |
| 416 | return; |
| 417 | |
| 418 | int row = itemIndex(index: idx); |
| 419 | if (row != -1) |
| 420 | expandRow(n: row); |
| 421 | else |
| 422 | m_expandedItems.insert(value: idx); |
| 423 | ASSERT_CONSISTENCY(); |
| 424 | |
| 425 | emit expanded(index: idx); |
| 426 | } |
| 427 | |
| 428 | void QQmlTreeModelToTableModel::collapse(const QModelIndex &idx) |
| 429 | { |
| 430 | ASSERT_CONSISTENCY(); |
| 431 | if (!m_model) |
| 432 | return; |
| 433 | |
| 434 | Q_ASSERT(!idx.isValid() || idx.model() == m_model); |
| 435 | |
| 436 | if (!idx.isValid() || !m_model->hasChildren(parent: idx)) |
| 437 | return; |
| 438 | if (!m_expandedItems.contains(value: idx)) |
| 439 | return; |
| 440 | |
| 441 | int row = itemIndex(index: idx); |
| 442 | if (row != -1) |
| 443 | collapseRow(n: row); |
| 444 | else |
| 445 | m_expandedItems.remove(value: idx); |
| 446 | ASSERT_CONSISTENCY(); |
| 447 | |
| 448 | emit collapsed(index: idx); |
| 449 | } |
| 450 | |
| 451 | bool QQmlTreeModelToTableModel::isExpanded(const QModelIndex &index) const |
| 452 | { |
| 453 | ASSERT_CONSISTENCY(); |
| 454 | if (!m_model) |
| 455 | return false; |
| 456 | |
| 457 | Q_ASSERT(!index.isValid() || index.model() == m_model); |
| 458 | return !index.isValid() || m_expandedItems.contains(value: index); |
| 459 | } |
| 460 | |
| 461 | bool QQmlTreeModelToTableModel::isExpanded(int row) const |
| 462 | { |
| 463 | if (row < 0 || row >= m_items.size()) |
| 464 | return false; |
| 465 | return m_items.at(i: row).expanded; |
| 466 | } |
| 467 | |
| 468 | bool QQmlTreeModelToTableModel::hasChildren(int row) const |
| 469 | { |
| 470 | if (row < 0 || row >= m_items.size()) |
| 471 | return false; |
| 472 | return m_model->hasChildren(parent: m_items[row].index); |
| 473 | } |
| 474 | |
| 475 | bool QQmlTreeModelToTableModel::hasSiblings(int row) const |
| 476 | { |
| 477 | const QModelIndex &index = mapToModel(row); |
| 478 | return index.row() != m_model->rowCount(parent: index.parent()) - 1; |
| 479 | } |
| 480 | |
| 481 | void QQmlTreeModelToTableModel::expandRow(int n) |
| 482 | { |
| 483 | if (!m_model || isExpanded(row: n)) |
| 484 | return; |
| 485 | |
| 486 | TreeItem &item = m_items[n]; |
| 487 | if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(parent: item.index)) |
| 488 | return; |
| 489 | item.expanded = true; |
| 490 | m_expandedItems.insert(value: item.index); |
| 491 | QVector<int> changedRole(1, ExpandedRole); |
| 492 | emit dataChanged(topLeft: index(row: n, column: m_column), bottomRight: index(row: n, column: m_column), roles: changedRole); |
| 493 | |
| 494 | m_itemsToExpand.append(t: item); |
| 495 | expandPendingRows(); |
| 496 | } |
| 497 | |
| 498 | void QQmlTreeModelToTableModel::expandRecursively(int row, int depth) |
| 499 | { |
| 500 | Q_ASSERT(depth == -1 || depth > 0); |
| 501 | const int startDepth = depthAtRow(row); |
| 502 | |
| 503 | auto expandHelp = [this, depth, startDepth] (const auto expandHelp, const QModelIndex &index) -> void { |
| 504 | const int rowToExpand = itemIndex(index); |
| 505 | if (!m_expandedItems.contains(value: index)) |
| 506 | expandRow(n: rowToExpand); |
| 507 | |
| 508 | if (depth != -1 && depthAtRow(row: rowToExpand) == startDepth + depth - 1) |
| 509 | return; |
| 510 | |
| 511 | const int childCount = m_model->rowCount(parent: index); |
| 512 | for (int childRow = 0; childRow < childCount; ++childRow) { |
| 513 | const QModelIndex childIndex = m_model->index(row: childRow, column: 0, parent: index); |
| 514 | if (m_model->hasChildren(parent: childIndex)) |
| 515 | expandHelp(expandHelp, childIndex); |
| 516 | } |
| 517 | }; |
| 518 | |
| 519 | const QModelIndex index = m_items[row].index; |
| 520 | if (index.isValid()) |
| 521 | expandHelp(expandHelp, index); |
| 522 | } |
| 523 | |
| 524 | void QQmlTreeModelToTableModel::expandPendingRows(bool doInsertRows) |
| 525 | { |
| 526 | while (!m_itemsToExpand.isEmpty()) { |
| 527 | const TreeItem item = m_itemsToExpand.takeFirst(); |
| 528 | Q_ASSERT(item.expanded); |
| 529 | const QModelIndex &index = item.index; |
| 530 | int childrenCount = m_model->rowCount(parent: index); |
| 531 | if (childrenCount == 0) { |
| 532 | if (m_model->hasChildren(parent: index) && m_model->canFetchMore(parent: index)) |
| 533 | m_model->fetchMore(parent: index); |
| 534 | continue; |
| 535 | } |
| 536 | |
| 537 | // TODO Pre-compute the total number of items made visible |
| 538 | // so that we only call a single beginInsertRows()/endInsertRows() |
| 539 | // pair per expansion (same as we do for collapsing). |
| 540 | showModelChildItems(parentItem: item, start: 0, end: childrenCount - 1, doInsertRows, doExpandPendingRows: false); |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | void QQmlTreeModelToTableModel::collapseRecursively(int row) |
| 545 | { |
| 546 | auto collapseHelp = [this] (const auto collapseHelp, const QModelIndex &index) -> void { |
| 547 | if (m_expandedItems.contains(value: index)) { |
| 548 | const int rowToCollapse = itemIndex(index); |
| 549 | if (rowToCollapse != -1) |
| 550 | collapseRow(n: rowToCollapse); |
| 551 | else |
| 552 | m_expandedItems.remove(value: index); |
| 553 | } |
| 554 | |
| 555 | const int childCount = m_model->rowCount(parent: index); |
| 556 | for (int childRow = 0; childRow < childCount; ++childRow) { |
| 557 | const QModelIndex childIndex = m_model->index(row: childRow, column: 0, parent: index); |
| 558 | if (m_model->hasChildren(parent: childIndex)) |
| 559 | collapseHelp(collapseHelp, childIndex); |
| 560 | } |
| 561 | }; |
| 562 | |
| 563 | const QModelIndex index = m_items[row].index; |
| 564 | if (index.isValid()) |
| 565 | collapseHelp(collapseHelp, index); |
| 566 | } |
| 567 | |
| 568 | void QQmlTreeModelToTableModel::collapseRow(int n) |
| 569 | { |
| 570 | if (!m_model || !isExpanded(row: n)) |
| 571 | return; |
| 572 | |
| 573 | SignalFreezer aggregator(this); |
| 574 | |
| 575 | TreeItem &item = m_items[n]; |
| 576 | item.expanded = false; |
| 577 | m_expandedItems.remove(value: item.index); |
| 578 | QVector<int> changedRole(1, ExpandedRole); |
| 579 | queueDataChanged(topLeft: index(row: n, column: m_column), bottomRight: index(row: n, column: m_column), roles: changedRole); |
| 580 | int childrenCount = m_model->rowCount(parent: item.index); |
| 581 | if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(parent: item.index) || childrenCount == 0) |
| 582 | return; |
| 583 | |
| 584 | const QModelIndex &emi = m_model->index(row: childrenCount - 1, column: 0, parent: item.index); |
| 585 | int lastIndex = lastChildIndex(index: emi); |
| 586 | removeVisibleRows(startIndex: n + 1, endIndex: lastIndex); |
| 587 | } |
| 588 | |
| 589 | int QQmlTreeModelToTableModel::lastChildIndex(const QModelIndex &index) const |
| 590 | { |
| 591 | // The purpose of this function is to return the row of the last decendant of a node N. |
| 592 | // But note: index should point to the last child of N, and not N itself! |
| 593 | // This means that if index is not expanded, the last child will simply be index itself. |
| 594 | // Otherwise, since the tree underneath index can be of any depth, it will instead find |
| 595 | // the first sibling of N, get its table row, and simply return the row above. |
| 596 | if (!m_expandedItems.contains(value: index)) |
| 597 | return itemIndex(index); |
| 598 | |
| 599 | QModelIndex parent = index.parent(); |
| 600 | QModelIndex nextSiblingIndex; |
| 601 | while (parent.isValid()) { |
| 602 | nextSiblingIndex = parent.sibling(arow: parent.row() + 1, acolumn: 0); |
| 603 | if (nextSiblingIndex.isValid()) |
| 604 | break; |
| 605 | parent = parent.parent(); |
| 606 | } |
| 607 | |
| 608 | int firstIndex = nextSiblingIndex.isValid() ? itemIndex(index: nextSiblingIndex) : m_items.size(); |
| 609 | return firstIndex - 1; |
| 610 | } |
| 611 | |
| 612 | void QQmlTreeModelToTableModel::removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows) |
| 613 | { |
| 614 | if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) |
| 615 | return; |
| 616 | |
| 617 | if (doRemoveRows) |
| 618 | beginRemoveRows(parent: QModelIndex(), first: startIndex, last: endIndex); |
| 619 | m_items.erase(abegin: m_items.begin() + startIndex, aend: m_items.begin() + endIndex + 1); |
| 620 | if (doRemoveRows) { |
| 621 | endRemoveRows(); |
| 622 | |
| 623 | /* We need to update the model index for all the items below the removed ones */ |
| 624 | int lastIndex = m_items.size() - 1; |
| 625 | if (startIndex <= lastIndex) { |
| 626 | const QModelIndex &topLeft = index(row: startIndex, column: 0, parent: QModelIndex()); |
| 627 | const QModelIndex &bottomRight = index(row: lastIndex, column: 0, parent: QModelIndex()); |
| 628 | const QVector<int> changedRole(1, ModelIndexRole); |
| 629 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
| 630 | } |
| 631 | } |
| 632 | } |
| 633 | |
| 634 | void QQmlTreeModelToTableModel::modelHasBeenDestroyed() |
| 635 | { |
| 636 | // The model has been deleted. This should behave as if no model was set |
| 637 | clearModelData(); |
| 638 | emit modelChanged(model: nullptr); |
| 639 | } |
| 640 | |
| 641 | void QQmlTreeModelToTableModel::modelHasBeenReset() |
| 642 | { |
| 643 | clearModelData(); |
| 644 | |
| 645 | showModelTopLevelItems(); |
| 646 | ASSERT_CONSISTENCY(); |
| 647 | } |
| 648 | |
| 649 | void QQmlTreeModelToTableModel::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) |
| 650 | { |
| 651 | Q_ASSERT(topLeft.parent() == bottomRight.parent()); |
| 652 | const QModelIndex &parent = topLeft.parent(); |
| 653 | if (parent.isValid() && !childrenVisible(index: parent)) { |
| 654 | ASSERT_CONSISTENCY(); |
| 655 | return; |
| 656 | } |
| 657 | |
| 658 | int topIndex = itemIndex(index: topLeft.siblingAtColumn(acolumn: 0)); |
| 659 | if (topIndex == -1) // 'parent' is not visible anymore, though it's been expanded previously |
| 660 | return; |
| 661 | for (int i = topLeft.row(); i <= bottomRight.row(); i++) { |
| 662 | // Group items with same parent to minize the number of 'dataChanged()' emits |
| 663 | int bottomIndex = topIndex; |
| 664 | while (bottomIndex < m_items.size()) { |
| 665 | const QModelIndex &idx = m_items.at(i: bottomIndex).index; |
| 666 | if (idx.parent() != parent) { |
| 667 | --bottomIndex; |
| 668 | break; |
| 669 | } |
| 670 | if (idx.row() == bottomRight.row()) |
| 671 | break; |
| 672 | ++bottomIndex; |
| 673 | } |
| 674 | emit dataChanged(topLeft: index(row: topIndex, column: topLeft.column()), bottomRight: index(row: bottomIndex, column: bottomRight.column()), roles); |
| 675 | |
| 676 | i += bottomIndex - topIndex; |
| 677 | if (i == bottomRight.row()) |
| 678 | break; |
| 679 | topIndex = bottomIndex + 1; |
| 680 | while (topIndex < m_items.size() |
| 681 | && m_items.at(i: topIndex).index.parent() != parent) |
| 682 | topIndex++; |
| 683 | } |
| 684 | ASSERT_CONSISTENCY(); |
| 685 | } |
| 686 | |
| 687 | void QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) |
| 688 | { |
| 689 | Q_UNUSED(hint) |
| 690 | |
| 691 | // Since the m_items is a list of TreeItems that contains QPersistentModelIndexes, we |
| 692 | // cannot wait until we get a modelLayoutChanged() before we remove the affected rows |
| 693 | // from that list. After the layout has changed, the list (or, the persistent indexes |
| 694 | // that it contains) is no longer in sync with the model (after all, that is what we're |
| 695 | // supposed to correct in modelLayoutChanged()). |
| 696 | // This means that vital functions, like itemIndex(index), cannot be trusted at that point. |
| 697 | // Therefore we need to do the update in two steps; First remove all the affected rows |
| 698 | // from here (while we're still in sync with the model), and then add back the |
| 699 | // affected rows, and notify about it, from modelLayoutChanged(). |
| 700 | m_modelLayoutChanged = false; |
| 701 | |
| 702 | if (parents.isEmpty() || !parents[0].isValid()) { |
| 703 | // Update entire model |
| 704 | emit layoutAboutToBeChanged(); |
| 705 | m_modelLayoutChanged = true; |
| 706 | m_items.clear(); |
| 707 | return; |
| 708 | } |
| 709 | |
| 710 | for (const QPersistentModelIndex &pmi : parents) { |
| 711 | if (!m_expandedItems.contains(value: pmi)) |
| 712 | continue; |
| 713 | const int row = itemIndex(index: pmi); |
| 714 | if (row == -1) |
| 715 | continue; |
| 716 | const int rowCount = m_model->rowCount(parent: pmi); |
| 717 | if (rowCount == 0) |
| 718 | continue; |
| 719 | |
| 720 | if (!m_modelLayoutChanged) { |
| 721 | emit layoutAboutToBeChanged(); |
| 722 | m_modelLayoutChanged = true; |
| 723 | } |
| 724 | |
| 725 | const QModelIndex &lmi = m_model->index(row: rowCount - 1, column: 0, parent: pmi); |
| 726 | const int lastRow = lastChildIndex(index: lmi); |
| 727 | removeVisibleRows(startIndex: row + 1, endIndex: lastRow, doRemoveRows: false /*doRemoveRows*/); |
| 728 | } |
| 729 | |
| 730 | ASSERT_CONSISTENCY(); |
| 731 | } |
| 732 | |
| 733 | void QQmlTreeModelToTableModel::modelLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) |
| 734 | { |
| 735 | Q_UNUSED(hint) |
| 736 | |
| 737 | if (!m_modelLayoutChanged) { |
| 738 | // No relevant changes done from modelLayoutAboutToBeChanged() |
| 739 | return; |
| 740 | } |
| 741 | |
| 742 | if (m_items.isEmpty()) { |
| 743 | // Entire model has changed. Add back all rows. |
| 744 | showModelTopLevelItems(doInsertRows: false /*doInsertRows*/); |
| 745 | const QModelIndex &mi = m_model->index(row: 0, column: 0); |
| 746 | const int columnCount = m_model->columnCount(parent: mi); |
| 747 | emit dataChanged(topLeft: index(row: 0, column: 0), bottomRight: index(row: m_items.size() - 1, column: columnCount - 1)); |
| 748 | emit layoutChanged(); |
| 749 | return; |
| 750 | } |
| 751 | |
| 752 | for (const QPersistentModelIndex &pmi : parents) { |
| 753 | if (!m_expandedItems.contains(value: pmi)) |
| 754 | continue; |
| 755 | const int row = itemIndex(index: pmi); |
| 756 | if (row == -1) |
| 757 | continue; |
| 758 | const int rowCount = m_model->rowCount(parent: pmi); |
| 759 | if (rowCount == 0) |
| 760 | continue; |
| 761 | |
| 762 | const QModelIndex &lmi = m_model->index(row: rowCount - 1, column: 0, parent: pmi); |
| 763 | const int columnCount = m_model->columnCount(parent: lmi); |
| 764 | showModelChildItems(parentItem: m_items.at(i: row), start: 0, end: rowCount - 1, doInsertRows: false /*doInsertRows*/); |
| 765 | const int lastRow = lastChildIndex(index: lmi); |
| 766 | emit dataChanged(topLeft: index(row: row + 1, column: 0), bottomRight: index(row: lastRow, column: columnCount - 1)); |
| 767 | } |
| 768 | |
| 769 | emit layoutChanged(); |
| 770 | |
| 771 | ASSERT_CONSISTENCY(); |
| 772 | } |
| 773 | |
| 774 | void QQmlTreeModelToTableModel::modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end) |
| 775 | { |
| 776 | Q_UNUSED(parent) |
| 777 | Q_UNUSED(start) |
| 778 | Q_UNUSED(end) |
| 779 | ASSERT_CONSISTENCY(); |
| 780 | } |
| 781 | |
| 782 | void QQmlTreeModelToTableModel::modelRowsInserted(const QModelIndex & parent, int start, int end) |
| 783 | { |
| 784 | TreeItem item; |
| 785 | int parentRow = itemIndex(index: parent); |
| 786 | if (parentRow >= 0) { |
| 787 | const QModelIndex& parentIndex = index(row: parentRow, column: m_column); |
| 788 | QVector<int> changedRole(1, HasChildrenRole); |
| 789 | queueDataChanged(topLeft: parentIndex, bottomRight: parentIndex, roles: changedRole); |
| 790 | item = m_items.at(i: parentRow); |
| 791 | if (!item.expanded) { |
| 792 | ASSERT_CONSISTENCY(); |
| 793 | return; |
| 794 | } |
| 795 | } else if (parent == m_rootIndex) { |
| 796 | item = TreeItem(parent); |
| 797 | } else { |
| 798 | ASSERT_CONSISTENCY(); |
| 799 | return; |
| 800 | } |
| 801 | showModelChildItems(parentItem: item, start, end); |
| 802 | ASSERT_CONSISTENCY(); |
| 803 | } |
| 804 | |
| 805 | void QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) |
| 806 | { |
| 807 | ASSERT_CONSISTENCY(); |
| 808 | enableSignalAggregation(); |
| 809 | if (parent == m_rootIndex || childrenVisible(index: parent)) { |
| 810 | const QModelIndex &smi = m_model->index(row: start, column: 0, parent); |
| 811 | int startIndex = itemIndex(index: smi); |
| 812 | const QModelIndex &emi = m_model->index(row: end, column: 0, parent); |
| 813 | int endIndex = -1; |
| 814 | if (isExpanded(index: emi)) { |
| 815 | int rowCount = m_model->rowCount(parent: emi); |
| 816 | if (rowCount > 0) { |
| 817 | const QModelIndex &idx = m_model->index(row: rowCount - 1, column: 0, parent: emi); |
| 818 | endIndex = lastChildIndex(index: idx); |
| 819 | } |
| 820 | } |
| 821 | if (endIndex == -1) |
| 822 | endIndex = itemIndex(index: emi); |
| 823 | |
| 824 | removeVisibleRows(startIndex, endIndex); |
| 825 | } |
| 826 | |
| 827 | for (int r = start; r <= end; r++) { |
| 828 | const QModelIndex &cmi = m_model->index(row: r, column: 0, parent); |
| 829 | m_expandedItems.remove(value: cmi); |
| 830 | } |
| 831 | } |
| 832 | |
| 833 | void QQmlTreeModelToTableModel::modelRowsRemoved(const QModelIndex & parent, int start, int end) |
| 834 | { |
| 835 | Q_UNUSED(start) |
| 836 | Q_UNUSED(end) |
| 837 | int parentRow = itemIndex(index: parent); |
| 838 | if (parentRow >= 0) { |
| 839 | const QModelIndex& parentIndex = index(row: parentRow, column: m_column); |
| 840 | QVector<int> changedRole(1, HasChildrenRole); |
| 841 | queueDataChanged(topLeft: parentIndex, bottomRight: parentIndex, roles: changedRole); |
| 842 | } |
| 843 | disableSignalAggregation(); |
| 844 | ASSERT_CONSISTENCY(); |
| 845 | } |
| 846 | |
| 847 | void QQmlTreeModelToTableModel::modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) |
| 848 | { |
| 849 | ASSERT_CONSISTENCY(); |
| 850 | enableSignalAggregation(); |
| 851 | m_visibleRowsMoved = false; |
| 852 | if (!childrenVisible(index: sourceParent)) |
| 853 | return; // Do nothing now. See modelRowsMoved() below. |
| 854 | |
| 855 | if (!childrenVisible(index: destinationParent)) { |
| 856 | modelRowsAboutToBeRemoved(parent: sourceParent, start: sourceStart, end: sourceEnd); |
| 857 | /* If the destination parent has no children, we'll need to |
| 858 | * report a change on the HasChildrenRole */ |
| 859 | if (isVisible(index: destinationParent) && m_model->rowCount(parent: destinationParent) == 0) { |
| 860 | const QModelIndex &topLeft = index(row: itemIndex(index: destinationParent), column: 0, parent: QModelIndex()); |
| 861 | const QModelIndex &bottomRight = topLeft; |
| 862 | const QVector<int> changedRole(1, HasChildrenRole); |
| 863 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
| 864 | } |
| 865 | } else { |
| 866 | int depthDifference = -1; |
| 867 | if (destinationParent.isValid()) { |
| 868 | int destParentIndex = itemIndex(index: destinationParent); |
| 869 | depthDifference = m_items.at(i: destParentIndex).depth; |
| 870 | } |
| 871 | if (sourceParent.isValid()) { |
| 872 | int sourceParentIndex = itemIndex(index: sourceParent); |
| 873 | depthDifference -= m_items.at(i: sourceParentIndex).depth; |
| 874 | } else { |
| 875 | depthDifference++; |
| 876 | } |
| 877 | |
| 878 | int startIndex = itemIndex(index: m_model->index(row: sourceStart, column: 0, parent: sourceParent)); |
| 879 | const QModelIndex &emi = m_model->index(row: sourceEnd, column: 0, parent: sourceParent); |
| 880 | int endIndex = -1; |
| 881 | if (isExpanded(index: emi)) { |
| 882 | int rowCount = m_model->rowCount(parent: emi); |
| 883 | if (rowCount > 0) |
| 884 | endIndex = lastChildIndex(index: m_model->index(row: rowCount - 1, column: 0, parent: emi)); |
| 885 | } |
| 886 | if (endIndex == -1) |
| 887 | endIndex = itemIndex(index: emi); |
| 888 | |
| 889 | int destIndex = -1; |
| 890 | if (destinationRow == m_model->rowCount(parent: destinationParent)) { |
| 891 | const QModelIndex &emi = m_model->index(row: destinationRow - 1, column: 0, parent: destinationParent); |
| 892 | destIndex = lastChildIndex(index: emi) + 1; |
| 893 | } else { |
| 894 | destIndex = itemIndex(index: m_model->index(row: destinationRow, column: 0, parent: destinationParent)); |
| 895 | } |
| 896 | |
| 897 | int totalMovedCount = endIndex - startIndex + 1; |
| 898 | |
| 899 | /* This beginMoveRows() is matched by a endMoveRows() in the |
| 900 | * modelRowsMoved() method below. */ |
| 901 | m_visibleRowsMoved = startIndex != destIndex && |
| 902 | beginMoveRows(sourceParent: QModelIndex(), sourceFirst: startIndex, sourceLast: endIndex, destinationParent: QModelIndex(), destinationRow: destIndex); |
| 903 | |
| 904 | const QList<TreeItem> &buffer = m_items.mid(pos: startIndex, len: totalMovedCount); |
| 905 | int bufferCopyOffset; |
| 906 | if (destIndex > endIndex) { |
| 907 | for (int i = endIndex + 1; i < destIndex; i++) { |
| 908 | m_items.swapItemsAt(i, j: i - totalMovedCount); // Fast move from 1st to 2nd position |
| 909 | } |
| 910 | bufferCopyOffset = destIndex - totalMovedCount; |
| 911 | } else { |
| 912 | // NOTE: we will not enter this loop if startIndex == destIndex |
| 913 | for (int i = startIndex - 1; i >= destIndex; i--) { |
| 914 | m_items.swapItemsAt(i, j: i + totalMovedCount); // Fast move from 1st to 2nd position |
| 915 | } |
| 916 | bufferCopyOffset = destIndex; |
| 917 | } |
| 918 | for (int i = 0; i < buffer.size(); i++) { |
| 919 | TreeItem item = buffer.at(i); |
| 920 | item.depth += depthDifference; |
| 921 | m_items.replace(i: bufferCopyOffset + i, t: item); |
| 922 | } |
| 923 | |
| 924 | /* If both source and destination items are visible, the indexes of |
| 925 | * all the items in between will change. If they share the same |
| 926 | * parent, then this is all; however, if they belong to different |
| 927 | * parents, their bottom siblings will also get displaced, so their |
| 928 | * index also needs to be updated. |
| 929 | * Given that the bottom siblings of the top moved elements are |
| 930 | * already included in the update (since they lie between the |
| 931 | * source and the dest elements), we only need to worry about the |
| 932 | * siblings of the bottom moved element. |
| 933 | */ |
| 934 | const int top = qMin(a: startIndex, b: bufferCopyOffset); |
| 935 | int bottom = qMax(a: endIndex, b: bufferCopyOffset + totalMovedCount - 1); |
| 936 | if (sourceParent != destinationParent) { |
| 937 | const QModelIndex &bottomParent = |
| 938 | bottom == endIndex ? sourceParent : destinationParent; |
| 939 | |
| 940 | const int rowCount = m_model->rowCount(parent: bottomParent); |
| 941 | if (rowCount > 0) |
| 942 | bottom = qMax(a: bottom, b: lastChildIndex(index: m_model->index(row: rowCount - 1, column: 0, parent: bottomParent))); |
| 943 | } |
| 944 | const QModelIndex &topLeft = index(row: top, column: 0, parent: QModelIndex()); |
| 945 | const QModelIndex &bottomRight = index(row: bottom, column: 0, parent: QModelIndex()); |
| 946 | const QVector<int> changedRole(1, ModelIndexRole); |
| 947 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
| 948 | |
| 949 | if (depthDifference != 0) { |
| 950 | const QModelIndex &topLeft = index(row: bufferCopyOffset, column: 0, parent: QModelIndex()); |
| 951 | const QModelIndex &bottomRight = index(row: bufferCopyOffset + totalMovedCount - 1, column: 0, parent: QModelIndex()); |
| 952 | const QVector<int> changedRole(1, DepthRole); |
| 953 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
| 954 | } |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | void QQmlTreeModelToTableModel::modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) |
| 959 | { |
| 960 | if (!childrenVisible(index: sourceParent)) { |
| 961 | modelRowsInserted(parent: destinationParent, start: destinationRow, end: destinationRow + sourceEnd - sourceStart); |
| 962 | } else if (!childrenVisible(index: destinationParent)) { |
| 963 | modelRowsRemoved(parent: sourceParent, start: sourceStart, end: sourceEnd); |
| 964 | } |
| 965 | |
| 966 | if (m_visibleRowsMoved) |
| 967 | endMoveRows(); |
| 968 | |
| 969 | if (isVisible(index: sourceParent) && m_model->rowCount(parent: sourceParent) == 0) { |
| 970 | int parentRow = itemIndex(index: sourceParent); |
| 971 | collapseRow(n: parentRow); |
| 972 | const QModelIndex &topLeft = index(row: parentRow, column: 0, parent: QModelIndex()); |
| 973 | const QModelIndex &bottomRight = topLeft; |
| 974 | const QVector<int> changedRole { ExpandedRole, HasChildrenRole }; |
| 975 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
| 976 | } |
| 977 | |
| 978 | disableSignalAggregation(); |
| 979 | |
| 980 | ASSERT_CONSISTENCY(); |
| 981 | } |
| 982 | |
| 983 | void QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted(const QModelIndex & parent, int start, int end) |
| 984 | { |
| 985 | Q_UNUSED(parent); |
| 986 | beginInsertColumns(parent: {}, first: start, last: end); |
| 987 | } |
| 988 | |
| 989 | void QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved(const QModelIndex & parent, int start, int end) |
| 990 | { |
| 991 | Q_UNUSED(parent); |
| 992 | beginRemoveColumns(parent: {}, first: start, last: end); |
| 993 | } |
| 994 | |
| 995 | void QQmlTreeModelToTableModel::modelColumnsInserted(const QModelIndex & parent, int start, int end) |
| 996 | { |
| 997 | Q_UNUSED(parent); |
| 998 | Q_UNUSED(start); |
| 999 | Q_UNUSED(end); |
| 1000 | endInsertColumns(); |
| 1001 | m_items.clear(); |
| 1002 | showModelTopLevelItems(); |
| 1003 | ASSERT_CONSISTENCY(); |
| 1004 | } |
| 1005 | |
| 1006 | void QQmlTreeModelToTableModel::modelColumnsRemoved(const QModelIndex & parent, int start, int end) |
| 1007 | { |
| 1008 | Q_UNUSED(parent); |
| 1009 | Q_UNUSED(start); |
| 1010 | Q_UNUSED(end); |
| 1011 | endRemoveColumns(); |
| 1012 | m_items.clear(); |
| 1013 | showModelTopLevelItems(); |
| 1014 | ASSERT_CONSISTENCY(); |
| 1015 | } |
| 1016 | |
| 1017 | void QQmlTreeModelToTableModel::dump() const |
| 1018 | { |
| 1019 | if (!m_model) |
| 1020 | return; |
| 1021 | int count = m_items.size(); |
| 1022 | if (count == 0) |
| 1023 | return; |
| 1024 | int countWidth = floor(x: log10(x: double(count))) + 1; |
| 1025 | qInfo() << "Dumping" << this; |
| 1026 | for (int i = 0; i < count; i++) { |
| 1027 | const TreeItem &item = m_items.at(i); |
| 1028 | bool hasChildren = m_model->hasChildren(parent: item.index); |
| 1029 | int children = m_model->rowCount(parent: item.index); |
| 1030 | qInfo().noquote().nospace() |
| 1031 | << QStringLiteral("%1 " ).arg(a: i, fieldWidth: countWidth) << QString(4 * item.depth, QChar::fromLatin1(c: '.')) |
| 1032 | << QLatin1String(!hasChildren ? ".. " : item.expanded ? " v " : " > " ) |
| 1033 | << item.index << children; |
| 1034 | } |
| 1035 | } |
| 1036 | |
| 1037 | bool QQmlTreeModelToTableModel::testConsistency(bool dumpOnFail) const |
| 1038 | { |
| 1039 | if (!m_model) { |
| 1040 | if (!m_items.isEmpty()) { |
| 1041 | qWarning() << "Model inconsistency: No model but stored visible items" ; |
| 1042 | return false; |
| 1043 | } |
| 1044 | if (!m_expandedItems.isEmpty()) { |
| 1045 | qWarning() << "Model inconsistency: No model but stored expanded items" ; |
| 1046 | return false; |
| 1047 | } |
| 1048 | return true; |
| 1049 | } |
| 1050 | QModelIndex parent = m_rootIndex; |
| 1051 | QStack<QModelIndex> ancestors; |
| 1052 | QModelIndex idx = m_model->index(row: 0, column: 0, parent); |
| 1053 | for (int i = 0; i < m_items.size(); i++) { |
| 1054 | bool isConsistent = true; |
| 1055 | const TreeItem &item = m_items.at(i); |
| 1056 | if (item.index != idx) { |
| 1057 | qWarning() << "QModelIndex inconsistency" << i << item.index; |
| 1058 | qWarning() << " expected" << idx; |
| 1059 | isConsistent = false; |
| 1060 | } |
| 1061 | if (item.index.parent() != parent) { |
| 1062 | qWarning() << "Parent inconsistency" << i << item.index; |
| 1063 | qWarning() << " stored index parent" << item.index.parent() << "model parent" << parent; |
| 1064 | isConsistent = false; |
| 1065 | } |
| 1066 | if (item.depth != ancestors.size()) { |
| 1067 | qWarning() << "Depth inconsistency" << i << item.index; |
| 1068 | qWarning() << " item depth" << item.depth << "ancestors stack" << ancestors.size(); |
| 1069 | isConsistent = false; |
| 1070 | } |
| 1071 | if (item.expanded && !m_expandedItems.contains(value: item.index)) { |
| 1072 | qWarning() << "Expanded inconsistency" << i << item.index; |
| 1073 | qWarning() << " set" << m_expandedItems.contains(value: item.index) << "item" << item.expanded; |
| 1074 | isConsistent = false; |
| 1075 | } |
| 1076 | if (!isConsistent) { |
| 1077 | if (dumpOnFail) |
| 1078 | dump(); |
| 1079 | return false; |
| 1080 | } |
| 1081 | QModelIndex firstChildIndex; |
| 1082 | if (item.expanded) |
| 1083 | firstChildIndex = m_model->index(row: 0, column: 0, parent: idx); |
| 1084 | if (firstChildIndex.isValid()) { |
| 1085 | ancestors.push(t: parent); |
| 1086 | parent = idx; |
| 1087 | idx = m_model->index(row: 0, column: 0, parent); |
| 1088 | } else { |
| 1089 | while (idx.row() == m_model->rowCount(parent) - 1) { |
| 1090 | if (ancestors.isEmpty()) |
| 1091 | break; |
| 1092 | idx = parent; |
| 1093 | parent = ancestors.pop(); |
| 1094 | } |
| 1095 | idx = m_model->index(row: idx.row() + 1, column: 0, parent); |
| 1096 | } |
| 1097 | } |
| 1098 | |
| 1099 | return true; |
| 1100 | } |
| 1101 | |
| 1102 | void QQmlTreeModelToTableModel::enableSignalAggregation() { |
| 1103 | m_signalAggregatorStack++; |
| 1104 | } |
| 1105 | |
| 1106 | void QQmlTreeModelToTableModel::disableSignalAggregation() { |
| 1107 | m_signalAggregatorStack--; |
| 1108 | Q_ASSERT(m_signalAggregatorStack >= 0); |
| 1109 | if (m_signalAggregatorStack == 0) { |
| 1110 | emitQueuedSignals(); |
| 1111 | } |
| 1112 | } |
| 1113 | |
| 1114 | void QQmlTreeModelToTableModel::queueDataChanged(const QModelIndex &topLeft, |
| 1115 | const QModelIndex &bottomRight, |
| 1116 | const QVector<int> &roles) |
| 1117 | { |
| 1118 | if (isAggregatingSignals()) { |
| 1119 | m_queuedDataChanged.append(t: DataChangedParams { .topLeft: topLeft, .bottomRight: bottomRight, .roles: roles }); |
| 1120 | } else { |
| 1121 | emit dataChanged(topLeft, bottomRight, roles); |
| 1122 | } |
| 1123 | } |
| 1124 | |
| 1125 | void QQmlTreeModelToTableModel::emitQueuedSignals() |
| 1126 | { |
| 1127 | QVector<DataChangedParams> combinedUpdates; |
| 1128 | /* First, iterate through the queued updates and merge the overlapping ones |
| 1129 | * to reduce the number of updates. |
| 1130 | * We don't merge adjacent updates, because they are typically filed with a |
| 1131 | * different role (a parent row is next to its children). |
| 1132 | */ |
| 1133 | for (const DataChangedParams &dataChange : std::as_const(t&: m_queuedDataChanged)) { |
| 1134 | int startRow = dataChange.topLeft.row(); |
| 1135 | int endRow = dataChange.bottomRight.row(); |
| 1136 | bool merged = false; |
| 1137 | for (DataChangedParams &combined : combinedUpdates) { |
| 1138 | int combinedStartRow = combined.topLeft.row(); |
| 1139 | int combinedEndRow = combined.bottomRight.row(); |
| 1140 | if ((startRow <= combinedStartRow && endRow >= combinedStartRow) || |
| 1141 | (startRow <= combinedEndRow && endRow >= combinedEndRow)) { |
| 1142 | if (startRow < combinedStartRow) { |
| 1143 | combined.topLeft = dataChange.topLeft; |
| 1144 | } |
| 1145 | if (endRow > combinedEndRow) { |
| 1146 | combined.bottomRight = dataChange.bottomRight; |
| 1147 | } |
| 1148 | for (int role : dataChange.roles) { |
| 1149 | if (!combined.roles.contains(t: role)) |
| 1150 | combined.roles.append(t: role); |
| 1151 | } |
| 1152 | merged = true; |
| 1153 | break; |
| 1154 | } |
| 1155 | } |
| 1156 | if (!merged) { |
| 1157 | combinedUpdates.append(t: dataChange); |
| 1158 | } |
| 1159 | } |
| 1160 | |
| 1161 | /* Finally, emit the dataChanged signals */ |
| 1162 | for (const DataChangedParams &dataChange : combinedUpdates) { |
| 1163 | emit dataChanged(topLeft: dataChange.topLeft, bottomRight: dataChange.bottomRight, roles: dataChange.roles); |
| 1164 | } |
| 1165 | m_queuedDataChanged.clear(); |
| 1166 | } |
| 1167 | |
| 1168 | QT_END_NAMESPACE |
| 1169 | |
| 1170 | #include "moc_qqmltreemodeltotablemodel_p_p.cpp" |
| 1171 | |