Basic tag editing

This commit is contained in:
David Sansome 2010-01-16 16:12:47 +00:00
parent e0bb24af07
commit 4108dc7c73
19 changed files with 529 additions and 21 deletions

4
TODO
View File

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

View File

@ -49,5 +49,6 @@
<file>last.fm/user_purple.png</file>
<file>list-remove.png</file>
<file>clear-list.png</file>
<file>edit-track.png</file>
</qresource>
</RCC>

BIN
data/edit-track.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

106
src/edittagdialog.cpp Normal file
View File

@ -0,0 +1,106 @@
#include "edittagdialog.h"
#include "ui_edittagdialog.h"
#include <QtDebug>
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();
}

29
src/edittagdialog.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef EDITTAGDIALOG_H
#define EDITTAGDIALOG_H
#include <QDialog>
#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

206
src/edittagdialog.ui Normal file
View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditTagDialog</class>
<widget class="QDialog" name="EditTagDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>602</width>
<height>290</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit track information</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<property name="horizontalSpacing">
<number>3</number>
</property>
<property name="verticalSpacing">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="title"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Album</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEdit" name="album"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Artist</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="LineEdit" name="artist"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Genre</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LineEdit" name="genre"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Track</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="track">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Year</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="year">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Comment</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPlainTextEdit" name="comment"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="filename">
<property name="styleSheet">
<string notr="true">QLineEdit {
background-color: transparent;
}</string>
</property>
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEdit</class>
<extends>QLineEdit</extends>
<header>lineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditTagDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditTagDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

36
src/lineedit.cpp Normal file
View File

@ -0,0 +1,36 @@
#include "lineedit.h"
#include <QPainter>
#include <QPaintEvent>
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_);
}
}

22
src/lineedit.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef LINEEDIT_H
#define LINEEDIT_H
#include <QLineEdit>
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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
<string>Clementine</string>
</property>
<property name="windowIcon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
@ -294,7 +294,7 @@
<item>
<widget class="QToolButton" name="library_filter_clear">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/clear.png</normaloff>:/clear.png</iconset>
</property>
<property name="iconSize">
@ -314,7 +314,7 @@
<item>
<widget class="QToolButton" name="library_options">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/configure.png</normaloff>:/configure.png</iconset>
</property>
<property name="iconSize">
@ -430,7 +430,7 @@
</widget>
<action name="action_previous_track">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/media-skip-backward.png</normaloff>:/media-skip-backward.png</iconset>
</property>
<property name="text">
@ -439,7 +439,7 @@
</action>
<action name="action_play_pause">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/media-playback-start.png</normaloff>:/media-playback-start.png</iconset>
</property>
<property name="text">
@ -451,7 +451,7 @@
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/media-playback-stop.png</normaloff>:/media-playback-stop.png</iconset>
</property>
<property name="text">
@ -460,7 +460,7 @@
</action>
<action name="action_next_track">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/media-skip-forward.png</normaloff>:/media-skip-forward.png</iconset>
</property>
<property name="text">
@ -469,7 +469,7 @@
</action>
<action name="action_quit">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/exit.png</normaloff>:/exit.png</iconset>
</property>
<property name="text">
@ -481,7 +481,7 @@
</action>
<action name="action_stop_after_this_track">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/media-playback-stop.png</normaloff>:/media-playback-stop.png</iconset>
</property>
<property name="text">
@ -547,7 +547,7 @@
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/last.fm/love.png</normaloff>:/last.fm/love.png</iconset>
</property>
<property name="text">
@ -559,7 +559,7 @@
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/last.fm/ban.png</normaloff>:/last.fm/ban.png</iconset>
</property>
<property name="text">
@ -568,7 +568,7 @@
</action>
<action name="action_clear_playlist">
<property name="icon">
<iconset resource="../data/data.qrc">
<iconset>
<normaloff>:/clear-list.png</normaloff>:/clear-list.png</iconset>
</property>
<property name="text">
@ -578,6 +578,15 @@
<string>Clear playlist</string>
</property>
</action>
<action name="action_edit_track">
<property name="icon">
<iconset>
<normaloff>:/edit-track.png</normaloff>:/edit-track.png</iconset>
</property>
<property name="text">
<string>Edit track information...</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
@ -614,8 +623,6 @@
<header>radioview.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../data/data.qrc"/>
</resources>
<resources/>
<connections/>
</ui>

View File

@ -502,3 +502,10 @@ void Playlist::Clear() {
items_.clear();
reset();
}
void Playlist::ReloadItems(const QList<int>& rows) {
foreach (int row, rows) {
item_at(row)->Reload();
emit dataChanged(index(row, 0), index(row, ColumnCount-1));
}
}

View File

@ -75,6 +75,7 @@ class Playlist : public QAbstractListModel {
QModelIndex InsertRadioStations(const QList<RadioItem*>& items, int after = -1);
QModelIndex InsertPaths(QList<QUrl> urls, int after = -1);
void StopAfter(int row);
void ReloadItems(const QList<int>& rows);
// QAbstractListModel
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }

View File

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

View File

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

View File

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

View File

@ -29,3 +29,7 @@ QUrl SongPlaylistItem::Url() {
ret.setHost("localhost");
return ret;
}
void SongPlaylistItem::Reload() {
song_.InitFromFile(song_.filename(), song_.directory_id());
}

View File

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

View File

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