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