From 4108dc7c730921f7f35e94784140a744c308eac0 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sat, 16 Jan 2010 16:12:47 +0000 Subject: [PATCH] Basic tag editing --- TODO | 4 +- data/data.qrc | 1 + data/edit-track.png | Bin 0 -> 353 bytes src/edittagdialog.cpp | 106 ++++++++++++++++++++ src/edittagdialog.h | 29 ++++++ src/edittagdialog.ui | 206 +++++++++++++++++++++++++++++++++++++++ src/lineedit.cpp | 36 +++++++ src/lineedit.h | 22 +++++ src/mainwindow.cpp | 40 ++++++++ src/mainwindow.h | 3 + src/mainwindow.ui | 37 ++++--- src/playlist.cpp | 7 ++ src/playlist.h | 1 + src/playlistitem.h | 1 + src/song.cpp | 18 +++- src/song.h | 23 ++++- src/songplaylistitem.cpp | 4 + src/songplaylistitem.h | 1 + src/src.pro | 11 ++- 19 files changed, 529 insertions(+), 21 deletions(-) create mode 100644 data/edit-track.png create mode 100644 src/edittagdialog.cpp create mode 100644 src/edittagdialog.h create mode 100644 src/edittagdialog.ui create mode 100644 src/lineedit.cpp create mode 100644 src/lineedit.h diff --git a/TODO b/TODO index 4651128f8..8dbeb84be 100644 --- a/TODO +++ b/TODO @@ -3,11 +3,13 @@ - Nice error messages - Automatically install xine plugins - Copy to library, move to library -- Edit tags in the playlist - Global shortcut keys - Database versioning - Clicking play plays selected item +- Edit tags in playlist view +- Watch subdirectories in library + Long-term: - iPod diff --git a/data/data.qrc b/data/data.qrc index c0fe574a2..d7345da99 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -49,5 +49,6 @@ last.fm/user_purple.png list-remove.png clear-list.png + edit-track.png diff --git a/data/edit-track.png b/data/edit-track.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8872fea61ae20585a422020c907e57dbb364f3 GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fEX7WqAsj$Z!;#Vf4nJ zFqeTaW9`+ZGeAMf64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1U;fk+p1}8$oO4U9J{M*|g*JgY|@JjTC6TDo;<`-reJkZwU>}i{1 z;qd9}_epB;5_S9@p`ivxk`)(ML>Tri%4n{1WpegwjM0!QlxVy2*+%)=B8?^$&m`4# zOV_<-N?tQP#Or3yoL38{-IF&z@tH^GHe3C}!ygNN9lc#?cx&H{-0u^X`4_11t(o?E zfH< literal 0 HcmV?d00001 diff --git a/src/edittagdialog.cpp b/src/edittagdialog.cpp new file mode 100644 index 000000000..97f77af22 --- /dev/null +++ b/src/edittagdialog.cpp @@ -0,0 +1,106 @@ +#include "edittagdialog.h" +#include "ui_edittagdialog.h" + +#include + +EditTagDialog::EditTagDialog(QWidget* parent) + : QDialog(parent) +{ + ui_.setupUi(this); + + static const char* kHintText = "[click to edit]"; + ui_.album->SetHint(kHintText); + ui_.artist->SetHint(kHintText); + ui_.genre->SetHint(kHintText); +} + +bool EditTagDialog::SetSongs(const SongList &s) { + SongList songs; + + foreach (const Song& song, s) { + if (song.IsEditable()) + songs << song; + } + songs_ = songs; + + // Don't allow editing of fields that don't make sense for multiple items + ui_.title->setEnabled(songs.count() == 1); + ui_.track->setEnabled(songs.count() == 1); + ui_.comment->setEnabled(songs.count() == 1); + + if (songs.count() == 0) + return false; + else if (songs.count() == 1) { + const Song& song = songs[0]; + + ui_.title->setText(song.title()); + ui_.artist->setText(song.artist()); + ui_.album->setText(song.album()); + ui_.genre->setText(song.genre()); + ui_.year->setValue(song.year()); + ui_.track->setValue(song.track()); + ui_.comment->setPlainText(song.comment()); + + ui_.filename->setText(song.filename()); + } else { + // Find any fields that are common to all items + + ui_.title->clear(); + ui_.track->clear(); + ui_.comment->clear(); + + QString artist(songs[0].artist()); + QString album(songs[0].album()); + QString genre(songs[0].genre()); + int year = songs[0].year(); + + foreach (const Song& song, songs) { + if (artist != song.artist()) + artist = QString::null; + if (album != song.album()) + album = QString::null; + if (genre != song.genre()) + genre = QString::null; + if (year != song.year()) + year = -1; + } + + ui_.artist->setText(artist); + ui_.album->setText(album); + ui_.genre->setText(genre); + ui_.year->setValue(year); + + ui_.filename->setText("Editing " + QString::number(songs.count()) + " tracks"); + } + + return true; +} + +void EditTagDialog::accept() { + foreach (const Song& old, songs_) { + Song song(old); + + if (ui_.title->isEnabled() && !ui_.title->text().isEmpty()) + song.set_title(ui_.title->text()); + if (ui_.artist->isEnabled() && !ui_.artist->text().isEmpty()) + song.set_artist(ui_.artist->text()); + if (ui_.album->isEnabled() && !ui_.album->text().isEmpty()) + song.set_album(ui_.album->text()); + if (ui_.genre->isEnabled() && !ui_.genre->text().isEmpty()) + song.set_genre(ui_.genre->text()); + + if (ui_.year->isEnabled()) + song.set_year(ui_.year->value()); + if (ui_.track->isEnabled()) + song.set_track(ui_.track->value()); + + if (ui_.comment->isEnabled()) + song.set_comment(ui_.comment->toPlainText()); + + song.Save(); + + emit SongEdited(old, song); + } + + QDialog::accept(); +} diff --git a/src/edittagdialog.h b/src/edittagdialog.h new file mode 100644 index 000000000..60d603c01 --- /dev/null +++ b/src/edittagdialog.h @@ -0,0 +1,29 @@ +#ifndef EDITTAGDIALOG_H +#define EDITTAGDIALOG_H + +#include + +#include "ui_edittagdialog.h" +#include "song.h" + +class EditTagDialog : public QDialog { + Q_OBJECT + + public: + EditTagDialog(QWidget* parent = 0); + + bool SetSongs(const SongList& songs); + + public slots: + void accept(); + + signals: + void SongEdited(const Song& old_song, const Song& new_song); + + private: + Ui::EditTagDialog ui_; + + SongList songs_; +}; + +#endif // EDITTAGDIALOG_H diff --git a/src/edittagdialog.ui b/src/edittagdialog.ui new file mode 100644 index 000000000..afce2915c --- /dev/null +++ b/src/edittagdialog.ui @@ -0,0 +1,206 @@ + + + EditTagDialog + + + + 0 + 0 + 602 + 290 + + + + Edit track information + + + + + + 3 + + + 3 + + + + + Title + + + + + + + + + + Album + + + + + + + + + + Artist + + + + + + + + + + Genre + + + + + + + + + + Track + + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + Year + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + + + Comment + + + + + + + + + + + + + + QLineEdit { + background-color: transparent; +} + + + false + + + true + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + LineEdit + QLineEdit +
lineedit.h
+
+
+ + + + buttonBox + accepted() + EditTagDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditTagDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/lineedit.cpp b/src/lineedit.cpp new file mode 100644 index 000000000..f0c7314eb --- /dev/null +++ b/src/lineedit.cpp @@ -0,0 +1,36 @@ +#include "lineedit.h" + +#include +#include + +LineEdit::LineEdit(QWidget* parent) + : QLineEdit(parent) +{ +} + +void LineEdit::SetHint(const QString& hint) { + hint_ = hint; + update(); +} + +void LineEdit::paintEvent(QPaintEvent* e) { + QLineEdit::paintEvent(e); + + if (!hasFocus() && displayText().isEmpty() && !hint_.isEmpty()) { + QPainter p(this); + + QFont font; + font.setItalic(true); + font.setPointSize(font.pointSize()-1); + + QFontMetrics m(font); + const int kBorder = (height() - m.height()) / 2; + + p.setPen(palette().color(QPalette::Disabled, QPalette::Text)); + p.setFont(font); + + QRect r(rect().topLeft() + QPoint(kBorder + 5, kBorder), + rect().bottomRight() - QPoint(kBorder, kBorder)); + p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, hint_); + } +} diff --git a/src/lineedit.h b/src/lineedit.h new file mode 100644 index 000000000..bbc0c5517 --- /dev/null +++ b/src/lineedit.h @@ -0,0 +1,22 @@ +#ifndef LINEEDIT_H +#define LINEEDIT_H + +#include + +class LineEdit : public QLineEdit { + Q_OBJECT + Q_PROPERTY(QString hint READ GetHint WRITE SetHint); + + public: + LineEdit(QWidget* parent = 0); + + QString GetHint() const { return hint_; } + void SetHint(const QString& hint); + + void paintEvent(QPaintEvent* e); + + private: + QString hint_; +}; + +#endif // LINEEDIT_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index adc4badd5..2b5daf7be 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -10,6 +10,7 @@ #include "lastfmservice.h" #include "osd.h" #include "trackslider.h" +#include "edittagdialog.h" #include "qxtglobalshortcut.h" @@ -34,6 +35,7 @@ MainWindow::MainWindow(QWidget *parent) tray_icon_(new SystemTrayIcon(this)), osd_(new OSD(tray_icon_, this)), track_slider_(new TrackSlider(this)), + edit_tag_dialog_(new EditTagDialog(this)), radio_model_(new RadioModel(this)), playlist_(new Playlist(this)), player_(new Player(playlist_, radio_model_->GetLastFMService(), this)), @@ -84,6 +86,7 @@ MainWindow::MainWindow(QWidget *parent) connect(ui_.action_ban, SIGNAL(triggered()), radio_model_->GetLastFMService(), SLOT(Ban())); connect(ui_.action_love, SIGNAL(triggered()), SLOT(Love())); connect(ui_.action_clear_playlist, SIGNAL(triggered()), playlist_, SLOT(Clear())); + connect(ui_.action_edit_track, SIGNAL(triggered()), SLOT(EditTracks())); // Give actions to buttons ui_.forward_button->setDefaultAction(ui_.action_next_track); @@ -171,6 +174,7 @@ MainWindow::MainWindow(QWidget *parent) playlist_play_pause_ = playlist_menu_->addAction("Play", this, SLOT(PlaylistPlay())); playlist_menu_->addAction(ui_.action_stop); playlist_stop_after_ = playlist_menu_->addAction(QIcon(":media-playback-stop.png"), "Stop after this track", this, SLOT(PlaylistStopAfter())); + playlist_menu_->addAction(ui_.action_edit_track); playlist_menu_->addSeparator(); playlist_menu_->addAction(ui_.action_clear_playlist); @@ -437,6 +441,19 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos, const QModelIndex& playlist_play_pause_->setEnabled(index.isValid()); playlist_stop_after_->setEnabled(index.isValid()); + // Are any of the selected songs editable? + bool editable = false; + foreach (const QModelIndex& index, + ui_.playlist->selectionModel()->selection().indexes()) { + if (index.column() != 0) + continue; + if (playlist_->item_at(index.row())->Metadata().IsEditable()) { + editable = true; + break; + } + } + ui_.action_edit_track->setEnabled(editable); + playlist_menu_->popup(global_pos); } @@ -451,3 +468,26 @@ void MainWindow::PlaylistPlay() { void MainWindow::PlaylistStopAfter() { playlist_->StopAfter(playlist_menu_index_.row()); } + +void MainWindow::EditTracks() { + SongList songs; + QList rows; + + foreach (const QModelIndex& index, + ui_.playlist->selectionModel()->selection().indexes()) { + if (index.column() != 0) + continue; + Song song = playlist_->item_at(index.row())->Metadata(); + + if (song.IsEditable()) { + songs << song; + rows << index.row(); + } + } + + edit_tag_dialog_->SetSongs(songs); + if (edit_tag_dialog_->exec() == QDialog::Rejected) + return; + + playlist_->ReloadItems(rows); +} diff --git a/src/mainwindow.h b/src/mainwindow.h index 582d92bdf..719a92f87 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -15,6 +15,7 @@ class Song; class RadioItem; class OSD; class TrackSlider; +class EditTagDialog; class QSortFilterProxyModel; class SystemTrayIcon; @@ -44,6 +45,7 @@ class MainWindow : public QMainWindow { void PlaylistRightClick(const QPoint& global_pos, const QModelIndex& index); void PlaylistPlay(); void PlaylistStopAfter(); + void EditTracks(); void PlayIndex(const QModelIndex& index); void StopAfterCurrent(); @@ -72,6 +74,7 @@ class MainWindow : public QMainWindow { SystemTrayIcon* tray_icon_; OSD* osd_; TrackSlider* track_slider_; + EditTagDialog* edit_tag_dialog_; RadioModel* radio_model_; Playlist* playlist_; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 40c137098..b92792f7e 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -14,7 +14,7 @@ Clementine - + :/icon.png:/icon.png @@ -294,7 +294,7 @@ - + :/clear.png:/clear.png @@ -314,7 +314,7 @@ - + :/configure.png:/configure.png @@ -430,7 +430,7 @@ - + :/media-skip-backward.png:/media-skip-backward.png @@ -439,7 +439,7 @@ - + :/media-playback-start.png:/media-playback-start.png @@ -451,7 +451,7 @@ false - + :/media-playback-stop.png:/media-playback-stop.png @@ -460,7 +460,7 @@ - + :/media-skip-forward.png:/media-skip-forward.png @@ -469,7 +469,7 @@ - + :/exit.png:/exit.png @@ -481,7 +481,7 @@ - + :/media-playback-stop.png:/media-playback-stop.png @@ -547,7 +547,7 @@ false - + :/last.fm/love.png:/last.fm/love.png @@ -559,7 +559,7 @@ false - + :/last.fm/ban.png:/last.fm/ban.png @@ -568,7 +568,7 @@ - + :/clear-list.png:/clear-list.png @@ -578,6 +578,15 @@ Clear playlist + + + + :/edit-track.png:/edit-track.png + + + Edit track information... + + @@ -614,8 +623,6 @@
radioview.h
- - - + diff --git a/src/playlist.cpp b/src/playlist.cpp index 937cdd10f..5437bbbcf 100644 --- a/src/playlist.cpp +++ b/src/playlist.cpp @@ -502,3 +502,10 @@ void Playlist::Clear() { items_.clear(); reset(); } + +void Playlist::ReloadItems(const QList& rows) { + foreach (int row, rows) { + item_at(row)->Reload(); + emit dataChanged(index(row, 0), index(row, ColumnCount-1)); + } +} diff --git a/src/playlist.h b/src/playlist.h index 7271b4982..bec6f24b1 100644 --- a/src/playlist.h +++ b/src/playlist.h @@ -75,6 +75,7 @@ class Playlist : public QAbstractListModel { QModelIndex InsertRadioStations(const QList& items, int after = -1); QModelIndex InsertPaths(QList urls, int after = -1); void StopAfter(int row); + void ReloadItems(const QList& rows); // QAbstractListModel int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); } diff --git a/src/playlistitem.h b/src/playlistitem.h index f294794ae..ca68cd1b6 100644 --- a/src/playlistitem.h +++ b/src/playlistitem.h @@ -37,6 +37,7 @@ class PlaylistItem { virtual void Save(QSettings& settings) const = 0; virtual void Restore(const QSettings& settings) = 0; + virtual void Reload() {} virtual Song Metadata() const = 0; diff --git a/src/song.cpp b/src/song.cpp index 4759dc86a..813801c1e 100644 --- a/src/song.cpp +++ b/src/song.cpp @@ -72,7 +72,7 @@ void Song::InitFromFile(const QString& filename, int directory_id) { d->filename_ = filename; d->directory_id_ = directory_id; - TagLib::FileRef fileref = TagLib::FileRef(QFile::encodeName(filename).constData()); + TagLib::FileRef fileref(QFile::encodeName(filename).constData()); if( fileref.isNull() ) return; @@ -301,3 +301,19 @@ bool Song::IsMetadataEqual(const Song& other) const { d->bitrate_ == other.d->bitrate_ && d->samplerate_ == other.d->samplerate_; } + +bool Song::Save() const { + if (d->filename_.isNull()) + return false; + + TagLib::FileRef ref(QFile::encodeName(d->filename_).constData()); + ref.tag()->setTitle(d->title_.toUtf8().constData()); + ref.tag()->setArtist(d->artist_.toUtf8().constData()); + ref.tag()->setAlbum(d->album_.toUtf8().constData()); + ref.tag()->setGenre(d->genre_.toUtf8().constData()); + ref.tag()->setComment(d->comment_.toUtf8().constData()); + ref.tag()->setYear(d->year_); + ref.tag()->setTrack(d->track_); + + return ref.save(); +} diff --git a/src/song.h b/src/song.h index 0b85b2b38..06bfd046a 100644 --- a/src/song.h +++ b/src/song.h @@ -92,8 +92,29 @@ class Song { QString PrettyLength() const; // Setters + bool IsEditable() const { return d->valid_ && !d->filename_.isNull(); } + bool Save() const; + void set_id(int id) { d->id_ = id; } - void set_title(const QString& title) { d->title_ = title; } + void set_title(const QString& v) { d->title_ = v; } + + void set_album(const QString& v) { d->album_ = v; } + void set_artist(const QString& v) { d->artist_ = v; } + void set_albumartist(const QString& v) { d->albumartist_ = v; } + void set_composer(const QString& v) { d->composer_ = v; } + void set_track(int v) { d->track_ = v; } + void set_disc(int v) { d->disc_ = v; } + void set_bpm(float v) { d->bpm_ = v; } + void set_year(int v) { d->year_ = v; } + void set_genre(const QString& v) { d->genre_ = v; } + void set_comment(const QString& v) { d->comment_ = v; } + void set_compilation(bool v) { d->compilation_ = v; } + void set_length(int v) { d->length_ = v; } + void set_bitrate(int v) { d->bitrate_ = v; } + void set_samplerate(int v) { d->samplerate_ = v; } + void set_mtime(int v) { d->mtime_ = v; } + void set_ctime(int v) { d->ctime_ = v; } + void set_filesize(int v) { d->filesize_ = v; } // Comparison functions bool IsMetadataEqual(const Song& other) const; diff --git a/src/songplaylistitem.cpp b/src/songplaylistitem.cpp index eec206a66..bf67e953e 100644 --- a/src/songplaylistitem.cpp +++ b/src/songplaylistitem.cpp @@ -29,3 +29,7 @@ QUrl SongPlaylistItem::Url() { ret.setHost("localhost"); return ret; } + +void SongPlaylistItem::Reload() { + song_.InitFromFile(song_.filename(), song_.directory_id()); +} diff --git a/src/songplaylistitem.h b/src/songplaylistitem.h index 0a32b34fc..7a7417829 100644 --- a/src/songplaylistitem.h +++ b/src/songplaylistitem.h @@ -13,6 +13,7 @@ class SongPlaylistItem : public PlaylistItem { void Save(QSettings& settings) const; void Restore(const QSettings& settings); + void Reload(); Song Metadata() const { return song_; } diff --git a/src/src.pro b/src/src.pro index 2f34b3004..9b66afa42 100644 --- a/src/src.pro +++ b/src/src.pro @@ -45,7 +45,9 @@ SOURCES += main.cpp \ radioview.cpp \ lastfmstationdialog.cpp \ osd.cpp \ - trackslider.cpp + trackslider.cpp \ + edittagdialog.cpp \ + lineedit.cpp HEADERS += mainwindow.h \ player.h \ library.h \ @@ -91,14 +93,17 @@ HEADERS += mainwindow.h \ lastfmstationdialog.h \ ../3rdparty/qxt/keymapper_x11.h \ osd.h \ - trackslider.h + trackslider.h \ + edittagdialog.h \ + lineedit.h FORMS += mainwindow.ui \ libraryconfig.ui \ fileview.ui \ lastfmconfig.ui \ radioloadingindicator.ui \ lastfmstationdialog.ui \ - trackslider.ui + trackslider.ui \ + edittagdialog.ui RESOURCES += ../data/data.qrc OTHER_FILES += ../data/schema.sql \ ../data/mainwindow.css