1 /*
2     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
3     SPDX-FileContributor: Stephen Kelly <stephen@kdab.com>
4     SPDX-FileCopyrightText: 2016 Ableton AG <info@ableton.com>
5     SPDX-FileContributor: Stephen Kelly <stephen.kelly@ableton.com>
6 
7     SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "kmodelindexproxymapper.h"
11 #include "kitemmodels_debug.h"
12 
13 #include <QAbstractItemModel>
14 #include <QAbstractProxyModel>
15 #include <QItemSelectionModel>
16 #include <QPointer>
17 
18 class KModelIndexProxyMapperPrivate
19 {
KModelIndexProxyMapperPrivate(const QAbstractItemModel * leftModel,const QAbstractItemModel * rightModel,KModelIndexProxyMapper * qq)20     KModelIndexProxyMapperPrivate(const QAbstractItemModel *leftModel, const QAbstractItemModel *rightModel, KModelIndexProxyMapper *qq)
21         : q_ptr(qq)
22         , m_leftModel(leftModel)
23         , m_rightModel(rightModel)
24         , mConnected(false)
25     {
26         createProxyChain();
27     }
28 
29     void createProxyChain();
30     void checkConnected();
31     void setConnected(bool connected);
32 
assertSelectionValid(const QItemSelection & selection) const33     bool assertSelectionValid(const QItemSelection &selection) const
34     {
35         for (const QItemSelectionRange &range : selection) {
36             if (!range.isValid()) {
37                 qCDebug(KITEMMODELS_LOG) << selection << m_leftModel << m_rightModel << m_proxyChainDown << m_proxyChainUp;
38             }
39             Q_ASSERT(range.isValid());
40         }
41         return true;
42     }
43 
44     Q_DECLARE_PUBLIC(KModelIndexProxyMapper)
45     KModelIndexProxyMapper *const q_ptr;
46 
47     QList<QPointer<const QAbstractProxyModel>> m_proxyChainUp;
48     QList<QPointer<const QAbstractProxyModel>> m_proxyChainDown;
49 
50     QPointer<const QAbstractItemModel> m_leftModel;
51     QPointer<const QAbstractItemModel> m_rightModel;
52 
53     bool mConnected;
54 };
55 
56 /*
57 
58   The idea here is that <tt>this</tt> selection model and proxySelectionModel might be in different parts of the
59   proxy chain. We need to build up to two chains of proxy models to create mappings between them.
60 
61   Example 1:
62 
63      Root model
64           |
65         /    \
66     Proxy 1   Proxy 3
67        |       |
68     Proxy 2   Proxy 4
69 
70   Need Proxy 1 and Proxy 2 in one chain, and Proxy 3 and 4 in the other.
71 
72   Example 2:
73 
74      Root model
75           |
76         Proxy 1
77           |
78         Proxy 2
79         /     \
80     Proxy 3   Proxy 6
81        |       |
82     Proxy 4   Proxy 7
83        |
84     Proxy 5
85 
86   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
87   already in the first chain.
88 
89   Stephen Kelly, 30 March 2010.
90 */
91 
createProxyChain()92 void KModelIndexProxyMapperPrivate::createProxyChain()
93 {
94     for (auto p : std::as_const(m_proxyChainUp)) {
95         p->disconnect(q_ptr);
96     }
97     for (auto p : std::as_const(m_proxyChainDown)) {
98         p->disconnect(q_ptr);
99     }
100     m_proxyChainUp.clear();
101     m_proxyChainDown.clear();
102     QPointer<const QAbstractItemModel> targetModel = m_rightModel;
103 
104     QList<QPointer<const QAbstractProxyModel>> proxyChainDown;
105     QPointer<const QAbstractProxyModel> selectionTargetProxyModel = qobject_cast<const QAbstractProxyModel *>(targetModel);
106     while (selectionTargetProxyModel) {
107         proxyChainDown.prepend(selectionTargetProxyModel);
108         QObject::connect(selectionTargetProxyModel.data(), &QAbstractProxyModel::sourceModelChanged, q_ptr, [this] {
109             createProxyChain();
110         });
111 
112         selectionTargetProxyModel = qobject_cast<const QAbstractProxyModel *>(selectionTargetProxyModel->sourceModel());
113 
114         if (selectionTargetProxyModel == m_leftModel) {
115             m_proxyChainDown = proxyChainDown;
116             checkConnected();
117             return;
118         }
119     }
120 
121     QPointer<const QAbstractItemModel> sourceModel = m_leftModel;
122     QPointer<const QAbstractProxyModel> sourceProxyModel = qobject_cast<const QAbstractProxyModel *>(sourceModel);
123 
124     while (sourceProxyModel) {
125         m_proxyChainUp.append(sourceProxyModel);
126         QObject::connect(sourceProxyModel.data(), &QAbstractProxyModel::sourceModelChanged, q_ptr, [this] {
127             createProxyChain();
128         });
129 
130         sourceProxyModel = qobject_cast<const QAbstractProxyModel *>(sourceProxyModel->sourceModel());
131 
132         const int targetIndex = proxyChainDown.indexOf(sourceProxyModel);
133 
134         if (targetIndex != -1) {
135             m_proxyChainDown = proxyChainDown.mid(targetIndex + 1, proxyChainDown.size());
136             checkConnected();
137             return;
138         }
139     }
140     m_proxyChainDown = proxyChainDown;
141     checkConnected();
142 }
143 
checkConnected()144 void KModelIndexProxyMapperPrivate::checkConnected()
145 {
146     auto konamiRight = m_proxyChainUp.isEmpty() ? m_leftModel : m_proxyChainUp.last()->sourceModel();
147     auto konamiLeft = m_proxyChainDown.isEmpty() ? m_rightModel : m_proxyChainDown.first()->sourceModel();
148     setConnected(konamiLeft && (konamiLeft == konamiRight));
149 }
150 
setConnected(bool connected)151 void KModelIndexProxyMapperPrivate::setConnected(bool connected)
152 {
153     if (mConnected != connected) {
154         Q_Q(KModelIndexProxyMapper);
155         mConnected = connected;
156         Q_EMIT q->isConnectedChanged();
157     }
158 }
159 
KModelIndexProxyMapper(const QAbstractItemModel * leftModel,const QAbstractItemModel * rightModel,QObject * parent)160 KModelIndexProxyMapper::KModelIndexProxyMapper(const QAbstractItemModel *leftModel, const QAbstractItemModel *rightModel, QObject *parent)
161     : QObject(parent)
162     , d_ptr(new KModelIndexProxyMapperPrivate(leftModel, rightModel, this))
163 {
164 }
165 
166 KModelIndexProxyMapper::~KModelIndexProxyMapper() = default;
167 
mapLeftToRight(const QModelIndex & index) const168 QModelIndex KModelIndexProxyMapper::mapLeftToRight(const QModelIndex &index) const
169 {
170     const QItemSelection selection = mapSelectionLeftToRight(QItemSelection(index, index));
171     if (selection.isEmpty()) {
172         return QModelIndex();
173     }
174 
175     return selection.indexes().first();
176 }
177 
mapRightToLeft(const QModelIndex & index) const178 QModelIndex KModelIndexProxyMapper::mapRightToLeft(const QModelIndex &index) const
179 {
180     const QItemSelection selection = mapSelectionRightToLeft(QItemSelection(index, index));
181     if (selection.isEmpty()) {
182         return QModelIndex();
183     }
184 
185     return selection.indexes().first();
186 }
187 
mapSelectionLeftToRight(const QItemSelection & selection) const188 QItemSelection KModelIndexProxyMapper::mapSelectionLeftToRight(const QItemSelection &selection) const
189 {
190     Q_D(const KModelIndexProxyMapper);
191 
192     if (selection.isEmpty() || !d->mConnected) {
193         return QItemSelection();
194     }
195 
196     if (selection.first().model() != d->m_leftModel) {
197         qCDebug(KITEMMODELS_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel;
198     }
199     Q_ASSERT(selection.first().model() == d->m_leftModel);
200 
201     QItemSelection seekSelection = selection;
202     Q_ASSERT(d->assertSelectionValid(seekSelection));
203     QListIterator<QPointer<const QAbstractProxyModel>> iUp(d->m_proxyChainUp);
204 
205     while (iUp.hasNext()) {
206         const QPointer<const QAbstractProxyModel> proxy = iUp.next();
207         if (!proxy) {
208             return QItemSelection();
209         }
210 
211         Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy);
212         seekSelection = proxy->mapSelectionToSource(seekSelection);
213         Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy->sourceModel());
214 
215         Q_ASSERT(d->assertSelectionValid(seekSelection));
216     }
217 
218     QListIterator<QPointer<const QAbstractProxyModel>> iDown(d->m_proxyChainDown);
219 
220     while (iDown.hasNext()) {
221         const QPointer<const QAbstractProxyModel> proxy = iDown.next();
222         if (!proxy) {
223             return QItemSelection();
224         }
225         Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy->sourceModel());
226         seekSelection = proxy->mapSelectionFromSource(seekSelection);
227         Q_ASSERT(seekSelection.isEmpty() || seekSelection.first().model() == proxy);
228 
229         Q_ASSERT(d->assertSelectionValid(seekSelection));
230     }
231 
232     Q_ASSERT((!seekSelection.isEmpty() && seekSelection.first().model() == d->m_rightModel) || true);
233     return seekSelection;
234 }
235 
mapSelectionRightToLeft(const QItemSelection & selection) const236 QItemSelection KModelIndexProxyMapper::mapSelectionRightToLeft(const QItemSelection &selection) const
237 {
238     Q_D(const KModelIndexProxyMapper);
239 
240     if (selection.isEmpty() || !d->mConnected) {
241         return QItemSelection();
242     }
243 
244     if (selection.first().model() != d->m_rightModel) {
245         qCDebug(KITEMMODELS_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel;
246     }
247     Q_ASSERT(selection.first().model() == d->m_rightModel);
248 
249     QItemSelection seekSelection = selection;
250     Q_ASSERT(d->assertSelectionValid(seekSelection));
251     QListIterator<QPointer<const QAbstractProxyModel>> iDown(d->m_proxyChainDown);
252 
253     iDown.toBack();
254     while (iDown.hasPrevious()) {
255         const QPointer<const QAbstractProxyModel> proxy = iDown.previous();
256         if (!proxy) {
257             return QItemSelection();
258         }
259         seekSelection = proxy->mapSelectionToSource(seekSelection);
260 
261         Q_ASSERT(d->assertSelectionValid(seekSelection));
262     }
263 
264     QListIterator<QPointer<const QAbstractProxyModel>> iUp(d->m_proxyChainUp);
265 
266     iUp.toBack();
267     while (iUp.hasPrevious()) {
268         const QPointer<const QAbstractProxyModel> proxy = iUp.previous();
269         if (!proxy) {
270             return QItemSelection();
271         }
272         seekSelection = proxy->mapSelectionFromSource(seekSelection);
273 
274         Q_ASSERT(d->assertSelectionValid(seekSelection));
275     }
276 
277     Q_ASSERT((!seekSelection.isEmpty() && seekSelection.first().model() == d->m_leftModel) || true);
278     return seekSelection;
279 }
280 
isConnected() const281 bool KModelIndexProxyMapper::isConnected() const
282 {
283     Q_D(const KModelIndexProxyMapper);
284     return d->mConnected;
285 }
286