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