diff --git a/src/library/libraryplaylistitem.h b/src/library/libraryplaylistitem.h index 2f0e5a8f8..d593aa8a8 100644 --- a/src/library/libraryplaylistitem.h +++ b/src/library/libraryplaylistitem.h @@ -33,6 +33,8 @@ class LibraryPlaylistItem : public PlaylistItem { QUrl Url() const; + bool IsLocalLibraryItem() const { return song_.id() != -1; } + protected: QVariant DatabaseValue(DatabaseColumn column) const; diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index 4f576ac89..da009f0b5 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -178,10 +178,14 @@ QVariant Playlist::data(const QModelIndex& index, int role) const { case Role_QueuePosition: return queue_->PositionOf(index); + case Role_CanSetRating: + return index.column() == Column_Rating && + items_[index.row()]->IsLocalLibraryItem(); + case Qt::EditRole: case Qt::ToolTipRole: case Qt::DisplayRole: { - shared_ptr item = items_[index.row()]; + PlaylistItemPtr item = items_[index.row()]; Song song = item->Metadata(); // Don't forget to change Playlist::CompareItems when adding new columns diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 5160e61d8..0c5260358 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -94,6 +94,7 @@ class Playlist : public QAbstractListModel { Role_IsPaused, Role_StopAfter, Role_QueuePosition, + Role_CanSetRating, }; static const char* kRowsMimetype; diff --git a/src/playlist/playlistdelegates.cpp b/src/playlist/playlistdelegates.cpp index 66c127d7f..91ca5ac15 100644 --- a/src/playlist/playlistdelegates.cpp +++ b/src/playlist/playlistdelegates.cpp @@ -43,6 +43,8 @@ const float QueuedItemDelegate::kQueueOpacityLowerBound = 0.4; const int PlaylistDelegateBase::kMinHeight = 19; const int RatingItemDelegate::kStarCount = 5; // There are 4 stars +const float RatingItemDelegate::kFullOpacity = 1.0; +const float RatingItemDelegate::kEmptyOpacity = 0.5; QueuedItemDelegate::QueuedItemDelegate(QObject *parent, int indicator_column) : QStyledItemDelegate(parent), @@ -294,31 +296,54 @@ RatingItemDelegate::RatingItemDelegate(QObject* parent) { } +QRect RatingItemDelegate::ContentRect(const QRect& total) { + const int width = total.height() * kStarCount; + const int x = total.x() + (total.width() - width) / 2; + + return QRect(x, total.y(), width, total.height()); +} + +double RatingItemDelegate::RatingForPos(const QPoint& pos, const QRect& total_rect) { + const QRect contents = ContentRect(total_rect); + const double raw = double(pos.x() - contents.left()) / contents.width(); + + // Round to the nearest 0.1 + return double(int(raw * kStarCount * 2 + 0.5)) / (kStarCount * 2); +} + void RatingItemDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { - const double rating = index.data().toDouble() * kStarCount; - const int star_size = option.rect.height(); - const int width = star_size * kStarCount; - - const QPixmap empty(star_.pixmap(star_size, QIcon::Disabled)); - const QPixmap full(star_.pixmap(star_size)); - // Draw the background const QStyleOptionViewItemV3* vopt = qstyleoption_cast(&option); vopt->widget->style()->drawPrimitive( QStyle::PE_PanelItemViewItem, vopt, painter, vopt->widget); + // Don't draw anything else if the user can't set the rating of this item + if (!index.data(Playlist::Role_CanSetRating).toBool()) + return; + + const int star_size = option.rect.height(); + const int width = star_size * kStarCount; + const bool hover = mouse_over_index_ == index; + int x = option.rect.x() + (option.rect.width() - width) / 2; + + const double rating = hover ? double(mouse_over_pos_.x() - x) / star_size + : index.data().toDouble() * kStarCount; + + const QPixmap empty(star_.pixmap(star_size, QIcon::Disabled)); + const QPixmap full(star_.pixmap(star_size)); + // Set the clip rect so we don't draw outside the item painter->setClipRect(option.rect); // Draw the stars - int x = option.rect.x() + (option.rect.width() - width) / 2; for (int i=0 ; isetOpacity(kEmptyOpacity); painter->drawPixmap(rect, empty); } else if (rating - 0.75 <= i) { // Half full @@ -326,14 +351,18 @@ void RatingItemDelegate::paint( const QRect target_right(rect.x() + rect.width()/2, rect.y(), rect.width()/2, rect.height()); const QRect source_left(0, 0, empty.width()/2, empty.height()); const QRect source_right(empty.width()/2, 0, empty.width()/2, empty.height()); + painter->setOpacity(kFullOpacity); painter->drawPixmap(target_left, full, source_left); + painter->setOpacity(kEmptyOpacity); painter->drawPixmap(target_right, empty, source_right); } else { // Totally full + painter->setOpacity(kFullOpacity); painter->drawPixmap(rect, full); } } + painter->setOpacity(1.0); painter->setClipping(false); } @@ -349,8 +378,9 @@ QString RatingItemDelegate::displayText( if (value.isNull() || value.toDouble() <= 0) return QString(); - // Round to the nearest .5 - const float rating = float(int(value.toDouble() * kStarCount * 2 + 0.5)) / 2; + // Round to the nearest 0.5 + const double rating = float(int(value.toDouble() * kStarCount * 2 + 0.5)) / 2; + return QString::number(rating, 'f', 1); } diff --git a/src/playlist/playlistdelegates.h b/src/playlist/playlistdelegates.h index febcd0385..5689e1d9a 100644 --- a/src/playlist/playlistdelegates.h +++ b/src/playlist/playlistdelegates.h @@ -111,10 +111,24 @@ public: QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; QString displayText(const QVariant& value, const QLocale& locale) const; + void set_mouse_over(const QModelIndex& index, const QPoint& pos) { + mouse_over_index_ = index ; mouse_over_pos_ = pos; } + void set_mouse_out() { mouse_over_index_ = QModelIndex(); } + bool is_mouse_over() const { return mouse_over_index_.isValid(); } + QModelIndex mouse_over_index() const { return mouse_over_index_; } + + static QRect ContentRect(const QRect& total); + static double RatingForPos(const QPoint& pos, const QRect& total_rect); + static const int kStarCount; + static const float kEmptyOpacity; + static const float kFullOpacity; private: QIcon star_; + + QModelIndex mouse_over_index_; + QPoint mouse_over_pos_; }; class TagCompletionModel : public QStringListModel { diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp index 4810007c3..4ad559667 100644 --- a/src/playlist/playlistitem.cpp +++ b/src/playlist/playlistitem.cpp @@ -70,5 +70,3 @@ static void ReloadPlaylistItem(PlaylistItemPtr item) { QFuture PlaylistItem::BackgroundReload() { return QtConcurrent::run(ReloadPlaylistItem, shared_from_this()); } - - diff --git a/src/playlist/playlistitem.h b/src/playlist/playlistitem.h index c4105d0d4..5b80071f1 100644 --- a/src/playlist/playlistitem.h +++ b/src/playlist/playlistitem.h @@ -110,6 +110,10 @@ class PlaylistItem : public boost::enable_shared_from_this { void ClearTemporaryMetadata(); bool HasTemporaryMetadata() const { return temp_metadata_.is_valid(); } + // Convenience function to find out whether this item is from the local + // library, as opposed to a device, a file on disk, or a stream. + virtual bool IsLocalLibraryItem() const { return false; } + protected: enum DatabaseColumn { Column_LibraryId, diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 2b688ca35..388d0a49e 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -71,6 +71,7 @@ PlaylistView::PlaylistView(QWidget *parent) glow_enabled_(true), currently_glowing_(false), glow_intensity_step_(0), + rating_delegate_(NULL), inhibit_autoscroll_timer_(new QTimer(this)), inhibit_autoscroll_(false), currently_autoscrolling_(false), @@ -83,6 +84,7 @@ PlaylistView::PlaylistView(QWidget *parent) setHeader(header_); header_->setMovable(true); setStyle(style_); + setMouseTracking(true); connect(header_, SIGNAL(sectionResized(int,int,int)), SLOT(SaveGeometry())); connect(header_, SIGNAL(sectionMoved(int,int,int)), SLOT(SaveGeometry())); @@ -107,6 +109,8 @@ PlaylistView::PlaylistView(QWidget *parent) } void PlaylistView::SetItemDelegates(LibraryBackend* backend) { + rating_delegate_ = new RatingItemDelegate(this); + setItemDelegate(new PlaylistDelegateBase(this)); setItemDelegateForColumn(Playlist::Column_Title, new TextItemDelegate(this)); setItemDelegateForColumn(Playlist::Column_Album, @@ -124,7 +128,7 @@ void PlaylistView::SetItemDelegates(LibraryBackend* backend) { setItemDelegateForColumn(Playlist::Column_Samplerate, new PlaylistDelegateBase(this, ("Hz"))); setItemDelegateForColumn(Playlist::Column_Bitrate, new PlaylistDelegateBase(this, tr("kbps"))); setItemDelegateForColumn(Playlist::Column_Filename, new NativeSeparatorsDelegate(this)); - setItemDelegateForColumn(Playlist::Column_Rating, new RatingItemDelegate(this)); + setItemDelegateForColumn(Playlist::Column_Rating, rating_delegate_); setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this)); } @@ -454,8 +458,45 @@ void PlaylistView::closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHi } } -void PlaylistView::mousePressEvent(QMouseEvent *event) { - QTreeView::mousePressEvent(event); +void PlaylistView::mouseMoveEvent(QMouseEvent* event) { + QModelIndex index = indexAt(event->pos()); + if (index.isValid() && index.data(Playlist::Role_CanSetRating).toBool()) { + // Little hack to get hover effects on the rating column + rating_delegate_->set_mouse_over(index, event->pos()); + update(index); + setCursor(Qt::PointingHandCursor); + } else if (rating_delegate_->is_mouse_over()) { + QModelIndex old_index = rating_delegate_->mouse_over_index(); + rating_delegate_->set_mouse_out(); + update(old_index); + setCursor(QCursor()); + } + + QTreeView::mouseMoveEvent(event); +} + +void PlaylistView::leaveEvent(QEvent* e) { + if (rating_delegate_->is_mouse_over()) { + QModelIndex old_index = rating_delegate_->mouse_over_index(); + rating_delegate_->set_mouse_out(); + update(old_index); + setCursor(QCursor()); + } + + QTreeView::leaveEvent(e); +} + +void PlaylistView::mousePressEvent(QMouseEvent* event) { + QModelIndex index = indexAt(event->pos()); + if (index.isValid() && index.data(Playlist::Role_CanSetRating).toBool()) { + // Calculate which star was clicked + double new_rating = RatingItemDelegate::RatingForPos( + event->pos(), visualRect(index)); + emit SongRatingSet(index, new_rating); + } else { + QTreeView::mousePressEvent(event); + } + inhibit_autoscroll_ = true; inhibit_autoscroll_timer_->start(); } diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h index 12540a537..e5a2b8a29 100644 --- a/src/playlist/playlistview.h +++ b/src/playlist/playlistview.h @@ -30,6 +30,7 @@ class QCleanlooksStyle; class LibraryBackend; class PlaylistHeader; class RadioLoadingIndicator; +class RatingItemDelegate; // This proxy style works around a bug/feature introduced in Qt 4.7's QGtkStyle @@ -81,12 +82,15 @@ class PlaylistView : public QTreeView { signals: void PlayPauseItem(const QModelIndex& index); void RightClicked(const QPoint& global_pos, const QModelIndex& index); + void SongRatingSet(const QModelIndex& index, double rating); protected: void hideEvent(QHideEvent* event); void showEvent(QShowEvent* event); - void timerEvent(QTimerEvent *event); - void mousePressEvent(QMouseEvent *event); + void timerEvent(QTimerEvent* event); + void mouseMoveEvent(QMouseEvent* event); + void mousePressEvent(QMouseEvent* event); + void leaveEvent(QEvent*); void scrollContentsBy(int dx, int dy); void paintEvent(QPaintEvent *event); void dragMoveEvent(QDragMoveEvent *event); @@ -134,6 +138,8 @@ class PlaylistView : public QTreeView { QModelIndex last_current_item_; QRect last_glow_rect_; + RatingItemDelegate* rating_delegate_; + QTimer* inhibit_autoscroll_timer_; bool inhibit_autoscroll_; bool currently_autoscrolling_; diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 9b60011ea..f598e7719 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -362,6 +362,7 @@ MainWindow::MainWindow(Engine::Type engine, QWidget *parent) connect(ui_->playlist->view(), SIGNAL(doubleClicked(QModelIndex)), SLOT(PlayIndex(QModelIndex))); connect(ui_->playlist->view(), SIGNAL(PlayPauseItem(QModelIndex)), SLOT(PlayIndex(QModelIndex))); connect(ui_->playlist->view(), SIGNAL(RightClicked(QPoint,QModelIndex)), SLOT(PlaylistRightClick(QPoint,QModelIndex))); + connect(ui_->playlist->view(), SIGNAL(SongRatingSet(QModelIndex,double)), SLOT(PlaylistSongRated(QModelIndex,double))); connect(ui_->track_slider, SIGNAL(ValueChanged(int)), player_, SLOT(Seek(int))); @@ -754,7 +755,7 @@ void MainWindow::MediaPlaying() { void MainWindow::TrackSkipped(PlaylistItemPtr item) { // If it was a library item then we have to increment its skipped count in // the database. - if (item && item->type() == "Library" && item->Metadata().id() != -1) { + if (item && item->IsLocalLibraryItem()) { library_->backend()->IncrementSkipCountAsync(item->Metadata().id()); } } @@ -912,7 +913,7 @@ void MainWindow::UpdateTrackPosition() { playlists_->active()->set_scrobbled(true); // Update the play count for the song if it's from the library - if (item->type() == "Library" && item->Metadata().id() != -1) { + if (item->IsLocalLibraryItem()) { library_->backend()->IncrementPlayCountAsync(item->Metadata().id()); } } @@ -1080,7 +1081,7 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos, const QModelIndex& ui_->action_edit_value->setText(tr("Edit tag \"%1\"...").arg(column_name)); // Is it a library item? - if (playlists_->current()->item_at(source_index.row())->type() == "Library") { + if (playlists_->current()->item_at(source_index.row())->IsLocalLibraryItem()) { playlist_organise_->setVisible(editable); } else { playlist_copy_to_library_->setVisible(editable); @@ -1281,6 +1282,15 @@ void MainWindow::PlaylistEditFinished(const QModelIndex& index) { SelectionSetValue(); } +void MainWindow::PlaylistSongRated(const QModelIndex& index, double rating) { + const QModelIndex source_index = + playlists_->active()->proxy()->mapToSource(index); + PlaylistItemPtr item(playlists_->active()->item_at(source_index.row())); + if (item && item->IsLocalLibraryItem()) { + library_->backend()->UpdateSongRatingAsync(item->Metadata().id(), rating); + } +} + void MainWindow::CommandlineOptionsReceived(const QByteArray& serialized_options) { if (serialized_options == "wake up!") { // Old versions of Clementine sent this - just ignore it diff --git a/src/ui/mainwindow.h b/src/ui/mainwindow.h index 0ed638df5..259ad9b5b 100644 --- a/src/ui/mainwindow.h +++ b/src/ui/mainwindow.h @@ -113,6 +113,7 @@ class MainWindow : public QMainWindow, public PlatformInterface { void PlaylistQueue(); void PlaylistRemoveCurrent(); void PlaylistEditFinished(const QModelIndex& index); + void PlaylistSongRated(const QModelIndex& index, double rating); void EditTracks(); void RenumberTracks(); void SelectionSetValue();