| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2009 Stephen Kelly <steveire@gmail.com> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #ifndef KSELECTIONPROXYMODEL_H |
| 8 | #define KSELECTIONPROXYMODEL_H |
| 9 | |
| 10 | #include <QAbstractProxyModel> |
| 11 | #include <QItemSelectionModel> |
| 12 | |
| 13 | #include "kitemmodels_export.h" |
| 14 | |
| 15 | #include <memory> |
| 16 | |
| 17 | class KSelectionProxyModelPrivate; |
| 18 | |
| 19 | /*! |
| 20 | \class KSelectionProxyModel |
| 21 | \inmodule KItemModels |
| 22 | \brief A Proxy Model which presents a subset of its source model to observers. |
| 23 | |
| 24 | The KSelectionProxyModel is most useful as a convenience for displaying the selection in one view in |
| 25 | another view. The selectionModel of the initial view is used to create a proxied model which is filtered |
| 26 | based on the configuration of this class. |
| 27 | |
| 28 | For example, when a user clicks a mail folder in one view in an email application, the contained emails |
| 29 | should be displayed in another view. |
| 30 | |
| 31 | This takes away the need for the developer to handle the selection between the views, including all the |
| 32 | mapToSource(), mapFromSource() and setRootIndex() calls. |
| 33 | |
| 34 | \code |
| 35 | MyModel *sourceModel = new MyModel(this); |
| 36 | QTreeView *leftView = new QTreeView(this); |
| 37 | leftView->setModel(sourceModel); |
| 38 | |
| 39 | KSelectionProxyModel *selectionProxy = new KSelectionProxyModel(leftView->selectionModel(), this); |
| 40 | selectionProxy->setSourceModel(sourceModel); |
| 41 | |
| 42 | QTreeView *rightView = new QTreeView(this); |
| 43 | rightView->setModel(selectionProxy); |
| 44 | \endcode |
| 45 | |
| 46 | \image selectionproxymodelsimpleselection.png "A Selection in one view creating a model for use with another view." |
| 47 | |
| 48 | The KSelectionProxyModel can handle complex selections. |
| 49 | |
| 50 | \image selectionproxymodelmultipleselection.png "Non-contiguous selection creating a new simple model in a second view." |
| 51 | |
| 52 | The contents of the secondary view depends on the selection in the primary view, and the configuration of the proxy model. |
| 53 | See KSelectionProxyModel::setFilterBehavior for the different possible configurations. |
| 54 | |
| 55 | For example, if the filterBehavior is SubTrees, selecting another item in an already selected subtree has no effect. |
| 56 | |
| 57 | \image selectionproxymodelmultipleselection-withdescendant.png "Selecting an item and its descendant." |
| 58 | |
| 59 | See the test application in tests/proxymodeltestapp to try out the valid configurations. |
| 60 | |
| 61 | \image kselectionproxymodel-testapp.png "KSelectionProxyModel test application" |
| 62 | |
| 63 | Obviously, the KSelectionProxyModel may be used in a view, or further processed with other proxy models. |
| 64 | See KAddressBook and AkonadiConsole in kdepim for examples which use a further KDescendantsProxyModel |
| 65 | and QSortFilterProxyModel on top of a KSelectionProxyModel. |
| 66 | |
| 67 | Additionally, this class can be used to programmatically choose some items from the source model to display in the view. For example, |
| 68 | this is how the Favourite Folder View in KMail works, and is also used in unit testing. |
| 69 | |
| 70 | See also: \l {https://doc.qt.io/qt-6/model-view-programming.html#proxy-models}{Proxy Models in the Qt Documentation} |
| 71 | |
| 72 | \since 4.4 |
| 73 | |
| 74 | */ |
| 75 | class KITEMMODELS_EXPORT KSelectionProxyModel : public QAbstractProxyModel |
| 76 | { |
| 77 | Q_OBJECT |
| 78 | |
| 79 | /*! |
| 80 | * \property KSelectionProxyModel::filterBehavior |
| 81 | */ |
| 82 | Q_PROPERTY(FilterBehavior filterBehavior READ filterBehavior WRITE setFilterBehavior NOTIFY filterBehaviorChanged) |
| 83 | |
| 84 | /*! |
| 85 | * \property KSelectionProxyModel::selectionModel |
| 86 | */ |
| 87 | Q_PROPERTY(QItemSelectionModel *selectionModel READ selectionModel WRITE setSelectionModel NOTIFY selectionModelChanged) |
| 88 | public: |
| 89 | // KF6: Remove the selectionModel from the constructor here. |
| 90 | /*! |
| 91 | Constructor. |
| 92 | |
| 93 | \a selectionModel The selection model used to filter what is presented by the proxy. |
| 94 | */ |
| 95 | explicit KSelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent = nullptr); |
| 96 | |
| 97 | // KF6: Remove in favor of the constructor above. |
| 98 | /*! |
| 99 | Default constructor. Allow the creation of a KSelectionProxyModel in QML |
| 100 | code. QML will assign a parent after construction. |
| 101 | */ |
| 102 | explicit KSelectionProxyModel(); |
| 103 | |
| 104 | ~KSelectionProxyModel() override; |
| 105 | |
| 106 | void setSourceModel(QAbstractItemModel *sourceModel) override; |
| 107 | |
| 108 | QItemSelectionModel *selectionModel() const; |
| 109 | void setSelectionModel(QItemSelectionModel *selectionModel); |
| 110 | |
| 111 | enum FilterBehavior { |
| 112 | SubTrees, |
| 113 | SubTreeRoots, |
| 114 | SubTreesWithoutRoots, |
| 115 | ExactSelection, |
| 116 | ChildrenOfExactSelection, |
| 117 | InvalidBehavior, |
| 118 | }; |
| 119 | Q_ENUM(FilterBehavior) |
| 120 | |
| 121 | /*! |
| 122 | Set the filter behaviors of this model. |
| 123 | The filter behaviors of the model govern the content of the model based on the selection of the contained QItemSelectionModel. |
| 124 | |
| 125 | See kdeui/proxymodeltestapp to try out the different proxy model behaviors. |
| 126 | |
| 127 | The most useful behaviors are SubTrees, ExactSelection and ChildrenOfExactSelection. |
| 128 | |
| 129 | The default behavior is SubTrees. This means that this proxy model will contain the roots of the items in the source model. |
| 130 | Any descendants which are also selected have no additional effect. |
| 131 | For example if the source model is like: |
| 132 | |
| 133 | \code |
| 134 | (root) |
| 135 | - A |
| 136 | - B |
| 137 | - C |
| 138 | - D |
| 139 | - E |
| 140 | - F |
| 141 | - G |
| 142 | - H |
| 143 | - I |
| 144 | - J |
| 145 | - K |
| 146 | - L |
| 147 | \endcode |
| 148 | |
| 149 | And A, B, C and D are selected, the proxy will contain: |
| 150 | |
| 151 | \code |
| 152 | (root) |
| 153 | - A |
| 154 | - B |
| 155 | - C |
| 156 | - D |
| 157 | - E |
| 158 | - F |
| 159 | - G |
| 160 | \endcode |
| 161 | |
| 162 | That is, selecting 'D' or 'C' if 'B' is also selected has no effect. If 'B' is de-selected, then 'C' amd 'D' become top-level items: |
| 163 | |
| 164 | \code |
| 165 | (root) |
| 166 | - A |
| 167 | - C |
| 168 | - D |
| 169 | - E |
| 170 | - F |
| 171 | - G |
| 172 | \endcode |
| 173 | |
| 174 | This is the behavior used by KJots when rendering books. |
| 175 | |
| 176 | If the behavior is set to SubTreeRoots, then the children of selected indexes are not part of the model. If 'A', 'B' and 'D' are selected, |
| 177 | |
| 178 | \code |
| 179 | (root) |
| 180 | - A |
| 181 | - B |
| 182 | \endcode |
| 183 | |
| 184 | Note that although 'D' is selected, it is not part of the proxy model, because its parent 'B' is already selected. |
| 185 | |
| 186 | SubTreesWithoutRoots has the effect of not making the selected items part of the model, but making their children part of the model instead. If 'A', 'B' |
| 187 | and 'I' are selected: |
| 188 | |
| 189 | \code |
| 190 | (root) |
| 191 | - C |
| 192 | - D |
| 193 | - E |
| 194 | - F |
| 195 | - G |
| 196 | - J |
| 197 | - K |
| 198 | - L |
| 199 | \endcode |
| 200 | |
| 201 | Note that 'A' has no children, so selecting it has no outward effect on the model. |
| 202 | |
| 203 | ChildrenOfExactSelection causes the proxy model to contain the children of the selected indexes,but further descendants are omitted. |
| 204 | Additionally, if descendants of an already selected index are selected, their children are part of the proxy model. |
| 205 | For example, if 'A', 'B', 'D' and 'I' are selected: |
| 206 | |
| 207 | \code |
| 208 | (root) |
| 209 | - C |
| 210 | - D |
| 211 | - E |
| 212 | - G |
| 213 | - J |
| 214 | - K |
| 215 | - L |
| 216 | \endcode |
| 217 | |
| 218 | This would be useful for example if showing containers (for example maildirs) in one view and their items in another. Sub-maildirs would still appear in |
| 219 | the proxy, but could be filtered out using a QSortfilterProxyModel. |
| 220 | |
| 221 | The ExactSelection behavior causes the selected items to be part of the proxy model, even if their ancestors are already selected, but children of |
| 222 | selected items are not included. |
| 223 | |
| 224 | Again, if 'A', 'B', 'D' and 'I' are selected: |
| 225 | |
| 226 | \code |
| 227 | (root) |
| 228 | - A |
| 229 | - B |
| 230 | - D |
| 231 | - I |
| 232 | \endcode |
| 233 | |
| 234 | This is the behavior used by the Favourite Folder View in KMail. |
| 235 | |
| 236 | */ |
| 237 | void setFilterBehavior(FilterBehavior behavior); |
| 238 | FilterBehavior filterBehavior() const; |
| 239 | |
| 240 | QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; |
| 241 | QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; |
| 242 | |
| 243 | QItemSelection mapSelectionFromSource(const QItemSelection &selection) const override; |
| 244 | QItemSelection mapSelectionToSource(const QItemSelection &selection) const override; |
| 245 | |
| 246 | Qt::ItemFlags flags(const QModelIndex &index) const override; |
| 247 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; |
| 248 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; |
| 249 | QVariant (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; |
| 250 | |
| 251 | QMimeData *mimeData(const QModelIndexList &indexes) const override; |
| 252 | QStringList mimeTypes() const override; |
| 253 | Qt::DropActions supportedDropActions() const override; |
| 254 | bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; |
| 255 | |
| 256 | bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; |
| 257 | QModelIndex index(int, int, const QModelIndex & = QModelIndex()) const override; |
| 258 | QModelIndex parent(const QModelIndex &) const override; |
| 259 | int columnCount(const QModelIndex & = QModelIndex()) const override; |
| 260 | |
| 261 | virtual QModelIndexList match(const QModelIndex &start, |
| 262 | int role, |
| 263 | const QVariant &value, |
| 264 | int hits = 1, |
| 265 | Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; |
| 266 | |
| 267 | Q_SIGNALS: |
| 268 | /*! |
| 269 | \internal |
| 270 | Emitted before \a removeRootIndex, an index in the sourceModel is removed from |
| 271 | the root selected indexes. This may be unrelated to rows removed from the model, |
| 272 | depending on configuration. |
| 273 | */ |
| 274 | void rootIndexAboutToBeRemoved(const QModelIndex &removeRootIndex, QPrivateSignal); |
| 275 | |
| 276 | /*! |
| 277 | \internal |
| 278 | Emitted when \a newIndex, an index in the sourceModel is added to the root selected |
| 279 | indexes. This may be unrelated to rows inserted to the model, |
| 280 | depending on configuration. |
| 281 | */ |
| 282 | void rootIndexAdded(const QModelIndex &newIndex, QPrivateSignal); |
| 283 | |
| 284 | /*! |
| 285 | \internal |
| 286 | Emitted before \a selection, a selection in the sourceModel, is removed from |
| 287 | the root selection. |
| 288 | */ |
| 289 | void rootSelectionAboutToBeRemoved(const QItemSelection &selection, QPrivateSignal); |
| 290 | |
| 291 | /*! |
| 292 | \internal |
| 293 | Emitted after \a selection, a selection in the sourceModel, is added to |
| 294 | the root selection. |
| 295 | */ |
| 296 | void rootSelectionAdded(const QItemSelection &selection, QPrivateSignal); |
| 297 | |
| 298 | void selectionModelChanged(QPrivateSignal); |
| 299 | void filterBehaviorChanged(QPrivateSignal); |
| 300 | |
| 301 | protected: |
| 302 | QList<QPersistentModelIndex> sourceRootIndexes() const; |
| 303 | |
| 304 | private: |
| 305 | Q_DECLARE_PRIVATE(KSelectionProxyModel) |
| 306 | std::unique_ptr<KSelectionProxyModelPrivate> const d_ptr; |
| 307 | |
| 308 | Q_PRIVATE_SLOT(d_func(), void sourceRowsAboutToBeInserted(const QModelIndex &, int, int)) |
| 309 | Q_PRIVATE_SLOT(d_func(), void sourceRowsInserted(const QModelIndex &, int, int)) |
| 310 | Q_PRIVATE_SLOT(d_func(), void sourceRowsAboutToBeRemoved(const QModelIndex &, int, int)) |
| 311 | Q_PRIVATE_SLOT(d_func(), void sourceRowsRemoved(const QModelIndex &, int, int)) |
| 312 | Q_PRIVATE_SLOT(d_func(), void sourceRowsAboutToBeMoved(const QModelIndex &, int, int, const QModelIndex &, int)) |
| 313 | Q_PRIVATE_SLOT(d_func(), void sourceRowsMoved(const QModelIndex &, int, int, const QModelIndex &, int)) |
| 314 | Q_PRIVATE_SLOT(d_func(), void sourceModelAboutToBeReset()) |
| 315 | Q_PRIVATE_SLOT(d_func(), void sourceModelReset()) |
| 316 | Q_PRIVATE_SLOT(d_func(), void sourceLayoutAboutToBeChanged()) |
| 317 | Q_PRIVATE_SLOT(d_func(), void sourceLayoutChanged()) |
| 318 | Q_PRIVATE_SLOT(d_func(), void sourceDataChanged(const QModelIndex &, const QModelIndex &)) |
| 319 | Q_PRIVATE_SLOT(d_func(), void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)) |
| 320 | Q_PRIVATE_SLOT(d_func(), void sourceModelDestroyed()) |
| 321 | }; |
| 322 | |
| 323 | #endif |
| 324 | |