diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b26f7c..a05addc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,78 +1,82 @@ cmake_minimum_required(VERSION 3.0) set(KF5_VERSION "5.41.0") # handled by release scripts project(KItemModels VERSION ${KF5_VERSION}) include(FeatureSummary) find_package(ECM 5.40.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) +include(ECMQtDeclareLoggingCategory) set(REQUIRED_QT_VERSION 5.7.0) find_package(Qt5Core ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) include(GenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMAddQch) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") ecm_setup_version(PROJECT VARIABLE_PREFIX KITEMMODELS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kitemmodels_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5ItemModelsConfigVersion.cmake" SOVERSION 5) add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) add_subdirectory(tests) endif() # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5ItemModels") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5ItemModels_QCH FILE KF5ItemModelsQCHTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5ItemModelsQCHTargets.cmake\")") endif() include(CMakePackageConfigHelpers) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KF5ItemModelsConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5ItemModelsConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5ItemModelsConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5ItemModelsConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KF5ItemModelsTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5ItemModelsTargets.cmake NAMESPACE KF5:: ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kitemmodels_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) +# contains list of debug categories, for kdebugsettings +install(FILES kitemmodels.categories DESTINATION ${KDE_INSTALL_CONFDIR}) + feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/kitemmodels.categories b/kitemmodels.categories new file mode 100644 index 0000000..7c32d91 --- /dev/null +++ b/kitemmodels.categories @@ -0,0 +1 @@ +kf5.kitemmodels KItemModels diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 641dd1d..c6e4a6c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,99 +1,101 @@ set(kitemmodels_SRCS kbreadcrumbselectionmodel.cpp kcheckableproxymodel.cpp kconcatenaterowsproxymodel.cpp kdescendantsproxymodel.cpp kextracolumnsproxymodel.cpp klinkitemselectionmodel.cpp kmodelindexproxymapper.cpp krearrangecolumnsproxymodel.cpp krecursivefilterproxymodel.cpp kselectionproxymodel.cpp ) +ecm_qt_declare_logging_category(kitemmodels_SRCS HEADER kitemmodels_debug.h IDENTIFIER KITEMMODELS_LOG CATEGORY_NAME kf5.kitemmodels) + add_library(KF5ItemModels ${kitemmodels_SRCS}) generate_export_header(KF5ItemModels BASE_NAME KItemModels) add_library(KF5::ItemModels ALIAS KF5ItemModels) target_include_directories(KF5ItemModels INTERFACE "$") target_link_libraries(KF5ItemModels PUBLIC Qt5::Core) set_target_properties(KF5ItemModels PROPERTIES VERSION ${KITEMMODELS_VERSION_STRING} SOVERSION ${KITEMMODELS_SOVERSION} EXPORT_NAME ItemModels ) ecm_generate_headers(KItemModels_HEADERS HEADER_NAMES KBreadcrumbSelectionModel KConcatenateRowsProxyModel KCheckableProxyModel KExtraColumnsProxyModel KLinkItemSelectionModel KRearrangeColumnsProxyModel KRecursiveFilterProxyModel KDescendantsProxyModel KModelIndexProxyMapper KSelectionProxyModel REQUIRED_HEADERS KItemModels_HEADERS ) find_package(PythonModuleGeneration) if (PythonModuleGeneration_FOUND) ecm_generate_python_binding( TARGET KF5::ItemModels PYTHONNAMESPACE PyKF5 MODULENAME KItemModels INSTALL_DIR_SUFFIX ${KDE_INSTALL_PYTHONBINDINGSDIR} SIP_DEPENDS QtCore/QtCoremod.sip HEADERS kbreadcrumbselectionmodel.h kconcatenaterowsproxymodel.h kcheckableproxymodel.h kextracolumnsproxymodel.h klinkitemselectionmodel.h krearrangecolumnsproxymodel.h krecursivefilterproxymodel.h kdescendantsproxymodel.h kmodelindexproxymapper.h kselectionproxymodel.h ) endif() install(TARGETS KF5ItemModels EXPORT KF5ItemModelsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kitemmodels_export.h ${KItemModels_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KItemModels COMPONENT Devel ) if(BUILD_QCH) ecm_add_qch( KF5ItemModels_QCH NAME KItemModels BASE_NAME KF5ItemModels VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KItemModels_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt5Core_QCH BLANK_MACROS KITEMMODELS_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KItemModels LIB_NAME KF5ItemModels DEPS "core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KItemModels) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/kextracolumnsproxymodel.cpp b/src/kextracolumnsproxymodel.cpp index 3626d26..bd7fedc 100644 --- a/src/kextracolumnsproxymodel.cpp +++ b/src/kextracolumnsproxymodel.cpp @@ -1,338 +1,339 @@ /* Copyright (c) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Authors: David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kextracolumnsproxymodel.h" +#include "kitemmodels_debug.h" + #include -#include class KExtraColumnsProxyModelPrivate { Q_DECLARE_PUBLIC(KExtraColumnsProxyModel) KExtraColumnsProxyModel *const q_ptr; public: KExtraColumnsProxyModelPrivate(KExtraColumnsProxyModel *model) : q_ptr(model) { } void _ec_sourceLayoutAboutToBeChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint); void _ec_sourceLayoutChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint); // Configuration (doesn't change once source model is plugged in) QVector m_extraHeaders; // for layoutAboutToBeChanged/layoutChanged QVector layoutChangePersistentIndexes; QVector layoutChangeProxyColumns; QModelIndexList proxyIndexes; }; KExtraColumnsProxyModel::KExtraColumnsProxyModel(QObject *parent) : QIdentityProxyModel(parent), d_ptr(new KExtraColumnsProxyModelPrivate(this)) { } KExtraColumnsProxyModel::~KExtraColumnsProxyModel() { } void KExtraColumnsProxyModel::appendColumn(const QString &header) { Q_D(KExtraColumnsProxyModel); d->m_extraHeaders.append(header); } void KExtraColumnsProxyModel::removeExtraColumn(int idx) { Q_D(KExtraColumnsProxyModel); d->m_extraHeaders.remove(idx); } bool KExtraColumnsProxyModel::setExtraColumnData(const QModelIndex &parent, int row, int extraColumn, const QVariant &data, int role) { Q_UNUSED(parent); Q_UNUSED(row); Q_UNUSED(extraColumn); Q_UNUSED(data); Q_UNUSED(role); return false; } void KExtraColumnsProxyModel::extraColumnDataChanged(const QModelIndex &parent, int row, int extraColumn, const QVector &roles) { const QModelIndex idx = index(row, proxyColumnForExtraColumn(extraColumn), parent); emit dataChanged(idx, idx, roles); } void KExtraColumnsProxyModel::setSourceModel(QAbstractItemModel *model) { if (sourceModel()) { disconnect(sourceModel(), SIGNAL(layoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_ec_sourceLayoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint))); disconnect(sourceModel(), SIGNAL(layoutChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_ec_sourceLayoutChanged(QList,QAbstractItemModel::LayoutChangeHint))); } QIdentityProxyModel::setSourceModel(model); if (model) { // The handling of persistent model indexes assumes mapToSource can be called for any index // This breaks for the extra column, so we'll have to do it ourselves disconnect(model, SIGNAL(layoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_q_sourceLayoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint))); disconnect(model, SIGNAL(layoutChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_q_sourceLayoutChanged(QList,QAbstractItemModel::LayoutChangeHint))); connect(model, SIGNAL(layoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_ec_sourceLayoutAboutToBeChanged(QList,QAbstractItemModel::LayoutChangeHint))); connect(model, SIGNAL(layoutChanged(QList,QAbstractItemModel::LayoutChangeHint)), this, SLOT(_ec_sourceLayoutChanged(QList,QAbstractItemModel::LayoutChangeHint))); } } QModelIndex KExtraColumnsProxyModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) { // happens in e.g. rowCount(mapToSource(parent)) return QModelIndex(); } const int column = proxyIndex.column(); if (column >= sourceModel()->columnCount()) { - qDebug() << "Returning invalid index in mapToSource"; + qCDebug(KITEMMODELS_LOG) << "Returning invalid index in mapToSource"; return QModelIndex(); } return QIdentityProxyModel::mapToSource(proxyIndex); } QModelIndex KExtraColumnsProxyModel::buddy(const QModelIndex &proxyIndex) const { const int column = proxyIndex.column(); if (column >= sourceModel()->columnCount()) { return proxyIndex; } return QIdentityProxyModel::buddy(proxyIndex); } QModelIndex KExtraColumnsProxyModel::sibling(int row, int column, const QModelIndex &idx) const { if (row == idx.row() && column == idx.column()) { return idx; } return index(row, column, parent(idx)); } QItemSelection KExtraColumnsProxyModel::mapSelectionToSource(const QItemSelection &selection) const { QItemSelection sourceSelection; if (!sourceModel()) { return sourceSelection; } // mapToSource will give invalid index for our additional columns, so truncate the selection // to the columns known by the source model const int sourceColumnCount = sourceModel()->columnCount(); QItemSelection::const_iterator it = selection.constBegin(); const QItemSelection::const_iterator end = selection.constEnd(); for (; it != end; ++it) { Q_ASSERT(it->model() == this); QModelIndex topLeft = it->topLeft(); Q_ASSERT(topLeft.isValid()); Q_ASSERT(topLeft.model() == this); topLeft = topLeft.sibling(topLeft.row(), 0); QModelIndex bottomRight = it->bottomRight(); Q_ASSERT(bottomRight.isValid()); Q_ASSERT(bottomRight.model() == this); if (bottomRight.column() >= sourceColumnCount) { bottomRight = bottomRight.sibling(bottomRight.row(), sourceColumnCount - 1); } // This can lead to duplicate source indexes, so use merge(). const QItemSelectionRange range(mapToSource(topLeft), mapToSource(bottomRight)); QItemSelection newSelection; newSelection << range; sourceSelection.merge(newSelection, QItemSelectionModel::Select); } return sourceSelection; } int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const { Q_D(const KExtraColumnsProxyModel); return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count(); } QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const { Q_D(const KExtraColumnsProxyModel); const int extraCol = extraColumnForProxyColumn(index.column()); if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) { return extraColumnData(index.parent(), index.row(), extraCol, role); } return sourceModel()->data(mapToSource(index), role); } bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_D(const KExtraColumnsProxyModel); const int extraCol = extraColumnForProxyColumn(index.column()); if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) { return setExtraColumnData(index.parent(), index.row(), extraCol, value, role); } return sourceModel()->setData(mapToSource(index), value, role); } Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const { const int extraCol = extraColumnForProxyColumn(index.column()); if (extraCol >= 0) { // extra columns are readonly return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } return sourceModel()->flags(mapToSource(index)); } bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const { if (index.column() > 0) { return false; } return QIdentityProxyModel::hasChildren(index); } QVariant KExtraColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_D(const KExtraColumnsProxyModel); if (orientation == Qt::Horizontal) { const int extraCol = extraColumnForProxyColumn(section); if (extraCol >= 0) { // Only text is supported, in headers for extra columns if (role == Qt::DisplayRole) { return d->m_extraHeaders.at(extraCol); } return QVariant(); } } return QIdentityProxyModel::headerData(section, orientation, role); } QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const { const int extraCol = extraColumnForProxyColumn(column); if (extraCol >= 0) { // We store the internal pointer of the index for column 0 in the proxy index for extra columns. // This will be useful in the parent method. return createIndex(row, column, QIdentityProxyModel::index(row, 0, parent).internalPointer()); } return QIdentityProxyModel::index(row, column, parent); } QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const { const int extraCol = extraColumnForProxyColumn(child.column()); if (extraCol >= 0) { // Create an index for column 0 and use that to get the parent. const QModelIndex proxySibling = createIndex(child.row(), 0, child.internalPointer()); return QIdentityProxyModel::parent(proxySibling); } return QIdentityProxyModel::parent(child); } int KExtraColumnsProxyModel::extraColumnForProxyColumn(int proxyColumn) const { const int sourceColumnCount = sourceModel()->columnCount(); if (proxyColumn >= sourceColumnCount) { return proxyColumn - sourceColumnCount; } return -1; } int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const { return sourceModel()->columnCount() + extraColumn; } void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutAboutToBeChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint) { Q_Q(KExtraColumnsProxyModel); QList parents; parents.reserve(sourceParents.size()); foreach (const QPersistentModelIndex &parent, sourceParents) { if (!parent.isValid()) { parents << QPersistentModelIndex(); continue; } const QModelIndex mappedParent = q->mapFromSource(parent); Q_ASSERT(mappedParent.isValid()); parents << mappedParent; } emit q->layoutAboutToBeChanged(parents, hint); const QModelIndexList persistentIndexList = q->persistentIndexList(); layoutChangePersistentIndexes.reserve(persistentIndexList.size()); layoutChangeProxyColumns.reserve(persistentIndexList.size()); foreach (QPersistentModelIndex proxyPersistentIndex, persistentIndexList) { proxyIndexes << proxyPersistentIndex; Q_ASSERT(proxyPersistentIndex.isValid()); const int column = proxyPersistentIndex.column(); layoutChangeProxyColumns << column; if (column >= q->sourceModel()->columnCount()) { proxyPersistentIndex = proxyPersistentIndex.sibling(proxyPersistentIndex.row(), 0); } const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex); Q_ASSERT(srcPersistentIndex.isValid()); layoutChangePersistentIndexes << srcPersistentIndex; } } void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint) { Q_Q(KExtraColumnsProxyModel); for (int i = 0; i < proxyIndexes.size(); ++i) { const QModelIndex proxyIdx = proxyIndexes.at(i); QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i)); if (proxyIdx.column() >= q->sourceModel()->columnCount()) { newProxyIdx = newProxyIdx.sibling(newProxyIdx.row(), layoutChangeProxyColumns.at(i)); } q->changePersistentIndex(proxyIdx, newProxyIdx); } layoutChangePersistentIndexes.clear(); layoutChangeProxyColumns.clear(); proxyIndexes.clear(); QList parents; parents.reserve(sourceParents.size()); foreach (const QPersistentModelIndex &parent, sourceParents) { if (!parent.isValid()) { parents << QPersistentModelIndex(); continue; } const QModelIndex mappedParent = q->mapFromSource(parent); Q_ASSERT(mappedParent.isValid()); parents << mappedParent; } emit q->layoutChanged(parents, hint); } #include "moc_kextracolumnsproxymodel.cpp" diff --git a/src/klinkitemselectionmodel.cpp b/src/klinkitemselectionmodel.cpp index 82f016e..b4aad68 100644 --- a/src/klinkitemselectionmodel.cpp +++ b/src/klinkitemselectionmodel.cpp @@ -1,243 +1,242 @@ /* Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net, author Stephen Kelly Copyright (c) 2016 Ableton AG Author Stephen Kelly This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "klinkitemselectionmodel.h" - +#include "kitemmodels_debug.h" #include "kmodelindexproxymapper.h" #include -#include class KLinkItemSelectionModelPrivate { public: KLinkItemSelectionModelPrivate(KLinkItemSelectionModel *proxySelectionModel) : q_ptr(proxySelectionModel), m_linkedItemSelectionModel(nullptr), m_ignoreCurrentChanged(false), m_indexMapper(nullptr) { QObject::connect(q_ptr, &QItemSelectionModel::currentChanged, q_ptr, [this](const QModelIndex& idx) { slotCurrentChanged(idx); } ); QObject::connect(q_ptr, &QItemSelectionModel::modelChanged, q_ptr, [this] { reinitializeIndexMapper(); }); } Q_DECLARE_PUBLIC(KLinkItemSelectionModel) KLinkItemSelectionModel *const q_ptr; bool assertSelectionValid(const QItemSelection &selection) const { Q_FOREACH (const QItemSelectionRange &range, selection) { if (!range.isValid()) { - qDebug() << selection; + qCDebug(KITEMMODELS_LOG) << selection; } Q_ASSERT(range.isValid()); } return true; } void reinitializeIndexMapper() { delete m_indexMapper; m_indexMapper = nullptr; if (!q_ptr->model() || !m_linkedItemSelectionModel || !m_linkedItemSelectionModel->model()) { return; } m_indexMapper = new KModelIndexProxyMapper( q_ptr->model(), m_linkedItemSelectionModel->model(), q_ptr); const QItemSelection mappedSelection = m_indexMapper->mapSelectionRightToLeft(m_linkedItemSelectionModel->selection()); q_ptr->QItemSelectionModel::select(mappedSelection, QItemSelectionModel::ClearAndSelect); } void sourceSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void sourceCurrentChanged(const QModelIndex ¤t); void slotCurrentChanged(const QModelIndex ¤t); QItemSelectionModel *m_linkedItemSelectionModel; bool m_ignoreCurrentChanged; KModelIndexProxyMapper * m_indexMapper; }; KLinkItemSelectionModel::KLinkItemSelectionModel(QAbstractItemModel *model, QItemSelectionModel *proxySelector, QObject *parent) : QItemSelectionModel(model, parent), d_ptr(new KLinkItemSelectionModelPrivate(this)) { setLinkedItemSelectionModel(proxySelector); } KLinkItemSelectionModel::KLinkItemSelectionModel(QObject *parent) : QItemSelectionModel(nullptr, parent), d_ptr(new KLinkItemSelectionModelPrivate(this)) { } KLinkItemSelectionModel::~KLinkItemSelectionModel() { delete d_ptr; } QItemSelectionModel *KLinkItemSelectionModel::linkedItemSelectionModel() const { Q_D(const KLinkItemSelectionModel); return d->m_linkedItemSelectionModel; } void KLinkItemSelectionModel::setLinkedItemSelectionModel(QItemSelectionModel *selectionModel) { Q_D(KLinkItemSelectionModel); if (d->m_linkedItemSelectionModel != selectionModel) { if (d->m_linkedItemSelectionModel) { disconnect(d->m_linkedItemSelectionModel); } d->m_linkedItemSelectionModel = selectionModel; if (d->m_linkedItemSelectionModel) { connect(d->m_linkedItemSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(sourceSelectionChanged(QItemSelection,QItemSelection))); connect(d->m_linkedItemSelectionModel, SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(sourceCurrentChanged(QModelIndex))); connect(d->m_linkedItemSelectionModel, &QItemSelectionModel::modelChanged, this, [this] { d_ptr->reinitializeIndexMapper(); }); } d->reinitializeIndexMapper(); Q_EMIT linkedItemSelectionModelChanged(); } } void KLinkItemSelectionModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) { Q_D(KLinkItemSelectionModel); // When an item is removed, the current index is set to the top index in the model. // That causes a selectionChanged signal with a selection which we do not want. if (d->m_ignoreCurrentChanged) { return; } // Do *not* replace next line with: QItemSelectionModel::select(index, command) // // Doing so would end up calling KLinkItemSelectionModel::select(QItemSelection, QItemSelectionModel::SelectionFlags) // // This is because the code for QItemSelectionModel::select(QModelIndex, QItemSelectionModel::SelectionFlags) looks like this: // { // QItemSelection selection(index, index); // select(selection, command); // } // So it calls KLinkItemSelectionModel overload of // select(QItemSelection, QItemSelectionModel::SelectionFlags) // // When this happens and the selection flags include Toggle, it causes the // selection to be toggled twice. QItemSelectionModel::select(QItemSelection(index, index), command); if (index.isValid()) { d->m_linkedItemSelectionModel->select(d->m_indexMapper->mapSelectionLeftToRight(QItemSelection(index, index)), command); } else { d->m_linkedItemSelectionModel->clearSelection(); } } // QAbstractProxyModel::mapSelectionFromSource creates invalid ranges to we filter // those out manually in a loop. Hopefully fixed in Qt 4.7.2, so we ifdef it out. // http://qt.gitorious.org/qt/qt/merge_requests/2474 // http://qt.gitorious.org/qt/qt/merge_requests/831 #if QT_VERSION < 0x040702 #define RANGE_FIX_HACK #endif #ifdef RANGE_FIX_HACK static QItemSelection klink_removeInvalidRanges(const QItemSelection &selection) { QItemSelection result; Q_FOREACH (const QItemSelectionRange &range, selection) { if (!range.isValid()) { continue; } result << range; } return result; } #endif void KLinkItemSelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) { Q_D(KLinkItemSelectionModel); d->m_ignoreCurrentChanged = true; #ifdef RANGE_FIX_HACK QItemSelection _selection = klink_removeInvalidRanges(selection); #else QItemSelection _selection = selection; #endif QItemSelectionModel::select(_selection, command); Q_ASSERT(d->assertSelectionValid(_selection)); QItemSelection mappedSelection = d->m_indexMapper->mapSelectionLeftToRight(_selection); Q_ASSERT(d->assertSelectionValid(mappedSelection)); d->m_linkedItemSelectionModel->select(mappedSelection, command); d->m_ignoreCurrentChanged = false; } void KLinkItemSelectionModelPrivate::slotCurrentChanged(const QModelIndex ¤t) { const QModelIndex mappedCurrent = m_indexMapper->mapLeftToRight(current); if (!mappedCurrent.isValid()) { return; } m_linkedItemSelectionModel->setCurrentIndex(mappedCurrent, QItemSelectionModel::NoUpdate); } void KLinkItemSelectionModelPrivate::sourceSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_Q(KLinkItemSelectionModel); #ifdef RANGE_FIX_HACK QItemSelection _selected = klink_removeInvalidRanges(selected); QItemSelection _deselected = klink_removeInvalidRanges(deselected); #else QItemSelection _selected = selected; QItemSelection _deselected = deselected; #endif Q_ASSERT(assertSelectionValid(_selected)); Q_ASSERT(assertSelectionValid(_deselected)); const QItemSelection mappedDeselection = m_indexMapper->mapSelectionRightToLeft(_deselected); const QItemSelection mappedSelection = m_indexMapper->mapSelectionRightToLeft(_selected); q->QItemSelectionModel::select(mappedDeselection, QItemSelectionModel::Deselect); q->QItemSelectionModel::select(mappedSelection, QItemSelectionModel::Select); } void KLinkItemSelectionModelPrivate::sourceCurrentChanged(const QModelIndex ¤t) { Q_Q(KLinkItemSelectionModel); const QModelIndex mappedCurrent = m_indexMapper->mapRightToLeft(current); if (!mappedCurrent.isValid()) { return; } q->setCurrentIndex(mappedCurrent, QItemSelectionModel::NoUpdate); } #include "moc_klinkitemselectionmodel.cpp" diff --git a/src/kmodelindexproxymapper.cpp b/src/kmodelindexproxymapper.cpp index 4eaecb4..49d2f9d 100644 --- a/src/kmodelindexproxymapper.cpp +++ b/src/kmodelindexproxymapper.cpp @@ -1,331 +1,331 @@ /* Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net, author Stephen Kelly Copyright (c) 2016 Ableton AG Author Stephen Kelly This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kmodelindexproxymapper.h" +#include "kitemmodels_debug.h" #include #include -#include #include #include class KModelIndexProxyMapperPrivate { KModelIndexProxyMapperPrivate(const QAbstractItemModel *leftModel, const QAbstractItemModel *rightModel, KModelIndexProxyMapper *qq) : q_ptr(qq), m_leftModel(leftModel), m_rightModel(rightModel), mConnected(false) { createProxyChain(); } void createProxyChain(); void checkConnected(); void setConnected(bool connected); bool assertSelectionValid(const QItemSelection &selection) const { Q_FOREACH (const QItemSelectionRange &range, selection) { if (!range.isValid()) { - qDebug() << selection << m_leftModel << m_rightModel << m_proxyChainDown << m_proxyChainUp; + qCDebug(KITEMMODELS_LOG) << selection << m_leftModel << m_rightModel << m_proxyChainDown << m_proxyChainUp; } Q_ASSERT(range.isValid()); } return true; } Q_DECLARE_PUBLIC(KModelIndexProxyMapper) KModelIndexProxyMapper *const q_ptr; QList > m_proxyChainUp; QList > m_proxyChainDown; QPointer m_leftModel; QPointer m_rightModel; bool mConnected; }; /* The idea here is that this selection model and proxySelectionModel might be in different parts of the proxy chain. We need to build up to two chains of proxy models to create mappings between them. Example 1: Root model | / \ Proxy 1 Proxy 3 | | Proxy 2 Proxy 4 Need Proxy 1 and Proxy 2 in one chain, and Proxy 3 and 4 in the other. Example 2: Root model | Proxy 1 | Proxy 2 / \ Proxy 3 Proxy 6 | | Proxy 4 Proxy 7 | Proxy 5 We first build the chain from 1 to 5, then start building the chain from 7 to 1. We stop when we find that proxy 2 is already in the first chain. Stephen Kelly, 30 March 2010. */ void KModelIndexProxyMapperPrivate::createProxyChain() { Q_FOREACH (auto p, m_proxyChainUp) { p->disconnect(q_ptr); } Q_FOREACH (auto p, m_proxyChainDown) { p->disconnect(q_ptr); } m_proxyChainUp.clear(); m_proxyChainDown.clear(); QPointer targetModel = m_rightModel; QList > proxyChainDown; QPointer selectionTargetProxyModel = qobject_cast(targetModel); while (selectionTargetProxyModel) { proxyChainDown.prepend(selectionTargetProxyModel); QObject::connect(selectionTargetProxyModel.data(), &QAbstractProxyModel::sourceModelChanged, q_ptr, [this]{ createProxyChain(); }); selectionTargetProxyModel = qobject_cast(selectionTargetProxyModel->sourceModel()); if (selectionTargetProxyModel == m_leftModel) { m_proxyChainDown = proxyChainDown; checkConnected(); return; } } QPointer sourceModel = m_leftModel; QPointer sourceProxyModel = qobject_cast(sourceModel); while (sourceProxyModel) { m_proxyChainUp.append(sourceProxyModel); QObject::connect(sourceProxyModel.data(), &QAbstractProxyModel::sourceModelChanged, q_ptr, [this]{ createProxyChain(); }); sourceProxyModel = qobject_cast(sourceProxyModel->sourceModel()); const int targetIndex = proxyChainDown.indexOf(sourceProxyModel); if (targetIndex != -1) { m_proxyChainDown = proxyChainDown.mid(targetIndex + 1, proxyChainDown.size()); checkConnected(); return; } } m_proxyChainDown = proxyChainDown; checkConnected(); } void KModelIndexProxyMapperPrivate::checkConnected() { auto konamiRight = m_proxyChainUp.isEmpty() ? m_leftModel : m_proxyChainUp.last()->sourceModel(); auto konamiLeft = m_proxyChainDown.isEmpty() ? m_rightModel : m_proxyChainDown.first()->sourceModel(); setConnected(konamiLeft && (konamiLeft == konamiRight)); } void KModelIndexProxyMapperPrivate::setConnected(bool connected) { if (mConnected != connected) { Q_Q(KModelIndexProxyMapper); mConnected = connected; Q_EMIT q->isConnectedChanged(); } } KModelIndexProxyMapper::KModelIndexProxyMapper(const QAbstractItemModel *leftModel, const QAbstractItemModel *rightModel, QObject *parent) : QObject(parent), d_ptr(new KModelIndexProxyMapperPrivate(leftModel, rightModel, this)) { } KModelIndexProxyMapper::~KModelIndexProxyMapper() { delete d_ptr; } QModelIndex KModelIndexProxyMapper::mapLeftToRight(const QModelIndex &index) const { const QItemSelection selection = mapSelectionLeftToRight(QItemSelection(index, index)); if (selection.isEmpty()) { return QModelIndex(); } return selection.indexes().first(); } QModelIndex KModelIndexProxyMapper::mapRightToLeft(const QModelIndex &index) const { const QItemSelection selection = mapSelectionRightToLeft(QItemSelection(index, index)); if (selection.isEmpty()) { return QModelIndex(); } return selection.indexes().first(); } // QAbstractProxyModel::mapSelectionFromSource creates invalid ranges to we filter // those out manually in a loop. Hopefully fixed in Qt 4.7.2, so we ifdef it out. // http://qt.gitorious.org/qt/qt/merge_requests/2474 // http://qt.gitorious.org/qt/qt/merge_requests/831 #if QT_VERSION < 0x040702 #define RANGE_FIX_HACK #endif #ifdef RANGE_FIX_HACK static QItemSelection removeInvalidRanges(const QItemSelection &selection) { QItemSelection result; Q_FOREACH (const QItemSelectionRange &range, selection) { if (!range.isValid()) { continue; } result << range; } return result; } #endif QItemSelection KModelIndexProxyMapper::mapSelectionLeftToRight(const QItemSelection &selection) const { Q_D(const KModelIndexProxyMapper); if (selection.isEmpty() || !d->mConnected) { return QItemSelection(); } if (selection.first().model() != d->m_leftModel) { - qDebug() << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel; + qCDebug(KITEMMODELS_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel; } Q_ASSERT(selection.first().model() == d->m_leftModel); QItemSelection seekSelection = selection; Q_ASSERT(d->assertSelectionValid(seekSelection)); QListIterator > iUp(d->m_proxyChainUp); while (iUp.hasNext()) { const QPointer proxy = iUp.next(); if (!proxy) { return QItemSelection(); } Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy); seekSelection = proxy->mapSelectionToSource(seekSelection); Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy->sourceModel()); #ifdef RANGE_FIX_HACK seekSelection = removeInvalidRanges(seekSelection); #endif Q_ASSERT(d->assertSelectionValid(seekSelection)); } QListIterator > iDown(d->m_proxyChainDown); while (iDown.hasNext()) { const QPointer proxy = iDown.next(); if (!proxy) { return QItemSelection(); } Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy->sourceModel()); seekSelection = proxy->mapSelectionFromSource(seekSelection); Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy); #ifdef RANGE_FIX_HACK seekSelection = removeInvalidRanges(seekSelection); #endif Q_ASSERT(d->assertSelectionValid(seekSelection)); } Q_ASSERT((!seekSelection.isEmpty() && seekSelection.first().model() == d->m_rightModel) || true); return seekSelection; } QItemSelection KModelIndexProxyMapper::mapSelectionRightToLeft(const QItemSelection &selection) const { Q_D(const KModelIndexProxyMapper); if (selection.isEmpty() || !d->mConnected) { return QItemSelection(); } if (selection.first().model() != d->m_rightModel) { - qDebug() << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel; + qCDebug(KITEMMODELS_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel; } Q_ASSERT(selection.first().model() == d->m_rightModel); QItemSelection seekSelection = selection; Q_ASSERT(d->assertSelectionValid(seekSelection)); QListIterator > iDown(d->m_proxyChainDown); iDown.toBack(); while (iDown.hasPrevious()) { const QPointer proxy = iDown.previous(); if (!proxy) { return QItemSelection(); } seekSelection = proxy->mapSelectionToSource(seekSelection); #ifdef RANGE_FIX_HACK seekSelection = removeInvalidRanges(seekSelection); #endif Q_ASSERT(d->assertSelectionValid(seekSelection)); } QListIterator > iUp(d->m_proxyChainUp); iUp.toBack(); while (iUp.hasPrevious()) { const QPointer proxy = iUp.previous(); if (!proxy) { return QItemSelection(); } seekSelection = proxy->mapSelectionFromSource(seekSelection); #ifdef RANGE_FIX_HACK seekSelection = removeInvalidRanges(seekSelection); #endif Q_ASSERT(d->assertSelectionValid(seekSelection)); } Q_ASSERT((!seekSelection.isEmpty() && seekSelection.first().model() == d->m_leftModel) || true); return seekSelection; } bool KModelIndexProxyMapper::isConnected() const { Q_D(const KModelIndexProxyMapper); return d->mConnected; }