1 #include "library/sidebarmodel.h"
2
3 #include <QApplication>
4 #include <QUrl>
5 #include <QtDebug>
6
7 #include "library/browse/browsefeature.h"
8 #include "library/libraryfeature.h"
9 #include "library/treeitem.h"
10 #include "moc_sidebarmodel.cpp"
11 #include "util/assert.h"
12
13 namespace {
14
15 // The time between selecting and activating (= clicking) a feature item
16 // in the sidebar tree. This is essential to allow smooth scrolling through
17 // a list of items with an encoder or the keyboard! A value of 300 ms has
18 // been chosen as a compromise between usability and responsiveness.
19 const int kPressedUntilClickedTimeoutMillis = 300;
20
21 } // anonymous namespace
22
SidebarModel(QObject * parent)23 SidebarModel::SidebarModel(
24 QObject* parent)
25 : QAbstractItemModel(parent),
26 m_iDefaultSelectedIndex(0),
27 m_pressedUntilClickedTimer(new QTimer(this)) {
28 m_pressedUntilClickedTimer->setSingleShot(true);
29 connect(m_pressedUntilClickedTimer,
30 &QTimer::timeout,
31 this,
32 &SidebarModel::slotPressedUntilClickedTimeout);
33 }
34
addLibraryFeature(LibraryFeature * feature)35 void SidebarModel::addLibraryFeature(LibraryFeature* feature) {
36 m_sFeatures.push_back(feature);
37 connect(feature,
38 &LibraryFeature::featureIsLoading,
39 this,
40 &SidebarModel::slotFeatureIsLoading);
41 connect(feature,
42 &LibraryFeature::featureLoadingFinished,
43 this,
44 &SidebarModel::slotFeatureLoadingFinished);
45 connect(feature,
46 &LibraryFeature::featureSelect,
47 this,
48 &SidebarModel::slotFeatureSelect);
49
50 QAbstractItemModel* model = feature->getChildModel();
51
52 connect(model,
53 &QAbstractItemModel::modelAboutToBeReset,
54 this,
55 &SidebarModel::slotModelAboutToBeReset);
56 connect(model,
57 &QAbstractItemModel::modelReset,
58 this,
59 &SidebarModel::slotModelReset);
60 connect(model,
61 &QAbstractItemModel::dataChanged,
62 this,
63 &SidebarModel::slotDataChanged);
64
65 connect(model,
66 &QAbstractItemModel::rowsAboutToBeInserted,
67 this,
68 &SidebarModel::slotRowsAboutToBeInserted);
69 connect(model,
70 &QAbstractItemModel::rowsAboutToBeRemoved,
71 this,
72 &SidebarModel::slotRowsAboutToBeRemoved);
73 connect(model,
74 &QAbstractItemModel::rowsInserted,
75 this,
76 &SidebarModel::slotRowsInserted);
77 connect(model,
78 &QAbstractItemModel::rowsRemoved,
79 this,
80 &SidebarModel::slotRowsRemoved);
81 }
82
getDefaultSelection()83 QModelIndex SidebarModel::getDefaultSelection() {
84 if (m_sFeatures.size() == 0) {
85 return QModelIndex();
86 }
87 return createIndex(m_iDefaultSelectedIndex, 0, (void*)this);
88 }
89
setDefaultSelection(unsigned int index)90 void SidebarModel::setDefaultSelection(unsigned int index) {
91 m_iDefaultSelectedIndex = index;
92 }
93
activateDefaultSelection()94 void SidebarModel::activateDefaultSelection() {
95 if (m_iDefaultSelectedIndex <
96 static_cast<unsigned int>(m_sFeatures.size())) {
97 emit selectIndex(getDefaultSelection());
98 // Selecting an index does not activate it.
99 m_sFeatures[m_iDefaultSelectedIndex]->activate();
100 }
101 }
102
index(int row,int column,const QModelIndex & parent) const103 QModelIndex SidebarModel::index(int row, int column,
104 const QModelIndex& parent) const {
105 // qDebug() << "SidebarModel::index row=" << row
106 // << "column=" << column << "parent=" << parent.getData();
107 if (parent.isValid()) {
108 /* If we have selected the root of a library feature at position 'row'
109 * its internal pointer is the current sidebar object model
110 * we return its associated childmodel
111 */
112 if (parent.internalPointer() == this) {
113 const QAbstractItemModel* childModel = m_sFeatures[parent.row()]->getChildModel();
114 QModelIndex childIndex = childModel->index(row, column);
115 TreeItem* tree_item = (TreeItem*)childIndex.internalPointer();
116 if (tree_item && childIndex.isValid()) {
117 return createIndex(childIndex.row(), childIndex.column(), (void*)tree_item);
118 } else {
119 return QModelIndex();
120 }
121 } else {
122 // We have selected an item within the childmodel
123 // This item has always an internal pointer of (sub)type TreeItem
124 TreeItem* tree_item = (TreeItem*)parent.internalPointer();
125 if (row < tree_item->childRows()) {
126 return createIndex(row, column, (void*) tree_item->child(row));
127 } else {
128 // Otherwise this row might have been removed just now
129 // (just a dirty workaround for unmaintainable GUI code)
130 return QModelIndex();
131 }
132 }
133 }
134 return createIndex(row, column, (void*)this);
135 }
136
parent(const QModelIndex & index) const137 QModelIndex SidebarModel::parent(const QModelIndex& index) const {
138 //qDebug() << "SidebarModel::parent index=" << index.getData();
139 if (index.isValid()) {
140 // If we have selected the root of a library feature
141 // its internal pointer is the current sidebar object model
142 // A root library feature has no parent and thus we return
143 // an invalid QModelIndex
144 if (index.internalPointer() == this) {
145 return QModelIndex();
146 } else {
147 TreeItem* tree_item = (TreeItem*)index.internalPointer();
148 if (tree_item == nullptr) {
149 return QModelIndex();
150 }
151 TreeItem* tree_item_parent = tree_item->parent();
152 // if we have selected an item at the first level of a childnode
153
154 if (tree_item_parent) {
155 if (tree_item_parent->isRoot()) {
156 LibraryFeature* feature = tree_item->feature();
157 for (int i = 0; i < m_sFeatures.size(); ++i) {
158 if (feature == m_sFeatures[i]) {
159 // create a ModelIndex for parent 'this' having a
160 // library feature at position 'i'
161 return createIndex(i, 0, (void*)this);
162 }
163 }
164 }
165 // if we have selected an item at some deeper level of a childnode
166 return createIndex(tree_item_parent->parentRow(), 0 , tree_item_parent);
167 }
168 }
169 }
170 return QModelIndex();
171 }
172
rowCount(const QModelIndex & parent) const173 int SidebarModel::rowCount(const QModelIndex& parent) const {
174 //qDebug() << "SidebarModel::rowCount parent=" << parent.getData();
175 if (parent.isValid()) {
176 if (parent.internalPointer() == this) {
177 return m_sFeatures[parent.row()]->getChildModel()->rowCount();
178 } else {
179 // We support tree models deeper than 1 level
180 TreeItem* tree_item = (TreeItem*)parent.internalPointer();
181 if (tree_item) {
182 return tree_item->childRows();
183 }
184 return 0;
185 }
186 }
187 return m_sFeatures.size();
188 }
189
columnCount(const QModelIndex & parent) const190 int SidebarModel::columnCount(const QModelIndex& parent) const {
191 Q_UNUSED(parent);
192 //qDebug() << "SidebarModel::columnCount parent=" << parent;
193 // TODO(rryan) will we ever have columns? I don't think so.
194 return 1;
195 }
196
hasChildren(const QModelIndex & parent) const197 bool SidebarModel::hasChildren(const QModelIndex& parent) const {
198 if (parent.isValid()) {
199 if (parent.internalPointer() == this) {
200 return QAbstractItemModel::hasChildren(parent);
201 }
202 else
203 {
204 TreeItem* tree_item = (TreeItem*)parent.internalPointer();
205 if (tree_item) {
206 LibraryFeature* feature = tree_item->feature();
207 return feature->getChildModel()->hasChildren(parent);
208 }
209 }
210 }
211
212 return QAbstractItemModel::hasChildren(parent);
213 }
214
data(const QModelIndex & index,int role) const215 QVariant SidebarModel::data(const QModelIndex& index, int role) const {
216 // qDebug("SidebarModel::data row=%d column=%d pointer=%8x, role=%d",
217 // index.row(), index.column(), index.internalPointer(), role);
218 if (!index.isValid()) {
219 return QVariant();
220 }
221
222 if (index.internalPointer() == this) {
223 //If it points to SidebarModel
224 if (role == Qt::DisplayRole) {
225 return m_sFeatures[index.row()]->title();
226 } else if (role == Qt::DecorationRole) {
227 return m_sFeatures[index.row()]->getIcon();
228 }
229 }
230
231 if (index.internalPointer() != this) {
232 // If it points to a TreeItem
233 TreeItem* tree_item = (TreeItem*)index.internalPointer();
234 if (tree_item) {
235 if (role == Qt::DisplayRole) {
236 return tree_item->getLabel();
237 } else if (role == Qt::ToolTipRole) {
238 // If it's the "Quick Links" node, display it's name
239 if (tree_item->getData().toString() == QUICK_LINK_NODE) {
240 return tree_item->getLabel();
241 } else {
242 return tree_item->getData();
243 }
244 } else if (role == TreeItemModel::kDataRole) {
245 // We use Qt::UserRole to ask for the datapath.
246 return tree_item->getData();
247 } else if (role == Qt::FontRole) {
248 QFont font;
249 font.setBold(tree_item->isBold());
250 return font;
251 } else if (role == Qt::DecorationRole) {
252 return tree_item->getIcon();
253 }
254 }
255 }
256
257 return QVariant();
258 }
259
startPressedUntilClickedTimer(const QModelIndex & pressedIndex)260 void SidebarModel::startPressedUntilClickedTimer(const QModelIndex& pressedIndex) {
261 m_pressedIndex = pressedIndex;
262 m_pressedUntilClickedTimer->start(kPressedUntilClickedTimeoutMillis);
263 }
264
stopPressedUntilClickedTimer()265 void SidebarModel::stopPressedUntilClickedTimer() {
266 m_pressedUntilClickedTimer->stop();
267 m_pressedIndex = QModelIndex();
268 }
269
slotPressedUntilClickedTimeout()270 void SidebarModel::slotPressedUntilClickedTimeout() {
271 if (m_pressedIndex.isValid()) {
272 QModelIndex clickedIndex = m_pressedIndex;
273 stopPressedUntilClickedTimer();
274 clicked(clickedIndex);
275 }
276 }
277
pressed(const QModelIndex & index)278 void SidebarModel::pressed(const QModelIndex& index) {
279 stopPressedUntilClickedTimer();
280 if (index.isValid()) {
281 startPressedUntilClickedTimer(index);
282 }
283 }
284
clicked(const QModelIndex & index)285 void SidebarModel::clicked(const QModelIndex& index) {
286 // When triggered by a mouse event pressed() has been
287 // invoked immediately before. That doesn't matter,
288 // because we stop any running timer before handling
289 // this event.
290 stopPressedUntilClickedTimer();
291 if (index.isValid()) {
292 if (index.internalPointer() == this) {
293 m_sFeatures[index.row()]->activate();
294 } else {
295 TreeItem* tree_item = static_cast<TreeItem*>(index.internalPointer());
296 if (tree_item) {
297 LibraryFeature* feature = tree_item->feature();
298 DEBUG_ASSERT(feature);
299 feature->activateChild(index);
300 }
301 }
302 }
303 }
304
doubleClicked(const QModelIndex & index)305 void SidebarModel::doubleClicked(const QModelIndex& index) {
306 stopPressedUntilClickedTimer();
307 if (index.isValid()) {
308 if (index.internalPointer() == this) {
309 return;
310 } else {
311 TreeItem* tree_item = (TreeItem*)index.internalPointer();
312 if (tree_item) {
313 LibraryFeature* feature = tree_item->feature();
314 feature->onLazyChildExpandation(index);
315 }
316 }
317 }
318 }
319
rightClicked(const QPoint & globalPos,const QModelIndex & index)320 void SidebarModel::rightClicked(const QPoint& globalPos, const QModelIndex& index) {
321 stopPressedUntilClickedTimer();
322 if (index.isValid()) {
323 if (index.internalPointer() == this) {
324 m_sFeatures[index.row()]->onRightClick(globalPos);
325 }
326 else
327 {
328 TreeItem* tree_item = (TreeItem*)index.internalPointer();
329 if (tree_item) {
330 LibraryFeature* feature = tree_item->feature();
331 feature->onRightClickChild(globalPos, index);
332 }
333 }
334 }
335 }
336
dropAccept(const QModelIndex & index,const QList<QUrl> & urls,QObject * pSource)337 bool SidebarModel::dropAccept(const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
338 //qDebug() << "SidebarModel::dropAccept() index=" << index << url;
339 bool result = false;
340 if (index.isValid()) {
341 if (index.internalPointer() == this) {
342 result = m_sFeatures[index.row()]->dropAccept(urls, pSource);
343 } else {
344 TreeItem* tree_item = (TreeItem*)index.internalPointer();
345 if (tree_item) {
346 LibraryFeature* feature = tree_item->feature();
347 result = feature->dropAcceptChild(index, urls, pSource);
348 }
349 }
350 }
351 return result;
352 }
353
hasTrackTable(const QModelIndex & index) const354 bool SidebarModel::hasTrackTable(const QModelIndex& index) const {
355 if (index.internalPointer() == this) {
356 return m_sFeatures[index.row()]->hasTrackTable();
357 }
358 return false;
359 }
360
dragMoveAccept(const QModelIndex & index,const QUrl & url)361 bool SidebarModel::dragMoveAccept(const QModelIndex& index, const QUrl& url) {
362 //qDebug() << "SidebarModel::dragMoveAccept() index=" << index << url;
363 bool result = false;
364
365 if (index.isValid()) {
366 if (index.internalPointer() == this) {
367 result = m_sFeatures[index.row()]->dragMoveAccept(url);
368 } else {
369 TreeItem* tree_item = (TreeItem*)index.internalPointer();
370 if (tree_item) {
371 LibraryFeature* feature = tree_item->feature();
372 result = feature->dragMoveAcceptChild(index, url);
373 }
374 }
375 }
376 return result;
377 }
378
379 // Translates an index from the child models to an index of the sidebar models
translateSourceIndex(const QModelIndex & index)380 QModelIndex SidebarModel::translateSourceIndex(const QModelIndex& index) {
381 QModelIndex translatedIndex;
382
383 /* These method is called from the slot functions below.
384 * QObject::sender() return the object which emitted the signal
385 * handled by the slot functions.
386
387 * For child models, this always the child models itself
388 */
389
390 const QAbstractItemModel* model = qobject_cast<QAbstractItemModel*>(sender());
391 VERIFY_OR_DEBUG_ASSERT(model != nullptr) {
392 return QModelIndex();
393 }
394
395 if (index.isValid()) {
396 TreeItem* item = (TreeItem*)index.internalPointer();
397 translatedIndex = createIndex(index.row(), index.column(), item);
398 }
399 else
400 {
401 //Comment from Tobias Rafreider --> Dead Code????
402
403 for (int i = 0; i < m_sFeatures.size(); ++i) {
404 if (m_sFeatures[i]->getChildModel() == model) {
405 translatedIndex = createIndex(i, 0, (void*)this);
406 }
407 }
408 }
409 return translatedIndex;
410 }
411
slotDataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight)412 void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) {
413 //qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight;
414 QModelIndex topLeftTranslated = translateSourceIndex(topLeft);
415 QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight);
416 emit dataChanged(topLeftTranslated, bottomRightTranslated);
417 }
418
slotRowsAboutToBeInserted(const QModelIndex & parent,int start,int end)419 void SidebarModel::slotRowsAboutToBeInserted(const QModelIndex& parent, int start, int end) {
420 //qDebug() << "slotRowsABoutToBeInserted" << parent << start << end;
421
422 QModelIndex newParent = translateSourceIndex(parent);
423 beginInsertRows(newParent, start, end);
424 }
425
slotRowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)426 void SidebarModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) {
427 //qDebug() << "slotRowsABoutToBeRemoved" << parent << start << end;
428
429 QModelIndex newParent = translateSourceIndex(parent);
430 beginRemoveRows(newParent, start, end);
431 }
432
slotRowsInserted(const QModelIndex & parent,int start,int end)433 void SidebarModel::slotRowsInserted(const QModelIndex& parent, int start, int end) {
434 Q_UNUSED(parent);
435 Q_UNUSED(start);
436 Q_UNUSED(end);
437 //qDebug() << "slotRowsInserted" << parent << start << end;
438 //QModelIndex newParent = translateSourceIndex(parent);
439 endInsertRows();
440 }
441
slotRowsRemoved(const QModelIndex & parent,int start,int end)442 void SidebarModel::slotRowsRemoved(const QModelIndex& parent, int start, int end) {
443 Q_UNUSED(parent);
444 Q_UNUSED(start);
445 Q_UNUSED(end);
446 //qDebug() << "slotRowsRemoved" << parent << start << end;
447 //QModelIndex newParent = translateSourceIndex(parent);
448 endRemoveRows();
449 }
450
slotModelAboutToBeReset()451 void SidebarModel::slotModelAboutToBeReset() {
452 beginResetModel();
453 }
454
slotModelReset()455 void SidebarModel::slotModelReset() {
456 endResetModel();
457 }
458
459 /*
460 * Call this slot whenever the title of the feature has changed.
461 * See RhythmboxFeature for an example, in which the title becomes '(loading) Rhythmbox'
462 * If selectFeature is true, the feature is selected when the title change occurs.
463 */
slotFeatureIsLoading(LibraryFeature * feature,bool selectFeature)464 void SidebarModel::slotFeatureIsLoading(LibraryFeature * feature, bool selectFeature) {
465 featureRenamed(feature);
466 if (selectFeature) {
467 slotFeatureSelect(feature);
468 }
469 }
470
471 /* Tobias: This slot is somewhat redundant but I decided
472 * to leave it for code readability reasons
473 */
slotFeatureLoadingFinished(LibraryFeature * feature)474 void SidebarModel::slotFeatureLoadingFinished(LibraryFeature * feature) {
475 featureRenamed(feature);
476 slotFeatureSelect(feature);
477 }
478
featureRenamed(LibraryFeature * pFeature)479 void SidebarModel::featureRenamed(LibraryFeature* pFeature) {
480 for (int i=0; i < m_sFeatures.size(); ++i) {
481 if (m_sFeatures[i] == pFeature) {
482 QModelIndex ind = index(i, 0);
483 emit dataChanged(ind, ind);
484 }
485 }
486 }
487
slotFeatureSelect(LibraryFeature * pFeature,const QModelIndex & featureIndex)488 void SidebarModel::slotFeatureSelect(LibraryFeature* pFeature, const QModelIndex& featureIndex) {
489 QModelIndex ind;
490 if (featureIndex.isValid()) {
491 TreeItem* item = (TreeItem*)featureIndex.internalPointer();
492 ind = createIndex(featureIndex.row(), featureIndex.column(), item);
493 } else {
494 for (int i=0; i < m_sFeatures.size(); ++i) {
495 if (m_sFeatures[i] == pFeature) {
496 ind = index(i, 0);
497 break;
498 }
499 }
500 }
501 emit selectIndex(ind);
502 }
503