1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2010-03-21
7  * Description : A model to hold GPS information about items.
8  *
9  * Copyright (C) 2010-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  * Copyright (C) 2010      by Michael G. Hansen <mike at mghansen dot de>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "gpsitemsortproxymodel.h"
26 
27 // Qt includes
28 
29 #include <QPointer>
30 
31 // Local includes
32 
33 #include "digikam_debug.h"
34 
35 namespace Digikam
36 {
37 
38 class Q_DECL_HIDDEN GPSItemSortProxyModel::Private
39 {
40 public:
41 
Private()42     explicit Private()
43       : imageModel            (nullptr),
44         sourceSelectionModel  (nullptr),
45         linkItemSelectionModel(nullptr)
46     {
47     }
48 
49     GPSItemModel*              imageModel;
50     QItemSelectionModel*       sourceSelectionModel;
51     GPSLinkItemSelectionModel* linkItemSelectionModel;
52 };
53 
GPSItemSortProxyModel(GPSItemModel * const imageModel,QItemSelectionModel * const sourceSelectionModel)54 GPSItemSortProxyModel::GPSItemSortProxyModel(GPSItemModel* const imageModel,
55                                              QItemSelectionModel* const sourceSelectionModel)
56     : QSortFilterProxyModel(imageModel),
57       d                    (new Private())
58 {
59     d->imageModel             = imageModel;
60     d->sourceSelectionModel   = sourceSelectionModel;
61     setSourceModel(imageModel);
62     d->linkItemSelectionModel = new GPSLinkItemSelectionModel(this, d->sourceSelectionModel);
63 }
64 
~GPSItemSortProxyModel()65 GPSItemSortProxyModel::~GPSItemSortProxyModel()
66 {
67     delete d;
68 }
69 
lessThan(const QModelIndex & left,const QModelIndex & right) const70 bool GPSItemSortProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
71 {
72     if ((!left.isValid())||(!right.isValid()))
73     {
74 //      qCDebug(DIGIKAM_GENERAL_LOG) << "INVALID INDICES" << left << right;
75         return false;
76     }
77 
78     const int column                        = left.column();
79     const GPSItemContainer* const itemLeft  = d->imageModel->itemFromIndex(left);
80     const GPSItemContainer* const itemRight = d->imageModel->itemFromIndex(right);
81 
82 //  qCDebug(DIGIKAM_GENERAL_LOG) << itemLeft << itemRight << column << rowCount() << d->imageModel->rowCount();
83 
84     return itemLeft->lessThan(itemRight, column);
85 }
86 
mappedSelectionModel() const87 QItemSelectionModel* GPSItemSortProxyModel::mappedSelectionModel() const
88 {
89     return d->linkItemSelectionModel;
90 }
91 
92 // --------------------------------------------------------------------------------------
93 
94 class Q_DECL_HIDDEN GPSLinkItemSelectionModelPrivate
95 {
96 public:
97 
GPSLinkItemSelectionModelPrivate(GPSLinkItemSelectionModel * const proxySelectionModel)98     explicit GPSLinkItemSelectionModelPrivate(GPSLinkItemSelectionModel* const proxySelectionModel)
99         : q_ptr(proxySelectionModel),
100           m_linkedItemSelectionModel(nullptr),
101           m_ignoreCurrentChanged(false),
102           m_indexMapper(nullptr)
103     {
104         QObject::connect(q_ptr, &QItemSelectionModel::currentChanged, q_ptr,
105                          [this](const QModelIndex& idx) { slotCurrentChanged(idx); } );
106 
107         QObject::connect(q_ptr, &QItemSelectionModel::modelChanged, q_ptr,
108                          [this]{ reinitializeIndexMapper(); } );
109     }
110 
111 public:
112 
113     Q_DECLARE_PUBLIC(GPSLinkItemSelectionModel)
114     GPSLinkItemSelectionModel* const q_ptr;
115 
116 public:
117 
assertSelectionValid(const QItemSelection & selection) const118     bool assertSelectionValid(const QItemSelection& selection) const
119     {
120         foreach (const QItemSelectionRange& range, selection)
121         {
122             if (!range.isValid())
123             {
124                 qCDebug(DIGIKAM_GENERAL_LOG) << selection;
125             }
126 
127             Q_ASSERT(range.isValid());
128         }
129 
130         return true;
131     }
132 
reinitializeIndexMapper()133     void reinitializeIndexMapper()
134     {
135         delete m_indexMapper;
136         m_indexMapper = nullptr;
137 
138         if (!q_ptr->model() || !m_linkedItemSelectionModel || !m_linkedItemSelectionModel->model())
139         {
140             return;
141         }
142 
143         m_indexMapper                        = new GPSModelIndexProxyMapper(q_ptr->model(), m_linkedItemSelectionModel->model(), q_ptr);
144         const QItemSelection mappedSelection = m_indexMapper->mapSelectionRightToLeft(m_linkedItemSelectionModel->selection());
145         q_ptr->QItemSelectionModel::select(mappedSelection, QItemSelectionModel::ClearAndSelect);
146     }
147 
148     void sourceSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
149     void sourceCurrentChanged(const QModelIndex& current);
150     void slotCurrentChanged(const QModelIndex& current);
151 
152 public:
153 
154     QItemSelectionModel*      m_linkedItemSelectionModel;
155     bool                      m_ignoreCurrentChanged;
156     GPSModelIndexProxyMapper* m_indexMapper;
157 };
158 
GPSLinkItemSelectionModel(QAbstractItemModel * const model,QItemSelectionModel * const proxySelector,QObject * const parent)159 GPSLinkItemSelectionModel::GPSLinkItemSelectionModel(QAbstractItemModel* const model,
160                                                      QItemSelectionModel* const proxySelector,
161                                                      QObject* const parent)
162     : QItemSelectionModel(model, parent),
163       d_ptr(new GPSLinkItemSelectionModelPrivate(this))
164 {
165     setLinkedItemSelectionModel(proxySelector);
166 }
167 
GPSLinkItemSelectionModel(QObject * const parent)168 GPSLinkItemSelectionModel::GPSLinkItemSelectionModel(QObject* const parent)
169     : QItemSelectionModel(nullptr, parent),
170       d_ptr(new GPSLinkItemSelectionModelPrivate(this))
171 {
172 }
173 
~GPSLinkItemSelectionModel()174 GPSLinkItemSelectionModel::~GPSLinkItemSelectionModel()
175 {
176     delete d_ptr;
177 }
178 
linkedItemSelectionModel() const179 QItemSelectionModel* GPSLinkItemSelectionModel::linkedItemSelectionModel() const
180 {
181     Q_D(const GPSLinkItemSelectionModel);
182 
183     return d->m_linkedItemSelectionModel;
184 }
185 
setLinkedItemSelectionModel(QItemSelectionModel * const selectionModel)186 void GPSLinkItemSelectionModel::setLinkedItemSelectionModel(QItemSelectionModel* const selectionModel)
187 {
188     Q_D(GPSLinkItemSelectionModel);
189 
190     if (d->m_linkedItemSelectionModel != selectionModel)
191     {
192         if (d->m_linkedItemSelectionModel)
193         {
194             disconnect(d->m_linkedItemSelectionModel);
195         }
196 
197         d->m_linkedItemSelectionModel = selectionModel;
198 
199         if (d->m_linkedItemSelectionModel)
200         {
201             connect(d->m_linkedItemSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
202                     this, SLOT(sourceSelectionChanged(QItemSelection,QItemSelection)));
203 
204             connect(d->m_linkedItemSelectionModel, SIGNAL(currentChanged(QModelIndex,QModelIndex)),
205                     this, SLOT(sourceCurrentChanged(QModelIndex)));
206 
207             connect(d->m_linkedItemSelectionModel, &QItemSelectionModel::modelChanged,
208                     this, [this] { d_ptr->reinitializeIndexMapper(); });
209         }
210 
211         d->reinitializeIndexMapper();
212         emit linkedItemSelectionModelChanged();
213     }
214 }
215 
select(const QModelIndex & index,QItemSelectionModel::SelectionFlags command)216 void GPSLinkItemSelectionModel::select(const QModelIndex& index, QItemSelectionModel::SelectionFlags command)
217 {
218     Q_D(GPSLinkItemSelectionModel);
219 
220     // When an item is removed, the current index is set to the top index in the model.
221     // That causes a selectionChanged signal with a selection which we do not want.
222 
223     if (d->m_ignoreCurrentChanged)
224     {
225         return;
226     }
227 
228     // Do *not* replace next line with: QItemSelectionModel::select(index, command)
229     //
230     // Doing so would end up calling GPSLinkItemSelectionModel::select(QItemSelection, QItemSelectionModel::SelectionFlags)
231     //
232     // This is because the code for QItemSelectionModel::select(QModelIndex, QItemSelectionModel::SelectionFlags) looks like this:
233     // {
234     //     QItemSelection selection(index, index);
235     //     select(selection, command);
236     // }
237     // So it calls GPSLinkItemSelectionModel overload of
238     // select(QItemSelection, QItemSelectionModel::SelectionFlags)
239     //
240     // When this happens and the selection flags include Toggle, it causes the
241     // selection to be toggled twice.
242 
243     QItemSelectionModel::select(QItemSelection(index, index), command);
244 
245     if (index.isValid())
246     {
247         d->m_linkedItemSelectionModel->select(d->m_indexMapper->mapSelectionLeftToRight(QItemSelection(index, index)), command);
248     }
249     else
250     {
251         d->m_linkedItemSelectionModel->clearSelection();
252     }
253 }
254 
select(const QItemSelection & selection,QItemSelectionModel::SelectionFlags command)255 void GPSLinkItemSelectionModel::select(const QItemSelection& selection, QItemSelectionModel::SelectionFlags command)
256 {
257     Q_D(GPSLinkItemSelectionModel);
258     d->m_ignoreCurrentChanged      = true;
259     QItemSelection _selection      = selection;
260     QItemSelectionModel::select(_selection, command);
261     Q_ASSERT(d->assertSelectionValid(_selection));
262     QItemSelection mappedSelection = d->m_indexMapper->mapSelectionLeftToRight(_selection);
263     Q_ASSERT(d->assertSelectionValid(mappedSelection));
264     d->m_linkedItemSelectionModel->select(mappedSelection, command);
265     d->m_ignoreCurrentChanged      = false;
266 }
267 
slotCurrentChanged(const QModelIndex & current)268 void GPSLinkItemSelectionModelPrivate::slotCurrentChanged(const QModelIndex& current)
269 {
270     const QModelIndex mappedCurrent = m_indexMapper->mapLeftToRight(current);
271 
272     if (!mappedCurrent.isValid())
273     {
274         return;
275     }
276 
277     m_linkedItemSelectionModel->setCurrentIndex(mappedCurrent, QItemSelectionModel::NoUpdate);
278 }
279 
sourceSelectionChanged(const QItemSelection & selected,const QItemSelection & deselected)280 void GPSLinkItemSelectionModelPrivate::sourceSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
281 {
282     Q_Q(GPSLinkItemSelectionModel);
283     QItemSelection _selected               = selected;
284     QItemSelection _deselected             = deselected;
285     Q_ASSERT(assertSelectionValid(_selected));
286     Q_ASSERT(assertSelectionValid(_deselected));
287     const QItemSelection mappedDeselection = m_indexMapper->mapSelectionRightToLeft(_deselected);
288     const QItemSelection mappedSelection   = m_indexMapper->mapSelectionRightToLeft(_selected);
289 
290     q->QItemSelectionModel::select(mappedDeselection, QItemSelectionModel::Deselect);
291     q->QItemSelectionModel::select(mappedSelection, QItemSelectionModel::Select);
292 }
293 
sourceCurrentChanged(const QModelIndex & current)294 void GPSLinkItemSelectionModelPrivate::sourceCurrentChanged(const QModelIndex& current)
295 {
296     Q_Q(GPSLinkItemSelectionModel);
297     const QModelIndex mappedCurrent = m_indexMapper->mapRightToLeft(current);
298 
299     if (!mappedCurrent.isValid())
300     {
301         return;
302     }
303 
304     q->setCurrentIndex(mappedCurrent, QItemSelectionModel::NoUpdate);
305 }
306 
307 // --------------------------------------------------------------------------------------
308 
309 class Q_DECL_HIDDEN GPSModelIndexProxyMapperPrivate
310 {
311 public:
312 
GPSModelIndexProxyMapperPrivate(const QAbstractItemModel * const leftModel,const QAbstractItemModel * const rightModel,GPSModelIndexProxyMapper * const qq)313     explicit GPSModelIndexProxyMapperPrivate(const QAbstractItemModel* const leftModel,
314                                              const QAbstractItemModel* const rightModel,
315                                              GPSModelIndexProxyMapper* const qq)
316         : q_ptr(qq),
317           m_leftModel(leftModel),
318           m_rightModel(rightModel),
319           mConnected(false)
320     {
321         createProxyChain();
322     }
323 
324     void createProxyChain();
325     void checkConnected();
326     void setConnected(bool connected);
327 
328     // cppcheck-suppress unusedPrivateFunction
assertSelectionValid(const QItemSelection & selection) const329     bool assertSelectionValid(const QItemSelection& selection) const
330     {
331         foreach (const QItemSelectionRange& range, selection)
332         {
333             if (!range.isValid())
334             {
335                 qCDebug(DIGIKAM_GENERAL_LOG) << selection << m_leftModel << m_rightModel << m_proxyChainDown << m_proxyChainUp;
336             }
337 
338             Q_ASSERT(range.isValid());
339         }
340 
341         return true;
342     }
343 
344 public:
345 
346     Q_DECLARE_PUBLIC(GPSModelIndexProxyMapper)
347     GPSModelIndexProxyMapper* const             q_ptr;
348 
349     QList<QPointer<const QAbstractProxyModel> > m_proxyChainUp;
350     QList<QPointer<const QAbstractProxyModel> > m_proxyChainDown;
351 
352     QPointer<const QAbstractItemModel>          m_leftModel;
353     QPointer<const QAbstractItemModel>          m_rightModel;
354 
355     bool                                        mConnected;
356 };
357 
358 /**
359  * The idea here is that <tt>this</tt> selection model and proxySelectionModel might be in different parts of the
360  * proxy chain. We need to build up to two chains of proxy models to create mappings between them.
361  *
362  * 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
363  * already in the first chain.
364  */
createProxyChain()365 void GPSModelIndexProxyMapperPrivate::createProxyChain()
366 {
367     foreach (auto p, m_proxyChainUp)
368     {
369         p->disconnect(q_ptr);
370     }
371 
372     foreach (auto p, m_proxyChainDown)
373     {
374         p->disconnect(q_ptr);
375     }
376 
377     m_proxyChainUp.clear();
378     m_proxyChainDown.clear();
379     QPointer<const QAbstractItemModel> targetModel                = m_rightModel;
380 
381     QList<QPointer<const QAbstractProxyModel> > proxyChainDown;
382     QPointer<const QAbstractProxyModel> selectionTargetProxyModel = qobject_cast<const QAbstractProxyModel *>(targetModel);
383 
384     while (selectionTargetProxyModel)
385     {
386         proxyChainDown.prepend(selectionTargetProxyModel);
387 
388         QObject::connect(selectionTargetProxyModel.data(), &QAbstractProxyModel::sourceModelChanged,
389                          q_ptr, [this]
390                          {
391                              createProxyChain();
392                          }
393                         );
394 
395         selectionTargetProxyModel = qobject_cast<const QAbstractProxyModel*>(selectionTargetProxyModel->sourceModel());
396 
397         if (selectionTargetProxyModel == m_leftModel)
398         {
399             m_proxyChainDown = proxyChainDown;
400             checkConnected();
401 
402             return;
403         }
404     }
405 
406     QPointer<const QAbstractItemModel> sourceModel       = m_leftModel;
407     QPointer<const QAbstractProxyModel> sourceProxyModel = qobject_cast<const QAbstractProxyModel *>(sourceModel);
408 
409     while (sourceProxyModel)
410     {
411         m_proxyChainUp.append(sourceProxyModel);
412 
413         QObject::connect(sourceProxyModel.data(), &QAbstractProxyModel::sourceModelChanged,
414                          q_ptr, [this]
415                          {
416                              createProxyChain();
417                          }
418                         );
419 
420         sourceProxyModel      = qobject_cast<const QAbstractProxyModel *>(sourceProxyModel->sourceModel());
421         const int targetIndex = proxyChainDown.indexOf(sourceProxyModel);
422 
423         if (targetIndex != -1)
424         {
425             m_proxyChainDown = proxyChainDown.mid(targetIndex + 1, proxyChainDown.size());
426             checkConnected();
427 
428             return;
429         }
430     }
431 
432     m_proxyChainDown = proxyChainDown;
433     checkConnected();
434 }
435 
checkConnected()436 void GPSModelIndexProxyMapperPrivate::checkConnected()
437 {
438     auto konamiRight = m_proxyChainUp.isEmpty()   ? m_leftModel  : m_proxyChainUp.last()->sourceModel();
439     auto konamiLeft  = m_proxyChainDown.isEmpty() ? m_rightModel : m_proxyChainDown.first()->sourceModel();
440     setConnected(konamiLeft && (konamiLeft == konamiRight));
441 }
442 
setConnected(bool connected)443 void GPSModelIndexProxyMapperPrivate::setConnected(bool connected)
444 {
445     if (mConnected != connected)
446     {
447         Q_Q(GPSModelIndexProxyMapper);
448         mConnected = connected;
449         emit q->isConnectedChanged();
450     }
451 }
452 
GPSModelIndexProxyMapper(const QAbstractItemModel * const leftModel,const QAbstractItemModel * const rightModel,QObject * const parent)453 GPSModelIndexProxyMapper::GPSModelIndexProxyMapper(const QAbstractItemModel* const leftModel,
454                                                    const QAbstractItemModel* const rightModel,
455                                                    QObject* const parent)
456     : QObject(parent),
457       d_ptr(new GPSModelIndexProxyMapperPrivate(leftModel, rightModel, this))
458 {
459 }
460 
~GPSModelIndexProxyMapper()461 GPSModelIndexProxyMapper::~GPSModelIndexProxyMapper()
462 {
463     delete d_ptr;
464 }
465 
mapLeftToRight(const QModelIndex & index) const466 QModelIndex GPSModelIndexProxyMapper::mapLeftToRight(const QModelIndex& index) const
467 {
468     const QItemSelection selection = mapSelectionLeftToRight(QItemSelection(index, index));
469 
470     if (selection.isEmpty())
471     {
472         return QModelIndex();
473     }
474 
475     return selection.indexes().first();
476 }
477 
mapRightToLeft(const QModelIndex & index) const478 QModelIndex GPSModelIndexProxyMapper::mapRightToLeft(const QModelIndex& index) const
479 {
480     const QItemSelection selection = mapSelectionRightToLeft(QItemSelection(index, index));
481 
482     if (selection.isEmpty())
483     {
484         return QModelIndex();
485     }
486 
487     return selection.indexes().first();
488 }
489 
mapSelectionLeftToRight(const QItemSelection & selection) const490 QItemSelection GPSModelIndexProxyMapper::mapSelectionLeftToRight(const QItemSelection& selection) const
491 {
492     Q_D(const GPSModelIndexProxyMapper);
493 
494     if (selection.isEmpty() || !d->mConnected)
495     {
496         return QItemSelection();
497     }
498 
499     if (selection.first().model() != d->m_leftModel)
500     {
501         qCDebug(DIGIKAM_GENERAL_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel;
502     }
503 
504     Q_ASSERT(selection.first().model() == d->m_leftModel);
505 
506     QItemSelection seekSelection = selection;
507     Q_ASSERT(d->assertSelectionValid(seekSelection));
508     QListIterator<QPointer<const QAbstractProxyModel> > iUp(d->m_proxyChainUp);
509 
510     while (iUp.hasNext())
511     {
512         const QPointer<const QAbstractProxyModel> proxy = iUp.next();
513 
514         if (!proxy)
515         {
516             return QItemSelection();
517         }
518 
519         Q_ASSERT(seekSelection.isEmpty() || (seekSelection.first().model() == proxy));
520         seekSelection = proxy->mapSelectionToSource(seekSelection);
521         Q_ASSERT(seekSelection.isEmpty() || (seekSelection.first().model() == proxy->sourceModel()));
522 
523         Q_ASSERT(d->assertSelectionValid(seekSelection));
524     }
525 
526     QListIterator<QPointer<const QAbstractProxyModel> > iDown(d->m_proxyChainDown);
527 
528     while (iDown.hasNext())
529     {
530         const QPointer<const QAbstractProxyModel> proxy = iDown.next();
531 
532         if (!proxy)
533         {
534             return QItemSelection();
535         }
536 
537         Q_ASSERT(seekSelection.isEmpty() || (seekSelection.first().model() == proxy->sourceModel()));
538         seekSelection = proxy->mapSelectionFromSource(seekSelection);
539         Q_ASSERT(seekSelection.isEmpty() || (seekSelection.first().model() == proxy));
540 
541         Q_ASSERT(d->assertSelectionValid(seekSelection));
542     }
543 
544     Q_ASSERT((!seekSelection.isEmpty() && (seekSelection.first().model() == d->m_rightModel)) || true);
545 
546     return seekSelection;
547 }
548 
mapSelectionRightToLeft(const QItemSelection & selection) const549 QItemSelection GPSModelIndexProxyMapper::mapSelectionRightToLeft(const QItemSelection& selection) const
550 {
551     Q_D(const GPSModelIndexProxyMapper);
552 
553     if (selection.isEmpty() || !d->mConnected)
554     {
555         return QItemSelection();
556     }
557 
558     if (selection.first().model() != d->m_rightModel)
559     {
560         qCDebug(DIGIKAM_GENERAL_LOG) << "FAIL" << selection.first().model() << d->m_leftModel << d->m_rightModel;
561     }
562 
563     Q_ASSERT(selection.first().model() == d->m_rightModel);
564 
565     QItemSelection seekSelection = selection;
566     Q_ASSERT(d->assertSelectionValid(seekSelection));
567     QListIterator<QPointer<const QAbstractProxyModel> > iDown(d->m_proxyChainDown);
568 
569     iDown.toBack();
570 
571     while (iDown.hasPrevious())
572     {
573         const QPointer<const QAbstractProxyModel> proxy = iDown.previous();
574 
575         if (!proxy)
576         {
577             return QItemSelection();
578         }
579 
580         seekSelection = proxy->mapSelectionToSource(seekSelection);
581 
582         Q_ASSERT(d->assertSelectionValid(seekSelection));
583     }
584 
585     QListIterator<QPointer<const QAbstractProxyModel> > iUp(d->m_proxyChainUp);
586 
587     iUp.toBack();
588 
589     while (iUp.hasPrevious())
590     {
591         const QPointer<const QAbstractProxyModel> proxy = iUp.previous();
592 
593         if (!proxy)
594         {
595             return QItemSelection();
596         }
597 
598         seekSelection = proxy->mapSelectionFromSource(seekSelection);
599 
600         Q_ASSERT(d->assertSelectionValid(seekSelection));
601     }
602 
603     Q_ASSERT((!seekSelection.isEmpty() && (seekSelection.first().model() == d->m_leftModel)) || true);
604 
605     return seekSelection;
606 }
607 
isConnected() const608 bool GPSModelIndexProxyMapper::isConnected() const
609 {
610     Q_D(const GPSModelIndexProxyMapper);
611 
612     return d->mConnected;
613 }
614 
615 } // namespace Digikam
616 
617 #include "moc_gpsitemsortproxymodel.cpp"
618