From 6d7c2f1a0ce505fa6d22a36cbdb924796344bf89 Mon Sep 17 00:00:00 2001 From: Benjamin Jillich <43751992+amzn-jillich@users.noreply.github.com> Date: Tue, 18 May 2021 11:16:00 +0200 Subject: [PATCH] Reusable selection proxy model (#780) --- .../Utilities/SelectionProxyModel.cpp | 167 ++++++++++++++++++ .../Utilities/SelectionProxyModel.h | 66 +++++++ .../AzQtComponents/azqtcomponents_files.cmake | 2 + 3 files changed, 235 insertions(+) create mode 100644 Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.cpp create mode 100644 Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.h diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.cpp new file mode 100644 index 0000000000..ed7fda7fa8 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.cpp @@ -0,0 +1,167 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include + +namespace AzQtComponents +{ + SelectionProxyModel::SelectionProxyModel(QItemSelectionModel* sourceSelectionModel, QAbstractProxyModel* proxyModel, QObject* parent) + : QItemSelectionModel(proxyModel, parent) + , m_sourceSelectionModel(sourceSelectionModel) + { + connect(sourceSelectionModel, &QItemSelectionModel::selectionChanged, this, &SelectionProxyModel::OnSourceSelectionChanged); + connect(sourceSelectionModel, &QItemSelectionModel::currentChanged, this, &SelectionProxyModel::OnSourceSelectionCurrentChanged); + connect(proxyModel, &QAbstractItemModel::rowsInserted, this, &SelectionProxyModel::OnProxyModelRowsInserted); + connect(this, &QItemSelectionModel::selectionChanged, this, &SelectionProxyModel::OnProxySelectionChanged); + + // Find the chain of proxy models + QAbstractProxyModel* sourceProxyModel = proxyModel; + while (sourceProxyModel) + { + m_proxyModels.push_back(sourceProxyModel); + sourceProxyModel = qobject_cast(sourceProxyModel->sourceModel()); + } + + const QItemSelection currentSelection = mapFromSource(m_sourceSelectionModel->selection()); + QItemSelectionModel::select(currentSelection, QItemSelectionModel::ClearAndSelect); + + const QModelIndex currentModelIndex = mapFromSource(m_sourceSelectionModel->currentIndex()); + QItemSelectionModel::setCurrentIndex(currentModelIndex, QItemSelectionModel::ClearAndSelect); + } + + void SelectionProxyModel::setCurrentIndex(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) + { + const QModelIndex sourcetIndex = mapToSource(index); + m_sourceSelectionModel->setCurrentIndex(sourcetIndex, command); + } + + void SelectionProxyModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) + { + const QModelIndex sourceIndex = mapToSource(index); + m_sourceSelectionModel->select(sourceIndex, command); + } + + void SelectionProxyModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) + { + const QItemSelection sourceSelection = mapToSource(selection); + m_sourceSelectionModel->select(sourceSelection, command); + } + + void SelectionProxyModel::clear() + { + m_sourceSelectionModel->clear(); + } + + void SelectionProxyModel::reset() + { + m_sourceSelectionModel->reset(); + } + + void SelectionProxyModel::clearCurrentIndex() + { + m_sourceSelectionModel->clearCurrentIndex(); + } + + void SelectionProxyModel::OnSourceSelectionCurrentChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) + { + QModelIndex targetCurrent = mapFromSource(current); + QItemSelectionModel::setCurrentIndex(targetCurrent, QItemSelectionModel::NoUpdate); + } + + void SelectionProxyModel::OnSourceSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) + { + QItemSelection targetSelected = mapFromSource(selected); + QItemSelection targetDeselected = mapFromSource(deselected); + + QItemSelectionModel::select(targetSelected, QItemSelectionModel::Select); + QItemSelectionModel::select(targetDeselected, QItemSelectionModel::Deselect); + } + + void SelectionProxyModel::OnProxySelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) + { + const QItemSelection sourceSelected = mapToSource(selected); + const QItemSelection sourceDeselected = mapToSource(deselected); + + // Disconnect from the selectionChanged signal in the source model to prevent recursion. We could also block the signals + // of the source selection model, but someone else may be connected to its signals and expect to get an update. + disconnect(m_sourceSelectionModel, &QItemSelectionModel::selectionChanged, this, &SelectionProxyModel::OnSourceSelectionChanged); + if (selected.empty() && deselected.empty()) + { + // Force the signal to fire + emit m_sourceSelectionModel->selectionChanged({}, {}); + } + else + { + m_sourceSelectionModel->select(sourceSelected, QItemSelectionModel::Select); + m_sourceSelectionModel->select(sourceDeselected, QItemSelectionModel::Deselect); + } + connect(m_sourceSelectionModel, &QItemSelectionModel::selectionChanged, this, &SelectionProxyModel::OnSourceSelectionChanged); + } + + void SelectionProxyModel::OnProxyModelRowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) + { + QModelIndex sourceIndex = m_sourceSelectionModel->currentIndex(); + QModelIndex targetIndex = mapFromSource(sourceIndex); + if (targetIndex != currentIndex()) + { + QItemSelectionModel::setCurrentIndex(targetIndex, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + } + + QItemSelection sourceSelection = m_sourceSelectionModel->selection(); + QItemSelection targetSelection = mapFromSource(sourceSelection); + if (targetSelection != selection()) + { + QItemSelectionModel::select(targetSelection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + } + } + + QModelIndex SelectionProxyModel::mapFromSource(const QModelIndex& sourceIndex) + { + QModelIndex mappedIndex = sourceIndex; + for (QVector::const_reverse_iterator itProxy = m_proxyModels.rbegin(); itProxy != m_proxyModels.rend(); ++itProxy) + { + mappedIndex = (*itProxy)->mapFromSource(mappedIndex); + } + return mappedIndex; + } + + QItemSelection SelectionProxyModel::mapFromSource(const QItemSelection& sourceSelection) + { + QItemSelection mappedSelection = sourceSelection; + for (QVector::const_reverse_iterator itProxy = m_proxyModels.rbegin(); itProxy != m_proxyModels.rend(); ++itProxy) + { + mappedSelection = (*itProxy)->mapSelectionFromSource(mappedSelection); + } + return mappedSelection; + } + + QModelIndex SelectionProxyModel::mapToSource(const QModelIndex& targetIndex) + { + QModelIndex mappedIndex = targetIndex; + for (QVector::const_iterator itProxy = m_proxyModels.begin(); itProxy != m_proxyModels.end(); ++itProxy) + { + mappedIndex = (*itProxy)->mapToSource(mappedIndex); + } + return mappedIndex; + } + + QItemSelection SelectionProxyModel::mapToSource(const QItemSelection& targetSelection) + { + QItemSelection mappedSelection = targetSelection; + for (QVector::const_iterator itProxy = m_proxyModels.begin(); itProxy != m_proxyModels.end(); ++itProxy) + { + mappedSelection = (*itProxy)->mapSelectionToSource(mappedSelection); + } + return mappedSelection; + } +} // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.h b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.h new file mode 100644 index 0000000000..5187ed9cb0 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/SelectionProxyModel.h @@ -0,0 +1,66 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#include +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QAbstractProxyModel) + +namespace AzQtComponents +{ + //! This class is a QItemSelectionModel that syncs through proxy models and maintains + //! selection. In Qt we can have a model being filtered/sorted by proxy models. If the + //! selection model is connected to the original model, the view needs a new selection + //! model that understands the filtering. This class does that conversion. + //! @Note: this class does not support changing proxy models (anywhere in the chain). + //! The class will have to be recreated with the new proxy model. + class AZ_QT_COMPONENTS_API SelectionProxyModel + : public QItemSelectionModel + { + Q_OBJECT // AUTOMOC + + public: + SelectionProxyModel(QItemSelectionModel* sourceSelectionModel, QAbstractProxyModel* proxyModel, QObject* parent = nullptr); + + void setCurrentIndex(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) override; + void select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) override; + void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override; + void clear() override; + void reset() override; + void clearCurrentIndex() override; + + private slots: + void OnSourceSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + void OnSourceSelectionCurrentChanged(const QModelIndex& current, const QModelIndex& previous); + void OnProxySelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + void OnProxyModelRowsInserted(const QModelIndex& parent, int first, int last); + + private: + QModelIndex mapFromSource(const QModelIndex& sourceIndex); + QItemSelection mapFromSource(const QItemSelection& sourceSelection); + + QModelIndex mapToSource(const QModelIndex& targetIndex); + QItemSelection mapToSource(const QItemSelection& targetSelection); + + // Contains the chain of proxy models that leads us to the real model. The outer-most proxy model + // comes first and is followed by inner proxy models. + AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING + QVector m_proxyModels; + AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING + QItemSelectionModel* m_sourceSelectionModel; + }; +} // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake index 30929b712b..33a35e0524 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake @@ -287,6 +287,8 @@ set(FILES Utilities/ScreenUtilities.cpp Utilities/ScreenGrabber.h Utilities/ScopedCleanup.h + Utilities/SelectionProxyModel.cpp + Utilities/SelectionProxyModel.h Utilities/TextUtilities.cpp Utilities/TextUtilities.h )