1
0
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:
David Sansome 2010-10-17 21:56:19 +00:00
parent f706c21be6
commit bcddb2317a
11 changed files with 132 additions and 21 deletions

View File

@ -33,6 +33,8 @@ class LibraryPlaylistItem : public PlaylistItem {
QUrl Url() const;
bool IsLocalLibraryItem() const { return song_.id() != -1; }
protected:
QVariant DatabaseValue(DatabaseColumn column) const;

View File

@ -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

View File

@ -94,6 +94,7 @@ class Playlist : public QAbstractListModel {
Role_IsPaused,
Role_StopAfter,
Role_QueuePosition,
Role_CanSetRating,
};
static const char* kRowsMimetype;

View File

@ -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);
}

View File

@ -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 {

View File

@ -70,5 +70,3 @@ static void ReloadPlaylistItem(PlaylistItemPtr item) {
QFuture<void> PlaylistItem::BackgroundReload() {
return QtConcurrent::run(ReloadPlaylistItem, shared_from_this());
}

View File

@ -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,

View File

@ -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();
}

View File

@ -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_;

View File

@ -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

View File

@ -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();