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 "klinkitemselectionmodel.h"
11#include "kitemmodels_debug.h"
12#include "kmodelindexproxymapper.h"
13
14#include <QItemSelection>
15
16class KLinkItemSelectionModelPrivate
17{
18public:
19 KLinkItemSelectionModelPrivate(KLinkItemSelectionModel *proxySelectionModel)
20 : q_ptr(proxySelectionModel)
21 {
22 QObject::connect(sender: q_ptr, signal: &QItemSelectionModel::currentChanged, context: q_ptr, slot: [this](const QModelIndex &idx) {
23 slotCurrentChanged(current: idx);
24 });
25
26 QObject::connect(sender: q_ptr, signal: &QItemSelectionModel::modelChanged, context: q_ptr, slot: [this] {
27 reinitializeIndexMapper();
28 });
29 }
30
31 Q_DECLARE_PUBLIC(KLinkItemSelectionModel)
32 KLinkItemSelectionModel *const q_ptr;
33
34 bool assertSelectionValid(const QItemSelection &selection) const
35 {
36 for (const QItemSelectionRange &range : selection) {
37 if (!range.isValid()) {
38 qCDebug(KITEMMODELS_LOG) << selection;
39 }
40 Q_ASSERT(range.isValid());
41 }
42 return true;
43 }
44
45 void reinitializeIndexMapper()
46 {
47 delete m_indexMapper;
48 m_indexMapper = nullptr;
49 if (!q_ptr->model() || !m_linkedItemSelectionModel || !m_linkedItemSelectionModel->model()) {
50 return;
51 }
52 m_indexMapper = new KModelIndexProxyMapper(q_ptr->model(), m_linkedItemSelectionModel->model(), q_ptr);
53 const QItemSelection mappedSelection = m_indexMapper->mapSelectionRightToLeft(selection: m_linkedItemSelectionModel->selection());
54 q_ptr->QItemSelectionModel::select(selection: mappedSelection, command: QItemSelectionModel::ClearAndSelect);
55 }
56
57 void sourceSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
58 void sourceCurrentChanged(const QModelIndex &current);
59 void slotCurrentChanged(const QModelIndex &current);
60
61 QItemSelectionModel *m_linkedItemSelectionModel = nullptr;
62 bool m_ignoreCurrentChanged = false;
63 KModelIndexProxyMapper *m_indexMapper = nullptr;
64};
65
66KLinkItemSelectionModel::KLinkItemSelectionModel(QAbstractItemModel *model, QItemSelectionModel *proxySelector, QObject *parent)
67 : QItemSelectionModel(model, parent)
68 , d_ptr(new KLinkItemSelectionModelPrivate(this))
69{
70 setLinkedItemSelectionModel(proxySelector);
71}
72
73KLinkItemSelectionModel::KLinkItemSelectionModel(QObject *parent)
74 : QItemSelectionModel(nullptr, parent)
75 , d_ptr(new KLinkItemSelectionModelPrivate(this))
76{
77}
78
79KLinkItemSelectionModel::~KLinkItemSelectionModel() = default;
80
81QItemSelectionModel *KLinkItemSelectionModel::linkedItemSelectionModel() const
82{
83 Q_D(const KLinkItemSelectionModel);
84 return d->m_linkedItemSelectionModel;
85}
86
87void KLinkItemSelectionModel::setLinkedItemSelectionModel(QItemSelectionModel *selectionModel)
88{
89 Q_D(KLinkItemSelectionModel);
90 if (d->m_linkedItemSelectionModel != selectionModel) {
91 if (d->m_linkedItemSelectionModel) {
92 disconnect(receiver: d->m_linkedItemSelectionModel);
93 }
94
95 d->m_linkedItemSelectionModel = selectionModel;
96
97 if (d->m_linkedItemSelectionModel) {
98 connect(sender: d->m_linkedItemSelectionModel,
99 signal: &QItemSelectionModel::selectionChanged,
100 context: this,
101 slot: [d](const QItemSelection &selected, const QItemSelection &deselected) {
102 d->sourceSelectionChanged(selected, deselected);
103 });
104 connect(sender: d->m_linkedItemSelectionModel, signal: &QItemSelectionModel::currentChanged, context: this, slot: [d](const QModelIndex &current) {
105 d->sourceCurrentChanged(current);
106 });
107
108 connect(sender: d->m_linkedItemSelectionModel, signal: &QItemSelectionModel::modelChanged, context: this, slot: [this] {
109 d_ptr->reinitializeIndexMapper();
110 });
111 }
112 d->reinitializeIndexMapper();
113 Q_EMIT linkedItemSelectionModelChanged();
114 }
115}
116
117void KLinkItemSelectionModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command)
118{
119 Q_D(KLinkItemSelectionModel);
120 // When an item is removed, the current index is set to the top index in the model.
121 // That causes a selectionChanged signal with a selection which we do not want.
122 if (d->m_ignoreCurrentChanged) {
123 return;
124 }
125 // Do *not* replace next line with: QItemSelectionModel::select(index, command)
126 //
127 // Doing so would end up calling KLinkItemSelectionModel::select(QItemSelection, QItemSelectionModel::SelectionFlags)
128 //
129 // This is because the code for QItemSelectionModel::select(QModelIndex, QItemSelectionModel::SelectionFlags) looks like this:
130 // {
131 // QItemSelection selection(index, index);
132 // select(selection, command);
133 // }
134 // So it calls KLinkItemSelectionModel overload of
135 // select(QItemSelection, QItemSelectionModel::SelectionFlags)
136 //
137 // When this happens and the selection flags include Toggle, it causes the
138 // selection to be toggled twice.
139 QItemSelectionModel::select(selection: QItemSelection(index, index), command);
140 if (index.isValid()) {
141 d->m_linkedItemSelectionModel->select(selection: d->m_indexMapper->mapSelectionLeftToRight(selection: QItemSelection(index, index)), command);
142 } else {
143 d->m_linkedItemSelectionModel->clearSelection();
144 }
145}
146
147void KLinkItemSelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command)
148{
149 Q_D(KLinkItemSelectionModel);
150 d->m_ignoreCurrentChanged = true;
151 QItemSelection _selection = selection;
152 QItemSelectionModel::select(selection: _selection, command);
153 Q_ASSERT(d->assertSelectionValid(_selection));
154 QItemSelection mappedSelection = d->m_indexMapper->mapSelectionLeftToRight(selection: _selection);
155 Q_ASSERT(d->assertSelectionValid(mappedSelection));
156 d->m_linkedItemSelectionModel->select(selection: mappedSelection, command);
157 d->m_ignoreCurrentChanged = false;
158}
159
160void KLinkItemSelectionModelPrivate::slotCurrentChanged(const QModelIndex &current)
161{
162 const QModelIndex mappedCurrent = m_indexMapper->mapLeftToRight(index: current);
163 if (!mappedCurrent.isValid()) {
164 return;
165 }
166 m_linkedItemSelectionModel->setCurrentIndex(index: mappedCurrent, command: QItemSelectionModel::NoUpdate);
167}
168
169void KLinkItemSelectionModelPrivate::sourceSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
170{
171 Q_Q(KLinkItemSelectionModel);
172 QItemSelection _selected = selected;
173 QItemSelection _deselected = deselected;
174 Q_ASSERT(assertSelectionValid(_selected));
175 Q_ASSERT(assertSelectionValid(_deselected));
176 const QItemSelection mappedDeselection = m_indexMapper->mapSelectionRightToLeft(selection: _deselected);
177 const QItemSelection mappedSelection = m_indexMapper->mapSelectionRightToLeft(selection: _selected);
178
179 q->QItemSelectionModel::select(selection: mappedDeselection, command: QItemSelectionModel::Deselect);
180 q->QItemSelectionModel::select(selection: mappedSelection, command: QItemSelectionModel::Select);
181}
182
183void KLinkItemSelectionModelPrivate::sourceCurrentChanged(const QModelIndex &current)
184{
185 Q_Q(KLinkItemSelectionModel);
186 const QModelIndex mappedCurrent = m_indexMapper->mapRightToLeft(index: current);
187 if (!mappedCurrent.isValid()) {
188 return;
189 }
190 q->setCurrentIndex(index: mappedCurrent, command: QItemSelectionModel::NoUpdate);
191}
192
193#include "moc_klinkitemselectionmodel.cpp"
194

source code of kitemmodels/src/core/klinkitemselectionmodel.cpp