]> git.sur5r.net Git - minitube/blob - src/playlistmodel.cpp
Views refactoring, don't use RTTI
[minitube] / src / playlistmodel.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
6 Minitube is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Minitube is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
18
19 $END_LICENSE */
20
21 #include "playlistmodel.h"
22 #include "videomimedata.h"
23 #include "videosource.h"
24 #include "ytsearch.h"
25 #include "video.h"
26 #include "searchparams.h"
27 #include "mediaview.h"
28
29 static const int maxItems = 50;
30 static const QString recentKeywordsKey = "recentKeywords";
31 static const QString recentChannelsKey = "recentChannels";
32
33 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
34     videoSource = 0;
35     searching = false;
36     canSearchMore = true;
37     firstSearch = false;
38     m_activeVideo = 0;
39     m_activeRow = -1;
40     startIndex = 1;
41     max = 0;
42     hoveredRow = -1;
43     authorHovered = false;
44     authorPressed = false;
45 }
46
47 int PlaylistModel::rowCount(const QModelIndex &/*parent*/) const {
48     int count = videos.size();
49     
50     // add the message item
51     if (videos.isEmpty() || !searching)
52         count++;
53     
54     return count;
55 }
56
57 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
58     
59     int row = index.row();
60     
61     if (row == videos.size()) {
62         
63         QPalette palette;
64         
65         switch (role) {
66         case ItemTypeRole:
67             return ItemTypeShowMore;
68         case Qt::DisplayRole:
69             if (!errorMessage.isEmpty()) return errorMessage;
70             if (searching) return tr("Searching...");
71             if (canSearchMore) return tr("Show %1 More").arg("").simplified();
72             if (videos.isEmpty()) return tr("No videos");
73             else return tr("No more videos");
74         case Qt::TextAlignmentRole:
75             return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
76         case Qt::ForegroundRole:
77             if (!errorMessage.isEmpty())
78                 return palette.color(QPalette::ToolTipText);
79             else
80                 return palette.color(QPalette::Dark);
81         case Qt::BackgroundColorRole:
82             if (!errorMessage.isEmpty())
83                 return palette.color(QPalette::ToolTipBase);
84             else
85                 return QVariant();
86         default:
87             return QVariant();
88         }
89         
90     } else if (row < 0 || row >= videos.size())
91         return QVariant();
92     
93     Video *video = videos.at(row);
94     
95     switch (role) {
96     case ItemTypeRole:
97         return ItemTypeVideo;
98     case VideoRole:
99         return QVariant::fromValue(QPointer<Video>(video));
100     case ActiveTrackRole:
101         return video == m_activeVideo;
102     case Qt::DisplayRole:
103         return video->title();
104     case HoveredItemRole:
105         return hoveredRow == index.row();
106     case AuthorHoveredRole:
107         return authorHovered;
108     case AuthorPressedRole:
109         return authorPressed;
110         /*
111     case Qt::StatusTipRole:
112         return video->description();
113         */
114     }
115     
116     return QVariant();
117 }
118
119 void PlaylistModel::setActiveRow(int row, bool notify) {
120     if ( rowExists( row ) ) {
121         
122         m_activeRow = row;
123         m_activeVideo = videoAt(row);
124         
125         int oldactiverow = m_activeRow;
126         
127         if ( rowExists( oldactiverow ) )
128             emit dataChanged( createIndex( oldactiverow, 0 ), createIndex( oldactiverow, columnCount() - 1 ) );
129         
130         emit dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() - 1 ) );
131         if (notify) emit activeRowChanged(row);
132         
133     } else {
134         m_activeRow = -1;
135         m_activeVideo = 0;
136     }
137
138 }
139
140 int PlaylistModel::nextRow() const {
141     int nextRow = m_activeRow + 1;
142     if (rowExists(nextRow))
143         return nextRow;
144     return -1;
145 }
146
147 int PlaylistModel::previousRow() const {
148     int prevRow = m_activeRow - 1;
149     if (rowExists(prevRow))
150         return prevRow;
151     return -1;
152 }
153
154 Video* PlaylistModel::videoAt( int row ) const {
155     if ( rowExists( row ) )
156         return videos.at( row );
157     return 0;
158 }
159
160 Video* PlaylistModel::activeVideo() const {
161     return m_activeVideo;
162 }
163
164 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
165     beginResetModel();
166     while (!videos.isEmpty()) delete videos.takeFirst();
167     videos.clear();
168     m_activeVideo = 0;
169     m_activeRow = -1;
170     startIndex = 1;
171     endResetModel();
172
173     this->videoSource = videoSource;
174     connect(videoSource, SIGNAL(gotVideos(QList<Video*>)),
175             SLOT(addVideos(QList<Video*>)), Qt::UniqueConnection);
176     connect(videoSource, SIGNAL(finished(int)),
177             SLOT(searchFinished(int)), Qt::UniqueConnection);
178     connect(videoSource, SIGNAL(error(QString)),
179             SLOT(searchError(QString)), Qt::UniqueConnection);
180
181     searchMore();
182 }
183
184 void PlaylistModel::searchMore(int max) {
185     if (searching) return;
186     searching = true;
187     firstSearch = startIndex == 1;
188     this->max = max;
189     errorMessage.clear();
190     videoSource->loadVideos(max, startIndex);
191     startIndex += max;
192 }
193
194 void PlaylistModel::searchMore() {
195     searchMore(maxItems);
196 }
197
198 void PlaylistModel::searchNeeded() {
199     const int desiredRowsAhead = 10;
200     int remainingRows = videos.size() - m_activeRow;
201     if (remainingRows < desiredRowsAhead)
202         searchMore(maxItems);
203 }
204
205 void PlaylistModel::abortSearch() {
206     QMutexLocker locker(&mutex);
207     beginResetModel();
208     // while (!videos.isEmpty()) delete videos.takeFirst();
209     // if (videoSource) videoSource->abort();
210     videos.clear();
211     searching = false;
212     m_activeRow = -1;
213     m_activeVideo = 0;
214     startIndex = 1;
215     endResetModel();
216 }
217
218 void PlaylistModel::searchFinished(int total) {
219     qDebug() << __PRETTY_FUNCTION__ << total;
220     searching = false;
221     canSearchMore = videoSource->hasMoreVideos();
222
223     // update the message item
224     emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
225
226     if (!videoSource->getSuggestions().isEmpty())
227         emit haveSuggestions(videoSource->getSuggestions());
228
229     if (firstSearch && !videos.isEmpty())
230         handleFirstVideo(videos.first());
231 }
232
233 void PlaylistModel::searchError(const QString &message) {
234     errorMessage = message;
235     // update the message item
236     emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
237 }
238
239 void PlaylistModel::addVideos(QList<Video*> newVideos) {
240     if (newVideos.isEmpty()) return;
241     beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
242     videos.append(newVideos);
243     endInsertRows();
244     foreach (Video* video, newVideos) {
245         connect(video, SIGNAL(gotThumbnail()),
246                 SLOT(updateVideoSender()), Qt::UniqueConnection);
247         video->loadThumbnail();
248         qApp->processEvents();
249     }
250 }
251
252 void PlaylistModel::handleFirstVideo(Video *video) {
253
254     int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
255     if (currentVideoRow != -1) setActiveRow(currentVideoRow, false);
256     else {
257         QSettings settings;
258         if (!settings.value("manualplay", false).toBool())
259             setActiveRow(0);
260     }
261
262     QSettings settings;
263     if (!settings.value("manualplay", false).toBool()) {
264         int newActiveRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
265         if (newActiveRow != -1) setActiveRow(newActiveRow, false);
266         else setActiveRow(0);
267     }
268
269     if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
270
271         static const int maxRecentElements = 10;
272
273         YTSearch *search = qobject_cast<YTSearch *>(videoSource);
274         SearchParams *searchParams = search->getSearchParams();
275
276         // save keyword
277         QString query = searchParams->keywords();
278         if (!query.isEmpty() && !searchParams->isTransient()) {
279             if (query.startsWith("http://")) {
280                 // Save the video title
281                 query += "|" + videos.first()->title();
282             }
283             QStringList keywords = settings.value(recentKeywordsKey).toStringList();
284             keywords.removeAll(query);
285             keywords.prepend(query);
286             while (keywords.size() > maxRecentElements)
287                 keywords.removeLast();
288             settings.setValue(recentKeywordsKey, keywords);
289         }
290
291         // save channel
292         QString channelId = searchParams->channelId();
293         if (!channelId.isEmpty() && !searchParams->isTransient()) {
294             QString value;
295             if (!video->channelId().isEmpty() && video->channelId() != video->channelTitle())
296                 value = video->channelId() + "|" + video->channelTitle();
297             else value = video->channelTitle();
298             QStringList channels = settings.value(recentChannelsKey).toStringList();
299             channels.removeAll(value);
300             channels.removeAll(channelId);
301             channels.prepend(value);
302             while (channels.size() > maxRecentElements)
303                 channels.removeLast();
304             settings.setValue(recentChannelsKey, channels);
305         }
306     }
307 }
308
309 void PlaylistModel::updateVideoSender() {
310     Video *video = static_cast<Video *>(sender());
311     if (!video) {
312         qDebug() << "Cannot get sender";
313         return;
314     }
315     int row = rowForVideo(video);
316     emit dataChanged( createIndex( row, 0 ), createIndex( row, columnCount() - 1 ) );
317 }
318
319 void PlaylistModel::emitDataChanged() {
320     QModelIndex index = createIndex(rowCount()-1, 0);
321     emit dataChanged(index, index);
322 }
323
324 // --- item removal
325
326 /**
327   * This function does not free memory
328   */
329 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
330     beginRemoveRows(QModelIndex(), position, position+rows-1);
331     for (int row = 0; row < rows; ++row) {
332         videos.removeAt(position);
333     }
334     endRemoveRows();
335     return true;
336 }
337
338 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
339     QList<Video*> originalList(videos);
340     QList<Video*> delitems;
341     foreach (const QModelIndex &index, indexes) {
342         if (index.row() >= originalList.size()) continue;
343         Video* video = originalList.at(index.row());
344         int idx = videos.indexOf(video);
345         if (idx != -1) {
346             beginRemoveRows(QModelIndex(), idx, idx);
347             delitems.append(video);
348             videos.removeAll(video);
349             endRemoveRows();
350         }
351     }
352
353     qDeleteAll(delitems);
354
355 }
356
357 // --- Sturm und drang ---
358
359
360
361 Qt::DropActions PlaylistModel::supportedDropActions() const {
362     return Qt::CopyAction;
363 }
364
365 Qt::DropActions PlaylistModel::supportedDragActions() const {
366     return Qt::CopyAction;
367 }
368
369 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
370     if (index.isValid()) {
371         if (index.row() == videos.size()) {
372             // don't drag the "show more" item
373             return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
374         } else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
375     }
376     return Qt::ItemIsDropEnabled;
377 }
378
379 QStringList PlaylistModel::mimeTypes() const {
380     QStringList types;
381     types << "application/x-minitube-video";
382     return types;
383 }
384
385 QMimeData* PlaylistModel::mimeData( const QModelIndexList &indexes ) const {
386     VideoMimeData* mime = new VideoMimeData();
387
388     foreach( const QModelIndex &it, indexes ) {
389         int row = it.row();
390         if (row >= 0 && row < videos.size())
391             mime->addVideo( videos.at( it.row() ) );
392     }
393
394     return mime;
395 }
396
397 bool PlaylistModel::dropMimeData(const QMimeData *data,
398                                  Qt::DropAction action, int row, int column,
399                                  const QModelIndex &parent) {
400     if (action == Qt::IgnoreAction)
401         return true;
402
403     if (!data->hasFormat("application/x-minitube-video"))
404         return false;
405
406     if (column > 0)
407         return false;
408
409     int beginRow;
410     if (row != -1)
411         beginRow = row;
412     else if (parent.isValid())
413         beginRow = parent.row();
414     else
415         beginRow = rowCount(QModelIndex());
416
417     const VideoMimeData* videoMimeData = qobject_cast<const VideoMimeData*>( data );
418     if(!videoMimeData ) return false;
419
420     QList<Video*> droppedVideos = videoMimeData->videos();
421     foreach( Video *video, droppedVideos) {
422         
423         // remove videos
424         int videoRow = videos.indexOf(video);
425         removeRows(videoRow, 1, QModelIndex());
426         
427         // and then add them again at the new position
428         beginInsertRows(QModelIndex(), beginRow, beginRow);
429         videos.insert(beginRow, video);
430         endInsertRows();
431
432     }
433
434     // fix m_activeRow after all this
435     m_activeRow = videos.indexOf(m_activeVideo);
436
437     // let the MediaView restore the selection
438     emit needSelectionFor(droppedVideos);
439
440     return true;
441
442 }
443
444 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
445     if (videoId.isEmpty()) return -1;
446     for (int i = 0; i < videos.size(); ++i) {
447         Video *v = videos.at(i);
448         // qDebug() << "Comparing" << v->id() << videoId;
449         if (v->id() == videoId) return i;
450     }
451     return -1;
452 }
453
454 int PlaylistModel::rowForVideo(Video* video) {
455     return videos.indexOf(video);
456 }
457
458 QModelIndex PlaylistModel::indexForVideo(Video* video) {
459     return createIndex(videos.indexOf(video), 0);
460 }
461
462 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
463     QList<Video*> movedVideos;
464
465     foreach (const QModelIndex &index, indexes) {
466         int row = index.row();
467         if (row >= videos.size()) continue;
468         // qDebug() << "index row" << row;
469         Video *video = videoAt(row);
470         movedVideos << video;
471     }
472
473     int end=up ? -1 : rowCount()-1, mod=up ? -1 : 1;
474     foreach (Video *video, movedVideos) {
475
476         int row = rowForVideo(video);
477         if (row+mod==end) { end=row; continue; }
478         // qDebug() << "video row" << row;
479         removeRows(row, 1, QModelIndex());
480
481         if (up) row--;
482         else row++;
483
484         beginInsertRows(QModelIndex(), row, row);
485         videos.insert(row, video);
486         endInsertRows();
487
488     }
489
490     emit needSelectionFor(movedVideos);
491
492 }
493
494 /* row hovering */
495
496 void PlaylistModel::setHoveredRow(int row) {
497     int oldRow = hoveredRow;
498     hoveredRow = row;
499     emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
500     emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
501 }
502
503 void PlaylistModel::clearHover() {
504     int oldRow = hoveredRow;
505     hoveredRow = -1;
506     emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1) );
507 }
508
509 void PlaylistModel::updateHoveredRow() {
510     emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
511 }
512
513 /* clickable author */
514
515 void PlaylistModel::enterAuthorHover() {
516     if (authorHovered) return;
517     authorHovered = true;
518     updateHoveredRow();
519 }
520
521 void PlaylistModel::exitAuthorHover() {
522     if (!authorHovered) return;
523     authorHovered = false;
524     updateHoveredRow();
525     setHoveredRow(hoveredRow);
526 }
527
528 void PlaylistModel::enterAuthorPressed() {
529     if (authorPressed) return;
530     authorPressed = true;
531     updateHoveredRow();
532 }
533
534 void PlaylistModel::exitAuthorPressed() {
535     if (!authorPressed) return;
536     authorPressed = false;
537     updateHoveredRow();
538 }