mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-14 02:14:21 +01:00
Allow song ratings to be set by clicking on the stars in the playlist
This commit is contained in:
parent
f706c21be6
commit
bcddb2317a
@ -33,6 +33,8 @@ class LibraryPlaylistItem : public PlaylistItem {
|
||||
|
||||
QUrl Url() const;
|
||||
|
||||
bool IsLocalLibraryItem() const { return song_.id() != -1; }
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(DatabaseColumn column) const;
|
||||
|
||||
|
@ -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<PlaylistItem> item = items_[index.row()];
|
||||
PlaylistItemPtr item = items_[index.row()];
|
||||
Song song = item->Metadata();
|
||||
|
||||
// Don't forget to change Playlist::CompareItems when adding new columns
|
||||
|
@ -94,6 +94,7 @@ class Playlist : public QAbstractListModel {
|
||||
Role_IsPaused,
|
||||
Role_StopAfter,
|
||||
Role_QueuePosition,
|
||||
Role_CanSetRating,
|
||||
};
|
||||
|
||||
static const char* kRowsMimetype;
|
||||
|
@ -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<const QStyleOptionViewItemV3*>(&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 ; i<kStarCount ; ++i, x+=star_size) {
|
||||
const QRect rect(x, option.rect.y(), star_size, star_size);
|
||||
|
||||
if (rating - 0.25 <= i) {
|
||||
// Totally empty
|
||||
painter->setOpacity(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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -70,5 +70,3 @@ static void ReloadPlaylistItem(PlaylistItemPtr item) {
|
||||
QFuture<void> PlaylistItem::BackgroundReload() {
|
||||
return QtConcurrent::run(ReloadPlaylistItem, shared_from_this());
|
||||
}
|
||||
|
||||
|
||||
|
@ -110,6 +110,10 @@ class PlaylistItem : public boost::enable_shared_from_this<PlaylistItem> {
|
||||
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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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_;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user