1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "config.h"
19 #include "dynamicplaylistcontrols.h"
20 #include "playlist.h"
21 #include "playlistdelegates.h"
22 #include "playlistheader.h"
23 #include "playlistview.h"
24 #include "core/application.h"
25 #include "core/logging.h"
26 #include "core/player.h"
27 #include "covers/currentartloader.h"
28 #include "ui/qt_blurimage.h"
29 #include "ui/iconloader.h"
30 
31 #include <QCommonStyle>
32 #include <QClipboard>
33 #include <QPainter>
34 #include <QHeaderView>
35 #include <QSettings>
36 #include <QtDebug>
37 #include <QTimer>
38 #include <QKeyEvent>
39 #include <QApplication>
40 #include <QSortFilterProxyModel>
41 #include <QScrollBar>
42 #include <QTimeLine>
43 #include <QMimeData>
44 
45 #include <math.h>
46 #include <algorithm>
47 
48 #ifdef HAVE_MOODBAR
49 #include "moodbar/moodbaritemdelegate.h"
50 #endif
51 
52 const int PlaylistView::kStateVersion = 6;
53 const int PlaylistView::kGlowIntensitySteps = 24;
54 const int PlaylistView::kAutoscrollGraceTimeout = 30;  // seconds
55 const int PlaylistView::kDropIndicatorWidth = 2;
56 const int PlaylistView::kDropIndicatorGradientWidth = 5;
57 const char* PlaylistView::kSettingBackgroundImageType =
58     "playlistview_background_type";
59 const char* PlaylistView::kSettingBackgroundImageFilename =
60     "playlistview_background_image_file";
61 
62 const int PlaylistView::kDefaultBlurRadius = 0;
63 const int PlaylistView::kDefaultOpacityLevel = 40;
64 
PlaylistProxyStyle()65 PlaylistProxyStyle::PlaylistProxyStyle()
66     : QProxyStyle(), common_style_(new QCommonStyle) {}
67 
drawControl(ControlElement element,const QStyleOption * option,QPainter * painter,const QWidget * widget) const68 void PlaylistProxyStyle::drawControl(ControlElement element,
69                                      const QStyleOption* option,
70                                      QPainter* painter,
71                                      const QWidget* widget) const {
72   if (element == CE_Header) {
73     const QStyleOptionHeader* header_option =
74         qstyleoption_cast<const QStyleOptionHeader*>(option);
75     const QRect& rect = header_option->rect;
76     const QString& text = header_option->text;
77     const QFontMetrics& font_metrics = header_option->fontMetrics;
78 
79     // spaces added to make transition less abrupt
80     if (rect.width() < font_metrics.width(text + "  ")) {
81       const Playlist::Column column =
82           static_cast<Playlist::Column>(header_option->section);
83       QStyleOptionHeader new_option(*header_option);
84       new_option.text = Playlist::abbreviated_column_name(column);
85       QProxyStyle::drawControl(element, &new_option, painter, widget);
86       return;
87     }
88   }
89 
90   if (element == CE_ItemViewItem)
91     common_style_->drawControl(element, option, painter, widget);
92   else
93     QProxyStyle::drawControl(element, option, painter, widget);
94 }
95 
drawPrimitive(PrimitiveElement element,const QStyleOption * option,QPainter * painter,const QWidget * widget) const96 void PlaylistProxyStyle::drawPrimitive(PrimitiveElement element,
97                                        const QStyleOption* option,
98                                        QPainter* painter,
99                                        const QWidget* widget) const {
100   if (element == QStyle::PE_PanelItemViewRow ||
101       element == QStyle::PE_PanelItemViewItem)
102     common_style_->drawPrimitive(element, option, painter, widget);
103   else
104     QProxyStyle::drawPrimitive(element, option, painter, widget);
105 }
106 
PlaylistView(QWidget * parent)107 PlaylistView::PlaylistView(QWidget* parent)
108     : QTreeView(parent),
109       app_(nullptr),
110       style_(new PlaylistProxyStyle),
111       playlist_(nullptr),
112       header_(new PlaylistHeader(Qt::Horizontal, this, this)),
113       setting_initial_header_layout_(false),
114       upgrading_from_qheaderview_(false),
115       read_only_settings_(true),
116       upgrading_from_version_(-1),
117       header_loaded_(false),
118       background_initialized_(false),
119       background_image_type_(Default),
120       blur_radius_(kDefaultBlurRadius),
121       opacity_level_(kDefaultOpacityLevel),
122       previous_background_image_opacity_(0.0),
123       fade_animation_(new QTimeLine(1000, this)),
124       last_height_(-1),
125       last_width_(-1),
126       force_background_redraw_(false),
127       glow_enabled_(false),
128       currently_glowing_(false),
129       glow_intensity_step_(0),
130       rating_delegate_(nullptr),
131       inhibit_autoscroll_timer_(new QTimer(this)),
132       inhibit_autoscroll_(false),
133       currently_autoscrolling_(false),
134       row_height_(-1),
135       cached_current_row_row_(-1),
136       drop_indicator_row_(-1),
137       drag_over_(false),
138       dirty_geometry_(false),
139       dirty_settings_(false),
140       dynamic_controls_(new DynamicPlaylistControls(this)) {
141   setHeader(header_);
142   header_->setSectionsMovable(true);
143 #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
144   header_->setFirstSectionMovable(true);
145 #endif
146   setStyle(style_);
147   setMouseTracking(true);
148 
149   QIcon currenttrack_play =
150       IconLoader::Load("currenttrack_play", IconLoader::Other);
151   currenttrack_play_ =
152       currenttrack_play.pixmap(currenttrack_play.actualSize(QSize(32, 32)));
153   QIcon currenttrack_pause =
154       IconLoader::Load("currenttrack_pause", IconLoader::Other);
155   currenttrack_pause_ =
156       currenttrack_pause.pixmap(currenttrack_pause.actualSize(QSize(32, 32)));
157 
158   connect(header_, SIGNAL(sectionResized(int, int, int)),
159           SLOT(DirtyGeometry()));
160   connect(header_, SIGNAL(sectionMoved(int, int, int)), SLOT(DirtyGeometry()));
161   connect(header_, SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
162           SLOT(DirtyGeometry()));
163   connect(header_, SIGNAL(SectionVisibilityChanged(int, bool)),
164           SLOT(DirtyGeometry()));
165   connect(header_, SIGNAL(SectionRatingLockStatusChanged(bool)),
166           SLOT(SetRatingLockStatus(bool)));
167   connect(header_, SIGNAL(sectionResized(int, int, int)),
168           SLOT(InvalidateCachedCurrentPixmap()));
169   connect(header_, SIGNAL(sectionMoved(int, int, int)),
170           SLOT(InvalidateCachedCurrentPixmap()));
171   connect(header_, SIGNAL(SectionVisibilityChanged(int, bool)),
172           SLOT(InvalidateCachedCurrentPixmap()));
173   connect(header_, SIGNAL(StretchEnabledChanged(bool)), SLOT(DirtySettings()));
174   connect(header_, SIGNAL(StretchEnabledChanged(bool)),
175           SLOT(StretchChanged(bool)));
176   connect(header_, SIGNAL(MouseEntered()), SLOT(RatingHoverOut()));
177 
178   inhibit_autoscroll_timer_->setInterval(kAutoscrollGraceTimeout * 1000);
179   inhibit_autoscroll_timer_->setSingleShot(true);
180   connect(inhibit_autoscroll_timer_, SIGNAL(timeout()),
181           SLOT(InhibitAutoscrollTimeout()));
182 
183   horizontalScrollBar()->installEventFilter(this);
184   verticalScrollBar()->installEventFilter(this);
185 
186   setAlternatingRowColors(true);
187 
188   setAttribute(Qt::WA_MacShowFocusRect, false);
189 
190   dynamic_controls_->hide();
191 
192 #ifdef Q_OS_DARWIN
193   setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
194 #endif
195   // For fading
196   connect(fade_animation_, SIGNAL(valueChanged(qreal)),
197           SLOT(FadePreviousBackgroundImage(qreal)));
198   fade_animation_->setDirection(QTimeLine::Backward);  // 1.0 -> 0.0
199 }
200 
~PlaylistView()201 PlaylistView::~PlaylistView() {
202   delete style_;
203 }
204 
SetApplication(Application * app)205 void PlaylistView::SetApplication(Application* app) {
206   Q_ASSERT(app);
207   app_ = app;
208   connect(app_->current_art_loader(),
209           SIGNAL(ArtLoaded(const Song&, const QString&, const QImage&)),
210           SLOT(CurrentSongChanged(const Song&, const QString&, const QImage&)));
211   connect(app_->player(), SIGNAL(Paused()), SLOT(StopGlowing()));
212   connect(app_->player(), SIGNAL(Playing()), SLOT(StartGlowing()));
213   connect(app_->player(), SIGNAL(Stopped()), SLOT(StopGlowing()));
214   connect(app_->player(), SIGNAL(Stopped()), SLOT(PlayerStopped()));
215   connect(app_, SIGNAL(SaveSettings(QSettings*)),
216           SLOT(SaveGeometry(QSettings*)));
217   connect(app_, SIGNAL(SaveSettings(QSettings*)),
218           SLOT(SaveSettings(QSettings*)));
219 }
220 
SetItemDelegates(LibraryBackend * backend)221 void PlaylistView::SetItemDelegates(LibraryBackend* backend) {
222   rating_delegate_ = new RatingItemDelegate(this);
223 
224   setItemDelegate(new PlaylistDelegateBase(this));
225   setItemDelegateForColumn(Playlist::Column_Title, new TextItemDelegate(this));
226   setItemDelegateForColumn(
227       Playlist::Column_Album,
228       new TagCompletionItemDelegate(this, backend, Playlist::Column_Album));
229   setItemDelegateForColumn(
230       Playlist::Column_Artist,
231       new TagCompletionItemDelegate(this, backend, Playlist::Column_Artist));
232   setItemDelegateForColumn(Playlist::Column_AlbumArtist,
233                            new TagCompletionItemDelegate(
234                                this, backend, Playlist::Column_AlbumArtist));
235   setItemDelegateForColumn(
236       Playlist::Column_Genre,
237       new TagCompletionItemDelegate(this, backend, Playlist::Column_Genre));
238   setItemDelegateForColumn(
239       Playlist::Column_Composer,
240       new TagCompletionItemDelegate(this, backend, Playlist::Column_Composer));
241   setItemDelegateForColumn(
242       Playlist::Column_Performer,
243       new TagCompletionItemDelegate(this, backend, Playlist::Column_Performer));
244   setItemDelegateForColumn(
245       Playlist::Column_Grouping,
246       new TagCompletionItemDelegate(this, backend, Playlist::Column_Grouping));
247   setItemDelegateForColumn(Playlist::Column_Length,
248                            new LengthItemDelegate(this));
249   setItemDelegateForColumn(Playlist::Column_Filesize,
250                            new SizeItemDelegate(this));
251   setItemDelegateForColumn(Playlist::Column_Filetype,
252                            new FileTypeItemDelegate(this));
253   setItemDelegateForColumn(Playlist::Column_DateCreated,
254                            new DateItemDelegate(this));
255   setItemDelegateForColumn(Playlist::Column_DateModified,
256                            new DateItemDelegate(this));
257   setItemDelegateForColumn(Playlist::Column_BPM,
258                            new PlaylistDelegateBase(this, tr("bpm")));
259   setItemDelegateForColumn(Playlist::Column_Samplerate,
260                            new PlaylistDelegateBase(this, ("Hz")));
261   setItemDelegateForColumn(Playlist::Column_Bitrate,
262                            new PlaylistDelegateBase(this, tr("kbps")));
263   setItemDelegateForColumn(Playlist::Column_Filename,
264                            new NativeSeparatorsDelegate(this));
265   setItemDelegateForColumn(Playlist::Column_Rating, rating_delegate_);
266   setItemDelegateForColumn(Playlist::Column_LastPlayed,
267                            new LastPlayedItemDelegate(this));
268 
269 #ifdef HAVE_MOODBAR
270   setItemDelegateForColumn(Playlist::Column_Mood,
271                            new MoodbarItemDelegate(app_, this, this));
272 #endif
273 
274   if (app_ && app_->player()) {
275     setItemDelegateForColumn(Playlist::Column_Source,
276                              new SongSourceDelegate(this, app_->player()));
277   } else {
278     header_->HideSection(Playlist::Column_Source);
279   }
280 }
281 
SetPlaylist(Playlist * playlist)282 void PlaylistView::SetPlaylist(Playlist* playlist) {
283   if (playlist_) {
284     disconnect(playlist_, SIGNAL(CurrentSongChanged(Song)), this,
285                SLOT(MaybeAutoscroll()));
286     disconnect(playlist_, SIGNAL(DynamicModeChanged(bool)), this,
287                SLOT(DynamicModeChanged(bool)));
288     disconnect(playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
289     disconnect(playlist_, SIGNAL(QueueChanged()), this, SLOT(update()));
290 
291     disconnect(dynamic_controls_, SIGNAL(Expand()), playlist_,
292                SLOT(ExpandDynamicPlaylist()));
293     disconnect(dynamic_controls_, SIGNAL(Repopulate()), playlist_,
294                SLOT(RepopulateDynamicPlaylist()));
295     disconnect(dynamic_controls_, SIGNAL(TurnOff()), playlist_,
296                SLOT(TurnOffDynamicPlaylist()));
297   }
298 
299   playlist_ = playlist;
300   LoadGeometry();
301   LoadRatingLockStatus();
302   ReloadSettings();
303   DynamicModeChanged(playlist->is_dynamic());
304   setFocus();
305   read_only_settings_ = false;
306   JumpToLastPlayedTrack();
307 
308   connect(playlist_, SIGNAL(RestoreFinished()), SLOT(JumpToLastPlayedTrack()));
309   connect(playlist_, SIGNAL(CurrentSongChanged(Song)), SLOT(MaybeAutoscroll()));
310   connect(playlist_, SIGNAL(DynamicModeChanged(bool)),
311           SLOT(DynamicModeChanged(bool)));
312   connect(playlist_, SIGNAL(destroyed()), SLOT(PlaylistDestroyed()));
313   connect(playlist_, SIGNAL(QueueChanged()), SLOT(update()));
314 
315   connect(dynamic_controls_, SIGNAL(Expand()), playlist_,
316           SLOT(ExpandDynamicPlaylist()));
317   connect(dynamic_controls_, SIGNAL(Repopulate()), playlist_,
318           SLOT(RepopulateDynamicPlaylist()));
319   connect(dynamic_controls_, SIGNAL(TurnOff()), playlist_,
320           SLOT(TurnOffDynamicPlaylist()));
321 }
322 
setModel(QAbstractItemModel * m)323 void PlaylistView::setModel(QAbstractItemModel* m) {
324   if (model()) {
325     disconnect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this,
326                SLOT(InvalidateCachedCurrentPixmap()));
327     disconnect(model(), SIGNAL(layoutAboutToBeChanged()), this,
328                SLOT(RatingHoverOut()));
329     // When changing the model, always invalidate the current pixmap.
330     // If a remote client uses "stop after", without invaliding the stop
331     // mark would not appear.
332     InvalidateCachedCurrentPixmap();
333   }
334 
335   QTreeView::setModel(m);
336 
337   connect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this,
338           SLOT(InvalidateCachedCurrentPixmap()));
339   connect(model(), SIGNAL(layoutAboutToBeChanged()), this,
340           SLOT(RatingHoverOut()));
341 }
342 
LoadGeometry()343 void PlaylistView::LoadGeometry() {
344   QSettings settings;
345   header_loaded_ = true;
346   settings.beginGroup(Playlist::kSettingsGroup);
347 
348   QByteArray state(settings.value("state").toByteArray());
349   if (!header_->RestoreState(state)) {
350     // Maybe we're upgrading from a version that persisted the state with
351     // QHeaderView.
352     if (!header_->restoreState(state)) {
353       header_->HideSection(Playlist::Column_Disc);
354       header_->HideSection(Playlist::Column_Year);
355       header_->HideSection(Playlist::Column_OriginalYear);
356       header_->HideSection(Playlist::Column_Genre);
357       header_->HideSection(Playlist::Column_BPM);
358       header_->HideSection(Playlist::Column_Bitrate);
359       header_->HideSection(Playlist::Column_Samplerate);
360       header_->HideSection(Playlist::Column_Filename);
361       header_->HideSection(Playlist::Column_Filesize);
362       header_->HideSection(Playlist::Column_Filetype);
363       header_->HideSection(Playlist::Column_DateCreated);
364       header_->HideSection(Playlist::Column_DateModified);
365       header_->HideSection(Playlist::Column_AlbumArtist);
366       header_->HideSection(Playlist::Column_Composer);
367       header_->HideSection(Playlist::Column_Performer);
368       header_->HideSection(Playlist::Column_Grouping);
369       header_->HideSection(Playlist::Column_Rating);
370       header_->HideSection(Playlist::Column_PlayCount);
371       header_->HideSection(Playlist::Column_SkipCount);
372       header_->HideSection(Playlist::Column_LastPlayed);
373 
374       header_->moveSection(header_->visualIndex(Playlist::Column_Track), 0);
375       setting_initial_header_layout_ = true;
376     } else {
377       upgrading_from_qheaderview_ = true;
378     }
379   }
380 
381   // New columns that we add are visible by default if the user has upgraded
382   // Clementine.  Hide them again here
383   const int state_version = settings.value("state_version", 0).toInt();
384   upgrading_from_version_ = state_version;
385 
386   if (state_version < 1) {
387     header_->HideSection(Playlist::Column_Rating);
388     header_->HideSection(Playlist::Column_PlayCount);
389     header_->HideSection(Playlist::Column_SkipCount);
390     header_->HideSection(Playlist::Column_LastPlayed);
391   }
392   if (state_version < 2) {
393     header_->HideSection(Playlist::Column_Score);
394   }
395   if (state_version < 3) {
396     header_->HideSection(Playlist::Column_Comment);
397   }
398   if (state_version < 5) {
399     header_->HideSection(Playlist::Column_Mood);
400   }
401   if (state_version < 6) {
402     header_->HideSection(Playlist::Column_Performer);
403     header_->HideSection(Playlist::Column_Grouping);
404   }
405 
406   // Make sure at least one column is visible
407   bool all_hidden = true;
408   for (int i = 0; i < header_->count(); ++i) {
409     if (!header_->isSectionHidden(i) && header_->sectionSize(i) > 0) {
410       all_hidden = false;
411       break;
412     }
413   }
414   if (all_hidden) {
415     header_->ShowSection(Playlist::Column_Title);
416   }
417 }
418 
LoadRatingLockStatus()419 void PlaylistView::LoadRatingLockStatus() {
420   QSettings s;
421   s.beginGroup(Playlist::kSettingsGroup);
422   ratings_locked_ = s.value("RatingLocked", false).toBool();
423 }
424 
DirtyGeometry()425 void PlaylistView::DirtyGeometry() {
426   dirty_geometry_ = true;
427   app_->DirtySettings();
428 }
429 
SaveGeometry(QSettings * settings)430 void PlaylistView::SaveGeometry(QSettings* settings) {
431   if (!dirty_geometry_ || read_only_settings_ || !header_loaded_) return;
432   dirty_geometry_ = false;
433 
434   settings->beginGroup(Playlist::kSettingsGroup);
435   settings->setValue("state", header_->SaveState());
436   settings->setValue("state_version", kStateVersion);
437   settings->endGroup();
438 }
439 
SetRatingLockStatus(bool state)440 void PlaylistView::SetRatingLockStatus(bool state) {
441   if (read_only_settings_) return;
442 
443   ratings_locked_ = state;
444   QSettings s;
445   s.beginGroup(Playlist::kSettingsGroup);
446   s.setValue("RatingLocked", state);
447 }
448 
ReloadBarPixmaps()449 void PlaylistView::ReloadBarPixmaps() {
450   currenttrack_bar_left_ = LoadBarPixmap(":currenttrack_bar_left.png");
451   currenttrack_bar_mid_ = LoadBarPixmap(":currenttrack_bar_mid.png");
452   currenttrack_bar_right_ = LoadBarPixmap(":currenttrack_bar_right.png");
453 }
454 
LoadBarPixmap(const QString & filename)455 QList<QPixmap> PlaylistView::LoadBarPixmap(const QString& filename) {
456   QImage image(filename);
457   image = image.scaledToHeight(row_height_, Qt::SmoothTransformation);
458 
459   // Colour the bar with the palette colour
460   QPainter p(&image);
461   p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
462   p.setOpacity(0.7);
463   p.fillRect(image.rect(), QApplication::palette().color(QPalette::Highlight));
464   p.end();
465 
466   // Animation steps
467   QList<QPixmap> ret;
468   for (int i = 0; i < kGlowIntensitySteps; ++i) {
469     QImage step(image.copy());
470     p.begin(&step);
471     p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
472     p.setOpacity(0.4 - 0.6 * sin(float(i) / kGlowIntensitySteps * (M_PI / 2)));
473     p.fillRect(step.rect(), Qt::white);
474     p.end();
475     ret << QPixmap::fromImage(step);
476   }
477 
478   return ret;
479 }
480 
drawTree(QPainter * painter,const QRegion & region) const481 void PlaylistView::drawTree(QPainter* painter, const QRegion& region) const {
482   const_cast<PlaylistView*>(this)->current_paint_region_ = region;
483   QTreeView::drawTree(painter, region);
484   const_cast<PlaylistView*>(this)->current_paint_region_ = QRegion();
485 }
486 
drawRow(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const487 void PlaylistView::drawRow(QPainter* painter,
488                            const QStyleOptionViewItem& option,
489                            const QModelIndex& index) const {
490   QStyleOptionViewItem opt(option);
491 
492   bool is_current = index.data(Playlist::Role_IsCurrent).toBool();
493   bool is_paused = index.data(Playlist::Role_IsPaused).toBool();
494 
495   if (is_current) {
496     const_cast<PlaylistView*>(this)->last_current_item_ = index;
497     const_cast<PlaylistView*>(this)->last_glow_rect_ = opt.rect;
498 
499     int step = glow_intensity_step_;
500     if (step >= kGlowIntensitySteps)
501       step = 2 * (kGlowIntensitySteps - 1) - step + 1;
502 
503     int row_height = opt.rect.height();
504     if (row_height != row_height_) {
505       // Recreate the pixmaps if the height changed since last time
506       const_cast<PlaylistView*>(this)->row_height_ = row_height;
507       const_cast<PlaylistView*>(this)->ReloadBarPixmaps();
508     }
509 
510     QRect middle(opt.rect);
511     middle.setLeft(middle.left() + currenttrack_bar_left_[0].width());
512     middle.setRight(middle.right() - currenttrack_bar_right_[0].width());
513 
514     // Selection
515     if (selectionModel()->isSelected(index))
516       painter->fillRect(opt.rect, opt.palette.color(QPalette::Highlight));
517 
518     // Draw the bar
519     painter->drawPixmap(opt.rect.topLeft(), currenttrack_bar_left_[step]);
520     painter->drawPixmap(
521         opt.rect.topRight() - currenttrack_bar_right_[0].rect().topRight(),
522         currenttrack_bar_right_[step]);
523     painter->drawPixmap(middle, currenttrack_bar_mid_[step]);
524 
525     // Draw the play icon
526     QPoint play_pos(currenttrack_bar_left_[0].width() / 3 * 2,
527                     (row_height - currenttrack_play_.height()) / 2);
528     painter->drawPixmap(opt.rect.topLeft() + play_pos,
529                         is_paused ? currenttrack_pause_ : currenttrack_play_);
530 
531     // Set the font
532     opt.palette.setColor(QPalette::Inactive, QPalette::HighlightedText,
533                          QApplication::palette().color(
534                              QPalette::Active, QPalette::HighlightedText));
535     opt.palette.setColor(QPalette::Text, QApplication::palette().color(
536                                              QPalette::HighlightedText));
537     opt.palette.setColor(QPalette::Highlight, Qt::transparent);
538     opt.palette.setColor(QPalette::AlternateBase, Qt::transparent);
539     opt.decorationSize = QSize(20, 20);
540 
541     // Draw the actual row data on top.  We cache this, because it's fairly
542     // expensive (1-2ms), and we do it many times per second.
543     const bool cache_dirty = cached_current_row_rect_ != opt.rect ||
544                              cached_current_row_row_ != index.row() ||
545                              cached_current_row_.isNull();
546 
547     // We can't update the cache if we're not drawing the entire region,
548     // QTreeView clips its drawing to only the columns in the region, so it
549     // wouldn't update the whole pixmap properly.
550     const bool whole_region =
551         current_paint_region_.boundingRect().width() == viewport()->width();
552 
553     if (!cache_dirty) {
554       painter->drawPixmap(opt.rect, cached_current_row_);
555     } else {
556       if (whole_region) {
557         const_cast<PlaylistView*>(this)
558             ->UpdateCachedCurrentRowPixmap(opt, index);
559         painter->drawPixmap(opt.rect, cached_current_row_);
560       } else {
561         QTreeView::drawRow(painter, opt, index);
562       }
563     }
564   } else {
565     QTreeView::drawRow(painter, opt, index);
566   }
567 }
568 
UpdateCachedCurrentRowPixmap(QStyleOptionViewItem option,const QModelIndex & index)569 void PlaylistView::UpdateCachedCurrentRowPixmap(QStyleOptionViewItem option,
570                                                 const QModelIndex& index) {
571   cached_current_row_rect_ = option.rect;
572   cached_current_row_row_ = index.row();
573 
574   option.rect.moveTo(0, 0);
575   cached_current_row_ = QPixmap(option.rect.size());
576   cached_current_row_.fill(Qt::transparent);
577 
578   QPainter p(&cached_current_row_);
579   QTreeView::drawRow(&p, option, index);
580 }
581 
InvalidateCachedCurrentPixmap()582 void PlaylistView::InvalidateCachedCurrentPixmap() {
583   cached_current_row_ = QPixmap();
584 }
585 
timerEvent(QTimerEvent * event)586 void PlaylistView::timerEvent(QTimerEvent* event) {
587   QTreeView::timerEvent(event);
588   if (event->timerId() == glow_timer_.timerId()) GlowIntensityChanged();
589 }
590 
GlowIntensityChanged()591 void PlaylistView::GlowIntensityChanged() {
592   glow_intensity_step_ = (glow_intensity_step_ + 1) % (kGlowIntensitySteps * 2);
593 
594   viewport()->update(last_glow_rect_);
595 }
596 
StopGlowing()597 void PlaylistView::StopGlowing() {
598   currently_glowing_ = false;
599   glow_timer_.stop();
600   glow_intensity_step_ = kGlowIntensitySteps;
601 }
602 
StartGlowing()603 void PlaylistView::StartGlowing() {
604   currently_glowing_ = true;
605   if (isVisible() && glow_enabled_)
606     glow_timer_.start(1500 / kGlowIntensitySteps, this);
607 }
608 
hideEvent(QHideEvent *)609 void PlaylistView::hideEvent(QHideEvent*) { glow_timer_.stop(); }
610 
showEvent(QShowEvent *)611 void PlaylistView::showEvent(QShowEvent*) {
612   if (currently_glowing_ && glow_enabled_)
613     glow_timer_.start(1500 / kGlowIntensitySteps, this);
614   MaybeAutoscroll();
615 }
616 
CompareSelectionRanges(const QItemSelectionRange & a,const QItemSelectionRange & b)617 bool CompareSelectionRanges(const QItemSelectionRange& a,
618                             const QItemSelectionRange& b) {
619   return b.bottom() < a.bottom();
620 }
621 
keyPressEvent(QKeyEvent * event)622 void PlaylistView::keyPressEvent(QKeyEvent* event) {
623   if (!model() || state() == QAbstractItemView::EditingState) {
624     QTreeView::keyPressEvent(event);
625   } else if (event == QKeySequence::Delete) {
626     RemoveSelected(false);
627     event->accept();
628 #ifdef Q_OS_DARWIN
629   } else if (event->key() == Qt::Key_Backspace) {
630     RemoveSelected(false);
631     event->accept();
632 #endif
633   } else if (event == QKeySequence::Copy) {
634     CopyCurrentSongToClipboard();
635   } else if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
636     if (currentIndex().isValid()) emit PlayItem(currentIndex());
637     event->accept();
638   } else if (event->modifiers() != Qt::ControlModifier  // Ctrl+Space selects
639                                                         // the item
640              &&
641              event->key() == Qt::Key_Space) {
642     emit PlayPause();
643     event->accept();
644   } else if (event->key() == Qt::Key_Left) {
645     emit SeekBackward();
646     event->accept();
647   } else if (event->key() == Qt::Key_Right) {
648     emit SeekForward();
649     event->accept();
650   } else if (event->modifiers() ==
651                  Qt::NoModifier  // No modifier keys currently pressed...
652                  // ... and key pressed is something related to text
653              &&
654              ((event->key() >= Qt::Key_Exclam && event->key() <= Qt::Key_Z) ||
655               event->key() == Qt::Key_Backspace ||
656               event->key() == Qt::Key_Escape)) {
657     emit FocusOnFilterSignal(event);
658     event->accept();
659   } else {
660     QTreeView::keyPressEvent(event);
661   }
662 }
663 
contextMenuEvent(QContextMenuEvent * e)664 void PlaylistView::contextMenuEvent(QContextMenuEvent* e) {
665   emit RightClicked(e->globalPos(), indexAt(e->pos()));
666   e->accept();
667 }
668 
RemoveSelected(bool deleting_from_disk)669 void PlaylistView::RemoveSelected(bool deleting_from_disk) {
670   int rows_removed = 0;
671   QItemSelection selection(selectionModel()->selection());
672 
673   if (selection.isEmpty()) {
674     return;
675   }
676 
677   // Store the last selected row, which is the last in the list
678   int last_row = selection.last().top();
679 
680   // Sort the selection so we remove the items at the *bottom* first, ensuring
681   // we don't have to mess around with changing row numbers
682   std::sort(selection.begin(), selection.end(), CompareSelectionRanges);
683 
684   for (const QItemSelectionRange& range : selection) {
685     if (range.top() < last_row) rows_removed += range.height();
686 
687     if (!deleting_from_disk) {
688       model()->removeRows(range.top(), range.height(), range.topLeft());
689     } else {
690       model()->removeRows(range.top(), range.height(), QModelIndex());
691     }
692   }
693 
694   int new_row = last_row - rows_removed;
695   // Index of the first column for the row to select
696   QModelIndex new_index = model()->index(new_row, 0);
697 
698   // Select the new current item, we want always the item after the last
699   // selected
700   if (new_index.isValid()) {
701     // Workaround to update keyboard selected row, if it's not the first row
702     // (this also triggers selection)
703     if (new_row != 0)
704       keyPressEvent(
705           new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier));
706     // Update visual selection with the entire row
707     selectionModel()->select(new_index, QItemSelectionModel::ClearAndSelect |
708                                             QItemSelectionModel::Rows);
709   } else {
710     // We're removing the last item, select the new last row
711     selectionModel()->select(
712         model()->index(model()->rowCount() - 1, 0),
713         QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
714   }
715 }
716 
GetEditableColumns()717 QList<int> PlaylistView::GetEditableColumns() {
718   QList<int> columns;
719   QHeaderView* h = header();
720   for (int col = 0; col < h->count(); col++) {
721     if (h->isSectionHidden(col)) continue;
722     QModelIndex index = model()->index(0, col);
723     if (index.flags() & Qt::ItemIsEditable) columns << h->visualIndex(col);
724   }
725   std::sort(columns.begin(), columns.end());
726   return columns;
727 }
728 
NextEditableIndex(const QModelIndex & current)729 QModelIndex PlaylistView::NextEditableIndex(const QModelIndex& current) {
730   QList<int> columns = GetEditableColumns();
731   QHeaderView* h = header();
732   int index = columns.indexOf(h->visualIndex(current.column()));
733 
734   if (index + 1 >= columns.size())
735     return model()->index(current.row() + 1, h->logicalIndex(columns.first()));
736 
737   return model()->index(current.row(), h->logicalIndex(columns[index + 1]));
738 }
739 
PrevEditableIndex(const QModelIndex & current)740 QModelIndex PlaylistView::PrevEditableIndex(const QModelIndex& current) {
741   QList<int> columns = GetEditableColumns();
742   QHeaderView* h = header();
743   int index = columns.indexOf(h->visualIndex(current.column()));
744 
745   if (index - 1 < 0)
746     return model()->index(current.row() - 1, h->logicalIndex(columns.last()));
747 
748   return model()->index(current.row(), h->logicalIndex(columns[index - 1]));
749 }
750 
closeEditor(QWidget * editor,QAbstractItemDelegate::EndEditHint hint)751 void PlaylistView::closeEditor(QWidget* editor,
752                                QAbstractItemDelegate::EndEditHint hint) {
753   if (hint == QAbstractItemDelegate::NoHint) {
754     QTreeView::closeEditor(editor, QAbstractItemDelegate::SubmitModelCache);
755   } else if (hint == QAbstractItemDelegate::EditNextItem ||
756              hint == QAbstractItemDelegate::EditPreviousItem) {
757     QModelIndex index;
758     if (hint == QAbstractItemDelegate::EditNextItem)
759       index = NextEditableIndex(currentIndex());
760     else
761       index = PrevEditableIndex(currentIndex());
762 
763     if (!index.isValid()) {
764       QTreeView::closeEditor(editor, QAbstractItemDelegate::SubmitModelCache);
765     } else {
766       QTreeView::closeEditor(editor, QAbstractItemDelegate::NoHint);
767       setCurrentIndex(index);
768       edit(index);
769     }
770   } else {
771     QTreeView::closeEditor(editor, hint);
772   }
773 }
774 
mouseMoveEvent(QMouseEvent * event)775 void PlaylistView::mouseMoveEvent(QMouseEvent* event) {
776   // Check whether rating section is locked by user or not
777   if (!ratings_locked_) {
778     QModelIndex index = indexAt(event->pos());
779     if (index.isValid() && index.data(Playlist::Role_CanSetRating).toBool()) {
780       RatingHoverIn(index, event->pos());
781     } else if (rating_delegate_->is_mouse_over()) {
782       RatingHoverOut();
783     }
784   }
785   if (!drag_over_) {
786     QTreeView::mouseMoveEvent(event);
787   }
788 }
789 
leaveEvent(QEvent * e)790 void PlaylistView::leaveEvent(QEvent* e) {
791   if (rating_delegate_->is_mouse_over() && !ratings_locked_) {
792     RatingHoverOut();
793   }
794   QTreeView::leaveEvent(e);
795 }
796 
RatingHoverIn(const QModelIndex & index,const QPoint & pos)797 void PlaylistView::RatingHoverIn(const QModelIndex& index, const QPoint& pos) {
798   if (editTriggers() & QAbstractItemView::NoEditTriggers) {
799     return;
800   }
801 
802   const QModelIndex old_index = rating_delegate_->mouse_over_index();
803   rating_delegate_->set_mouse_over(index, selectedIndexes(), pos);
804   setCursor(Qt::PointingHandCursor);
805 
806   update(index);
807   update(old_index);
808   for (const QModelIndex& index : selectedIndexes()) {
809     if (index.column() == Playlist::Column_Rating) {
810       update(index);
811     }
812   }
813 
814   if (index.data(Playlist::Role_IsCurrent).toBool() ||
815       old_index.data(Playlist::Role_IsCurrent).toBool()) {
816     InvalidateCachedCurrentPixmap();
817   }
818 }
819 
RatingHoverOut()820 void PlaylistView::RatingHoverOut() {
821   if (editTriggers() & QAbstractItemView::NoEditTriggers) {
822     return;
823   }
824 
825   const QModelIndex old_index = rating_delegate_->mouse_over_index();
826   rating_delegate_->set_mouse_out();
827   setCursor(QCursor());
828 
829   update(old_index);
830   for (const QModelIndex& index : selectedIndexes()) {
831     if (index.column() == Playlist::Column_Rating) {
832       update(index);
833     }
834   }
835 
836   if (old_index.data(Playlist::Role_IsCurrent).toBool()) {
837     InvalidateCachedCurrentPixmap();
838   }
839 }
840 
mousePressEvent(QMouseEvent * event)841 void PlaylistView::mousePressEvent(QMouseEvent* event) {
842   if (editTriggers() & QAbstractItemView::NoEditTriggers) {
843     QTreeView::mousePressEvent(event);
844     return;
845   }
846 
847   QModelIndex index = indexAt(event->pos());
848   if (event->button() == Qt::LeftButton && index.isValid() &&
849       index.data(Playlist::Role_CanSetRating).toBool() && !ratings_locked_) {
850     // Calculate which star was clicked
851     double new_rating =
852         RatingPainter::RatingForPos(event->pos(), visualRect(index));
853 
854     if (selectedIndexes().contains(index)) {
855       // Update all the selected item ratings
856       QModelIndexList src_index_list;
857       for (const QModelIndex& index : selectedIndexes()) {
858         if (index.data(Playlist::Role_CanSetRating).toBool()) {
859           QModelIndex src_index = playlist_->proxy()->mapToSource(index);
860           src_index_list << src_index;
861         }
862       }
863       playlist_->RateSongs(src_index_list, new_rating);
864     } else {
865       // Update only this item rating
866       playlist_->RateSong(playlist_->proxy()->mapToSource(index), new_rating);
867     }
868   } else if (event->button() == Qt::XButton1 && index.isValid()) {
869     app_->player()->Previous();
870   } else if (event->button() == Qt::XButton2 && index.isValid()) {
871     app_->player()->Next();
872   } else {
873     QTreeView::mousePressEvent(event);
874   }
875 
876   inhibit_autoscroll_ = true;
877   inhibit_autoscroll_timer_->start();
878 }
879 
scrollContentsBy(int dx,int dy)880 void PlaylistView::scrollContentsBy(int dx, int dy) {
881   if (dx) {
882     InvalidateCachedCurrentPixmap();
883   }
884   cached_tree_ = QPixmap();
885 
886   QTreeView::scrollContentsBy(dx, dy);
887 
888   if (!currently_autoscrolling_) {
889     // We only want to do this if the scroll was initiated by the user
890     inhibit_autoscroll_ = true;
891     inhibit_autoscroll_timer_->start();
892   }
893 }
894 
InhibitAutoscrollTimeout()895 void PlaylistView::InhibitAutoscrollTimeout() {
896   // For 30 seconds after the user clicks on or scrolls the playlist we promise
897   // not to automatically scroll the view to keep up with a track change.
898   inhibit_autoscroll_ = false;
899 }
900 
MaybeAutoscroll()901 void PlaylistView::MaybeAutoscroll() {
902   if (!inhibit_autoscroll_) JumpToCurrentlyPlayingTrack();
903 }
904 
JumpToCurrentlyPlayingTrack()905 void PlaylistView::JumpToCurrentlyPlayingTrack() {
906   Q_ASSERT(playlist_);
907 
908   // Usage of the "Jump to the currently playing track" action shall enable
909   // autoscroll
910   inhibit_autoscroll_ = false;
911 
912   if (playlist_->current_row() == -1) return;
913 
914   QModelIndex current = playlist_->proxy()->mapFromSource(
915       playlist_->index(playlist_->current_row(), 0));
916   if (!current.isValid()) return;
917 
918   currently_autoscrolling_ = true;
919 
920   // Scroll to the item
921   scrollTo(current, QAbstractItemView::PositionAtCenter);
922 
923   currently_autoscrolling_ = false;
924 }
925 
JumpToLastPlayedTrack()926 void PlaylistView::JumpToLastPlayedTrack() {
927   Q_ASSERT(playlist_);
928 
929   if (playlist_->last_played_row() == -1) return;
930 
931   QModelIndex last_played = playlist_->proxy()->mapFromSource(
932       playlist_->index(playlist_->last_played_row(), 0));
933   if (!last_played.isValid()) return;
934 
935   // Select last played song
936   last_current_item_ = last_played;
937   setCurrentIndex(last_current_item_);
938 
939   currently_autoscrolling_ = true;
940 
941   // Scroll to the item
942   scrollTo(last_played, QAbstractItemView::PositionAtCenter);
943 
944   currently_autoscrolling_ = false;
945 }
946 
paintEvent(QPaintEvent * event)947 void PlaylistView::paintEvent(QPaintEvent* event) {
948   // Reimplemented to draw the background image.
949   // Reimplemented also to draw the drop indicator
950   // When the user is dragging some stuff over the playlist paintEvent gets
951   // called for the entire viewport every time the user moves the mouse.
952   // The drawTree is kinda expensive, so we cache the result and draw from the
953   // cache while the user is dragging.  The cached pixmap gets invalidated in
954   // dragLeaveEvent, dropEvent and scrollContentsBy.
955 
956   // Draw background
957   if (background_image_type_ == Custom ||
958       background_image_type_ == AlbumCover) {
959     if (!background_image_.isNull() || !previous_background_image_.isNull()) {
960       QPainter background_painter(viewport());
961 
962       // Check if we should recompute the background image
963       if (height() != last_height_ || width() != last_width_ ||
964           force_background_redraw_) {
965         if (background_image_.isNull()) {
966           cached_scaled_background_image_ = QPixmap();
967         } else {
968           cached_scaled_background_image_ =
969               QPixmap::fromImage(background_image_.scaled(
970                   width(), height(), Qt::KeepAspectRatioByExpanding,
971                   Qt::SmoothTransformation));
972         }
973 
974         last_height_ = height();
975         last_width_ = width();
976         force_background_redraw_ = false;
977       }
978 
979       // Actually draw the background image
980       if (!cached_scaled_background_image_.isNull()) {
981         // Set opactiy only if needed, as this deactivate hardware acceleration
982         if (!qFuzzyCompare(previous_background_image_opacity_, qreal(0.0))) {
983           background_painter.setOpacity(1.0 -
984                                         previous_background_image_opacity_);
985         }
986         background_painter.drawPixmap(
987             (width() - cached_scaled_background_image_.width()) / 2,
988             (height() - cached_scaled_background_image_.height()) / 2,
989             cached_scaled_background_image_);
990       }
991       // Draw the previous background image if we're fading
992       if (!previous_background_image_.isNull()) {
993         background_painter.setOpacity(previous_background_image_opacity_);
994         background_painter.drawPixmap(
995             (width() - previous_background_image_.width()) / 2,
996             (height() - previous_background_image_.height()) / 2,
997             previous_background_image_);
998       }
999     }
1000   }
1001 
1002   QPainter p(viewport());
1003 
1004   if (drop_indicator_row_ != -1) {
1005     if (cached_tree_.isNull()) {
1006       cached_tree_ = QPixmap(size());
1007       cached_tree_.fill(Qt::transparent);
1008 
1009       QPainter cache_painter(&cached_tree_);
1010       drawTree(&cache_painter, event->region());
1011     }
1012 
1013     p.drawPixmap(0, 0, cached_tree_);
1014   } else {
1015     drawTree(&p, event->region());
1016     return;
1017   }
1018 
1019   const int first_column = header_->logicalIndex(0);
1020 
1021   // Find the y position of the drop indicator
1022   QModelIndex drop_index = model()->index(drop_indicator_row_, first_column);
1023   int drop_pos = -1;
1024   switch (dropIndicatorPosition()) {
1025     case QAbstractItemView::OnItem:
1026       return;  // Don't draw anything
1027 
1028     case QAbstractItemView::AboveItem:
1029       drop_pos = visualRect(drop_index).top();
1030       break;
1031 
1032     case QAbstractItemView::BelowItem:
1033       drop_pos = visualRect(drop_index).bottom() + 1;
1034       break;
1035 
1036     case QAbstractItemView::OnViewport:
1037       if (model()->rowCount() == 0)
1038         drop_pos = 1;
1039       else
1040         drop_pos = 1 +
1041                    visualRect(model()->index(model()->rowCount() - 1,
1042                                              first_column)).bottom();
1043       break;
1044   }
1045 
1046   // Draw a nice gradient first
1047   QColor line_color(QApplication::palette().color(QPalette::Highlight));
1048   QColor shadow_color(line_color.lighter(140));
1049   QColor shadow_fadeout_color(shadow_color);
1050   shadow_color.setAlpha(255);
1051   shadow_fadeout_color.setAlpha(0);
1052 
1053   QLinearGradient gradient(QPoint(0, drop_pos - kDropIndicatorGradientWidth),
1054                            QPoint(0, drop_pos + kDropIndicatorGradientWidth));
1055   gradient.setColorAt(0.0, shadow_fadeout_color);
1056   gradient.setColorAt(0.5, shadow_color);
1057   gradient.setColorAt(1.0, shadow_fadeout_color);
1058   QPen gradient_pen(QBrush(gradient), kDropIndicatorGradientWidth * 2);
1059   p.setPen(gradient_pen);
1060   p.drawLine(QPoint(0, drop_pos), QPoint(width(), drop_pos));
1061 
1062   // Now draw the line on top
1063   QPen line_pen(line_color, kDropIndicatorWidth);
1064   p.setPen(line_pen);
1065   p.drawLine(QPoint(0, drop_pos), QPoint(width(), drop_pos));
1066 }
1067 
dragMoveEvent(QDragMoveEvent * event)1068 void PlaylistView::dragMoveEvent(QDragMoveEvent* event) {
1069   QTreeView::dragMoveEvent(event);
1070 
1071   QModelIndex index(indexAt(event->pos()));
1072   drop_indicator_row_ = index.isValid() ? index.row() : 0;
1073 }
1074 
dragEnterEvent(QDragEnterEvent * event)1075 void PlaylistView::dragEnterEvent(QDragEnterEvent* event) {
1076   QTreeView::dragEnterEvent(event);
1077   cached_tree_ = QPixmap();
1078   drag_over_ = true;
1079 }
1080 
dragLeaveEvent(QDragLeaveEvent * event)1081 void PlaylistView::dragLeaveEvent(QDragLeaveEvent* event) {
1082   QTreeView::dragLeaveEvent(event);
1083   cached_tree_ = QPixmap();
1084   drag_over_ = false;
1085   drop_indicator_row_ = -1;
1086 }
1087 
dropEvent(QDropEvent * event)1088 void PlaylistView::dropEvent(QDropEvent* event) {
1089   QTreeView::dropEvent(event);
1090   cached_tree_ = QPixmap();
1091   drop_indicator_row_ = -1;
1092   drag_over_ = false;
1093 }
1094 
PlaylistDestroyed()1095 void PlaylistView::PlaylistDestroyed() {
1096   playlist_ = nullptr;
1097   // We'll get a SetPlaylist() soon
1098 }
1099 
ReloadSettings()1100 void PlaylistView::ReloadSettings() {
1101   QSettings s;
1102   s.beginGroup(Playlist::kSettingsGroup);
1103   glow_enabled_ = s.value("glow_effect", false).toBool();
1104 
1105   if (setting_initial_header_layout_ || upgrading_from_qheaderview_) {
1106     header_->SetStretchEnabled(s.value("stretch", true).toBool());
1107     upgrading_from_qheaderview_ = false;
1108   }
1109 
1110   if (currently_glowing_ && glow_enabled_ && isVisible()) StartGlowing();
1111   if (!glow_enabled_) StopGlowing();
1112 
1113   if (setting_initial_header_layout_) {
1114     header_->SetColumnWidth(Playlist::Column_Length, 0.06);
1115     header_->SetColumnWidth(Playlist::Column_Track, 0.05);
1116     setting_initial_header_layout_ = false;
1117   }
1118 
1119   if (upgrading_from_version_ != -1) {
1120     if (upgrading_from_version_ < 4) {
1121       header_->SetColumnWidth(Playlist::Column_Source, 0.05);
1122     }
1123     upgrading_from_version_ = -1;
1124   }
1125 
1126   column_alignment_ = s.value("column_alignments").value<ColumnAlignmentMap>();
1127   if (column_alignment_.isEmpty()) {
1128     column_alignment_ = DefaultColumnAlignment();
1129   }
1130 
1131   emit ColumnAlignmentChanged(column_alignment_);
1132 
1133   // Background:
1134   QVariant q_playlistview_background_type =
1135       s.value(kSettingBackgroundImageType);
1136   BackgroundImageType background_type(Default);
1137   // bg_enabled should also be checked for backward compatibility (in releases
1138   // <= 1.0, there was just a boolean to activate/deactivate the background)
1139   QVariant bg_enabled = s.value("bg_enabled");
1140   if (q_playlistview_background_type.isValid()) {
1141     background_type = static_cast<BackgroundImageType>(
1142         q_playlistview_background_type.toInt());
1143   } else if (bg_enabled.isValid()) {
1144     if (bg_enabled.toBool()) {
1145       background_type = Default;
1146     } else {
1147       background_type = None;
1148     }
1149   }
1150   QString background_image_filename =
1151       s.value(kSettingBackgroundImageFilename).toString();
1152   int blur_radius = s.value("blur_radius", kDefaultBlurRadius).toInt();
1153   int opacity_level = s.value("opacity_level", kDefaultOpacityLevel).toInt();
1154   // Check if background properties have changed.
1155   // We change properties only if they have actually changed, to avoid to call
1156   // set_background_image when it is not needed, as this will cause the fading
1157   // animation to start again. This also avoid to do useless
1158   // "force_background_redraw".
1159   if (!background_initialized_ ||
1160       background_image_filename != background_image_filename_ ||
1161       background_type != background_image_type_ ||
1162       blur_radius_ != blur_radius || opacity_level_ != opacity_level) {
1163     // Store background properties
1164     background_initialized_ = true;
1165     background_image_type_ = background_type;
1166     background_image_filename_ = background_image_filename;
1167     blur_radius_ = blur_radius;
1168     opacity_level_ = opacity_level;
1169     if (background_image_type_ == Custom) {
1170       set_background_image(QImage(background_image_filename));
1171     } else if (background_image_type_ == AlbumCover) {
1172       set_background_image(current_song_cover_art_);
1173     } else {
1174       // User changed background image type to something that will not be
1175       // painted through paintEvent: reset all background images.
1176       // This avoid to use old (deprecated) images for fading when selecting
1177       // AlbumCover or Custom background image type later.
1178       set_background_image(QImage());
1179       cached_scaled_background_image_ = QPixmap();
1180       previous_background_image_ = QPixmap();
1181     }
1182     setProperty("default_background_enabled",
1183                 background_image_type_ == Default);
1184     emit BackgroundPropertyChanged();
1185     force_background_redraw_ = true;
1186   }
1187 
1188   if (!s.value("click_edit_inline", true).toBool())
1189     setEditTriggers(editTriggers() & ~QAbstractItemView::SelectedClicked);
1190   else
1191     setEditTriggers(editTriggers() | QAbstractItemView::SelectedClicked);
1192 }
1193 
DirtySettings()1194 void PlaylistView::DirtySettings() {
1195   dirty_settings_ = true;
1196   app_->DirtySettings();
1197 }
1198 
SaveSettings(QSettings * settings)1199 void PlaylistView::SaveSettings(QSettings* settings) {
1200   if (!dirty_settings_ || read_only_settings_) return;
1201   dirty_settings_ = false;
1202 
1203   settings->beginGroup(Playlist::kSettingsGroup);
1204   settings->setValue("glow_effect", glow_enabled_);
1205   settings->setValue("column_alignments",
1206                      QVariant::fromValue(column_alignment_));
1207   settings->setValue(kSettingBackgroundImageType, background_image_type_);
1208   settings->endGroup();
1209 }
1210 
StretchChanged(bool stretch)1211 void PlaylistView::StretchChanged(bool stretch) {
1212   setHorizontalScrollBarPolicy(stretch ? Qt::ScrollBarAlwaysOff
1213                                        : Qt::ScrollBarAsNeeded);
1214   dirty_geometry_ = true;
1215   app_->DirtySettings();
1216 }
1217 
DynamicModeChanged(bool dynamic)1218 void PlaylistView::DynamicModeChanged(bool dynamic) {
1219   if (!dynamic) {
1220     dynamic_controls_->hide();
1221   } else {
1222     RepositionDynamicControls();
1223     dynamic_controls_->show();
1224   }
1225 }
1226 
resizeEvent(QResizeEvent * e)1227 void PlaylistView::resizeEvent(QResizeEvent* e) {
1228   QTreeView::resizeEvent(e);
1229   if (dynamic_controls_->isVisible()) {
1230     RepositionDynamicControls();
1231   }
1232 }
1233 
RepositionDynamicControls()1234 void PlaylistView::RepositionDynamicControls() {
1235   dynamic_controls_->resize(dynamic_controls_->sizeHint());
1236   dynamic_controls_->move((width() - dynamic_controls_->width()) / 2,
1237                           height() - dynamic_controls_->height() - 20);
1238 }
1239 
eventFilter(QObject * object,QEvent * event)1240 bool PlaylistView::eventFilter(QObject* object, QEvent* event) {
1241   if (event->type() == QEvent::Enter &&
1242       (object == horizontalScrollBar() || object == verticalScrollBar())) {
1243     RatingHoverOut();
1244     return false;
1245   }
1246   return QObject::eventFilter(object, event);
1247 }
1248 
rowsInserted(const QModelIndex & parent,int start,int end)1249 void PlaylistView::rowsInserted(const QModelIndex& parent, int start, int end) {
1250   const bool at_end = end == model()->rowCount(parent) - 1;
1251 
1252   QTreeView::rowsInserted(parent, start, end);
1253 
1254   if (at_end) {
1255     // If the rows were inserted at the end of the playlist then let's scroll
1256     // the view so the user can see.
1257     scrollTo(model()->index(start, 0, parent),
1258              QAbstractItemView::PositionAtTop);
1259   }
1260 }
1261 
DefaultColumnAlignment()1262 ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
1263   ColumnAlignmentMap ret;
1264 
1265   ret[Playlist::Column_Length] = ret[Playlist::Column_Track] =
1266       ret[Playlist::Column_Disc] = ret[Playlist::Column_Year] =
1267           ret[Playlist::Column_BPM] = ret[Playlist::Column_Bitrate] =
1268               ret[Playlist::Column_Samplerate] =
1269                   ret[Playlist::Column_Filesize] =
1270                       ret[Playlist::Column_PlayCount] =
1271                           ret[Playlist::Column_SkipCount] =
1272                               ret[Playlist::Column_OriginalYear] =
1273                                   (Qt::AlignRight | Qt::AlignVCenter);
1274   ret[Playlist::Column_Score] = (Qt::AlignCenter);
1275 
1276   return ret;
1277 }
1278 
SetColumnAlignment(int section,Qt::Alignment alignment)1279 void PlaylistView::SetColumnAlignment(int section, Qt::Alignment alignment) {
1280   if (section < 0) return;
1281 
1282   column_alignment_[section] = alignment;
1283   emit ColumnAlignmentChanged(column_alignment_);
1284   dirty_settings_ = true;
1285   app_->DirtySettings();
1286 }
1287 
column_alignment(int section) const1288 Qt::Alignment PlaylistView::column_alignment(int section) const {
1289   return column_alignment_.value(section, Qt::AlignLeft | Qt::AlignVCenter);
1290 }
1291 
CopyCurrentSongToClipboard() const1292 void PlaylistView::CopyCurrentSongToClipboard() const {
1293   // Get the display text of all visible columns.
1294   QStringList columns;
1295 
1296   for (int i = 0; i < header()->count(); ++i) {
1297     if (header()->isSectionHidden(i)) {
1298       continue;
1299     }
1300 
1301     const QVariant data =
1302         model()->data(currentIndex().sibling(currentIndex().row(), i));
1303     if (data.type() == QVariant::String) {
1304       columns << data.toString();
1305     }
1306   }
1307 
1308   // Get the song's URL
1309   const QUrl url = model()
1310                        ->data(currentIndex().sibling(currentIndex().row(),
1311                                                      Playlist::Column_Filename))
1312                        .toUrl();
1313 
1314   QMimeData* mime_data = new QMimeData;
1315   mime_data->setUrls(QList<QUrl>() << url);
1316   mime_data->setText(columns.join(" - "));
1317 
1318   QApplication::clipboard()->setMimeData(mime_data);
1319 }
1320 
CurrentSongChanged(const Song & song,const QString & uri,const QImage & song_art)1321 void PlaylistView::CurrentSongChanged(const Song& song, const QString& uri,
1322                                       const QImage& song_art) {
1323   if (current_song_cover_art_ == song_art) return;
1324 
1325   current_song_cover_art_ = song_art;
1326   if (background_image_type_ == AlbumCover) {
1327     if (song.art_automatic().isEmpty() && song.art_manual().isEmpty()) {
1328       set_background_image(QImage());
1329     } else {
1330       set_background_image(current_song_cover_art_);
1331     }
1332     force_background_redraw_ = true;
1333     update();
1334   }
1335 }
1336 
set_background_image(const QImage & image)1337 void PlaylistView::set_background_image(const QImage& image) {
1338   // Save previous image, for fading
1339   previous_background_image_ = cached_scaled_background_image_;
1340 
1341   if (image.isNull() || image.format() == QImage::Format_ARGB32) {
1342     background_image_ = image;
1343   } else {
1344     background_image_ = image.convertToFormat(QImage::Format_ARGB32);
1345   }
1346 
1347   if (!background_image_.isNull()) {
1348     // Apply opacity filter
1349     uchar* bits = background_image_.bits();
1350     for (int i = 0;
1351          i < background_image_.height() * background_image_.bytesPerLine();
1352          i += 4) {
1353       bits[i + 3] = (opacity_level_ / 100.0) * 255;
1354     }
1355 
1356     if (blur_radius_ != 0) {
1357       QImage blurred(background_image_.size(),
1358                      QImage::Format_ARGB32_Premultiplied);
1359       blurred.fill(Qt::transparent);
1360       QPainter blur_painter(&blurred);
1361       qt_blurImage(&blur_painter, background_image_, blur_radius_, true, false);
1362       blur_painter.end();
1363 
1364       background_image_ = blurred;
1365     }
1366   }
1367 
1368   if (isVisible()) {
1369     previous_background_image_opacity_ = 1.0;
1370     fade_animation_->start();
1371   }
1372 }
1373 
FadePreviousBackgroundImage(qreal value)1374 void PlaylistView::FadePreviousBackgroundImage(qreal value) {
1375   previous_background_image_opacity_ = value;
1376   if (qFuzzyCompare(previous_background_image_opacity_, qreal(0.0))) {
1377     previous_background_image_ = QPixmap();
1378     previous_background_image_opacity_ = 0.0;
1379   }
1380 
1381   update();
1382 }
1383 
PlayerStopped()1384 void PlaylistView::PlayerStopped() {
1385   CurrentSongChanged(Song(), QString(), QImage());
1386 }
1387 
focusInEvent(QFocusEvent * event)1388 void PlaylistView::focusInEvent(QFocusEvent* event) {
1389   QTreeView::focusInEvent(event);
1390 
1391   if (event->reason() == Qt::TabFocusReason ||
1392       event->reason() == Qt::BacktabFocusReason) {
1393     // If there's a current item but no selection it probably means the list was
1394     // filtered, and the selected item does not match the filter.  If there's
1395     // only 1 item in the view it is now impossible to select that item without
1396     // using the mouse.
1397     const QModelIndex& current = selectionModel()->currentIndex();
1398     if (current.isValid() && selectionModel()->selectedIndexes().isEmpty()) {
1399       QItemSelection new_selection(
1400           current.sibling(current.row(), 0),
1401           current.sibling(current.row(),
1402                           current.model()->columnCount(current.parent()) - 1));
1403       selectionModel()->select(new_selection, QItemSelectionModel::Select);
1404     }
1405   }
1406 }
1407