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