Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit a64bdc6d authored by Michael Pyne's avatar Michael Pyne

Implement bug 61340 (Add undo support for multi-tagging).

What this does is introduce a class that manages changes to PlaylistItems.  A number of PlaylistItems can be altered before the underlying files are changed.  Once the files are changed, a record is kept of the changes, which can then be used to undo what happened.  Also, this class takes care of items that are to be renamed.

All code paths that are capable of altering a tag have been changed to use this class.

Note that the underlying code could probably use some cleaning up.  e.g. adding a copy ctor to Tag instead of making a duplicateTag() function. ;-)

I've tested this on a temp directory of copied music, so I'm pretty confident this all works and is safe.  However, you may want to test on a backup first to make sure it works on your system too. =D

Now the question is whether I should backport this or not. :-/

CCMAIL:61340-done@bugs.kde.org

svn path=/trunk/kdemultimedia/juk/; revision=349640
parent 8885e088
......@@ -48,6 +48,7 @@ juk_SOURCES = \
tagguesser.cpp \
tagguesserconfigdlg.cpp \
tagguesserconfigdlgwidget.ui \
tagtransactionmanager.cpp \
trackpickerdialog.cpp \
trackpickerdialogbase.ui \
tracksequenceiterator.cpp \
......
......@@ -380,6 +380,8 @@ void CollectionListItem::refresh()
repaint();
for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) {
(*it)->playlist()->update();
(*it)->playlist()->dataChanged();
if((*it)->listView()->isVisible())
(*it)->repaint();
}
......@@ -408,6 +410,13 @@ void CollectionListItem::updateCollectionDict(const QString &oldPath, const QStr
collection->addToDict(newPath, this);
}
void CollectionListItem::repaint() const
{
QListViewItem::repaint();
for(PlaylistItemList::ConstIterator it = m_children.begin(); it != m_children.end(); ++it)
(*it)->repaint();
}
////////////////////////////////////////////////////////////////////////////////
// CollectionListItem protected methods
////////////////////////////////////////////////////////////////////////////////
......
......@@ -172,6 +172,8 @@ public:
PlaylistItem *itemForPlaylist(const Playlist *playlist) const;
void updateCollectionDict(const QString &oldPath, const QString &newPath);
void repaint() const;
protected:
CollectionListItem(const FileHandle &file);
virtual ~CollectionListItem();
......
......@@ -33,6 +33,7 @@
#include "cache.h"
#include "playlistsplitter.h"
#include "collectionlist.h"
#include "tagtransactionmanager.h"
using namespace ActionCollection;
......@@ -92,6 +93,8 @@ KActionCollection *JuK::actionCollection() const
void JuK::setupLayout()
{
new TagTransactionManager(this);
m_splitter = new PlaylistSplitter(this, "playlistSplitter");
setCentralWidget(m_splitter);
......@@ -108,6 +111,7 @@ void JuK::setupActions()
ActionCollection::actions()->setWidget(this);
KStdAction::quit(this, SLOT(slotQuit()), actions());
KStdAction::undo(this, SLOT(slotUndo()), actions());
KStdAction::cut(kapp, SLOT(cut()), actions());
KStdAction::copy(kapp, SLOT(copy()), actions());
KStdAction::paste(kapp, SLOT(paste()), actions());
......@@ -418,4 +422,9 @@ void JuK::slotConfigureFileRenamer()
FileRenamerConfigDlg(this).exec();
}
void JuK::slotUndo()
{
TagTransactionManager::instance()->undo();
}
#include "juk.moc"
......@@ -70,6 +70,7 @@ private slots:
void slotEditKeys();
void slotConfigureTagGuesser();
void slotConfigureFileRenamer();
void slotUndo();
private:
PlaylistSplitter *m_splitter;
......
......@@ -20,6 +20,7 @@
#include "trackpickerdialog.h"
#include "tag.h"
#include "collectionlist.h"
#include "tagtransactionmanager.h"
#include <kmainwindow.h>
#include <kapplication.h>
......@@ -91,21 +92,22 @@ void MusicBrainzLookup::confirmation()
if(dialog.exec() == QDialog::Accepted && !dialog.result().isEmpty()) {
KTRMResult result = dialog.result();
Tag *tag = TagTransactionManager::duplicateTag(file.tag());
if(!result.title().isEmpty())
file.tag()->setTitle(result.title());
tag->setTitle(result.title());
if(!result.artist().isEmpty())
file.tag()->setArtist(result.artist());
tag->setArtist(result.artist());
if(!result.album().isEmpty())
file.tag()->setAlbum(result.album());
tag->setAlbum(result.album());
if(result.track() != 0)
file.tag()->setTrack(result.track());
tag->setTrack(result.track());
if(result.year() != 0)
file.tag()->setYear(result.year());
tag->setYear(result.year());
file.tag()->save();
CollectionList::instance()->slotRefreshItem(file.absFilePath());
PlaylistItem *item = CollectionList::instance()->lookup(file.absFilePath());
TagTransactionManager::instance()->changeTagOnItem(item, tag);
TagTransactionManager::instance()->commit();
}
queue.pop_front();
......
......@@ -55,6 +55,7 @@
#include "painteater.h"
#include "upcomingplaylist.h"
#include "deletedialog.h"
#include "tagtransactionmanager.h"
using namespace ActionCollection;
......@@ -691,6 +692,13 @@ void Playlist::slotGuessTagInfo(TagGuesser::Type type)
processEvents();
}
// MusicBrainz queries automatically commit at this point. What would
// be nice is having a signal emitted when the last query is completed.
if(type == TagGuesser::FileName)
TagTransactionManager::instance()->commit();
dataChanged();
setCanDeletePlaylist(true);
KApplication::restoreOverrideCursor();
......@@ -1671,50 +1679,47 @@ void Playlist::slotRenameTag()
bool Playlist::editTag(PlaylistItem *item, const QString &text, int column)
{
Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag());
switch(column - columnOffset())
{
case PlaylistItem::TrackColumn:
item->file().tag()->setTitle(text);
newTag->setTitle(text);
break;
case PlaylistItem::ArtistColumn:
item->file().tag()->setArtist(text);
newTag->setArtist(text);
break;
case PlaylistItem::AlbumColumn:
item->file().tag()->setAlbum(text);
newTag->setAlbum(text);
break;
case PlaylistItem::TrackNumberColumn:
{
bool ok;
int value = text.toInt(&ok);
if(ok)
item->file().tag()->setTrack(value);
newTag->setTrack(value);
break;
}
case PlaylistItem::GenreColumn:
item->file().tag()->setGenre(text);
newTag->setGenre(text);
break;
case PlaylistItem::YearColumn:
{
bool ok;
int value = text.toInt(&ok);
if(ok)
item->file().tag()->setYear(value);
newTag->setYear(value);
break;
}
}
if(item->file().tag()->save()) {
item->refresh();
return true;
}
return false;
TagTransactionManager::instance()->changeTagOnItem(item, newTag);
return true;
}
void Playlist::slotInlineEditDone(QListViewItem *, const QString &, int column)
{
QString text = renameLineEdit()->text();
QStringList failedFiles;
bool changed = false;
PlaylistItemList l = selectedItems();
......@@ -1737,19 +1742,10 @@ void Playlist::slotInlineEditDone(QListViewItem *, const QString &, int column)
return;
}
for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) {
processEvents();
QFileInfo fi((*it)->file().absFilePath());
if(!fi.isWritable() || !editTag(*it, text, column))
failedFiles += fi.absFilePath();
}
for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it)
editTag(*it, text, column);
if(!failedFiles.isEmpty())
KMessageBox::detailedSorry(this,
i18n("Could not save to specified file(s)."),
i18n("Could Not Write to:\n") + failedFiles.join("\n"));
TagTransactionManager::instance()->commit();
CollectionList::instance()->dataChanged();
dataChanged();
......
......@@ -38,6 +38,7 @@
#include "cache.h"
#include "k3bexporter.h"
#include "tracksequencemanager.h"
#include "tagtransactionmanager.h"
using namespace ActionCollection;
......@@ -126,6 +127,10 @@ PlaylistBox::PlaylistBox(QWidget *parent, QWidgetStack *playlistStack,
connect(this, SIGNAL(contextMenuRequested(QListViewItem *, const QPoint &, int)),
this, SLOT(slotShowContextMenu(QListViewItem *, const QPoint &, int)));
TagTransactionManager *tagManager = TagTransactionManager::instance();
connect(tagManager, SIGNAL(signalAboutToModifyTags()), SLOT(slotFreezePlaylists()));
connect(tagManager, SIGNAL(signalDoneModifyingTags()), SLOT(slotUnfreezePlaylists()));
CollectionList::initialize(this);
Cache::loadPlaylists(this);
TrackSequenceManager::instance()->setCurrentPlaylist(CollectionList::instance());
......@@ -224,6 +229,16 @@ Playlist *PlaylistBox::currentPlaylist() const
return PlaylistCollection::currentPlaylist();
}
void PlaylistBox::slotFreezePlaylists()
{
setCanDeletePlaylist(false);
}
void PlaylistBox::slotUnfreezePlaylists()
{
setCanDeletePlaylist(true);
}
void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName)
{
PlaylistCollection::setupPlaylist(playlist, iconName);
......@@ -415,6 +430,7 @@ void PlaylistBox::decode(QMimeSource *s, Item *item)
playlistItem = dynamic_cast<TreeViewItemPlaylist *>(item->playlist());
if(playlistItem) {
playlistItem->retag(files, currentPlaylist());
TagTransactionManager::instance()->commit();
currentPlaylist()->update();
return;
}
......
......@@ -68,6 +68,9 @@ public slots:
void clear() {}
virtual Playlist *currentPlaylist() const;
void slotFreezePlaylists();
void slotUnfreezePlaylists();
protected:
virtual void setupPlaylist(Playlist *playlist, const QString &iconName);
virtual void setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem);
......
......@@ -24,6 +24,7 @@
#include "tag.h"
#include "actioncollection.h"
#include "ktrm.h"
#include "tagtransactionmanager.h"
static void startMusicBrainzQuery(const FileHandle &file)
{
......@@ -126,20 +127,20 @@ void PlaylistItem::guessTagInfo(TagGuesser::Type type)
case TagGuesser::FileName:
{
TagGuesser guesser(d->fileHandle.absFilePath());
Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag());
if(!guesser.title().isNull())
d->fileHandle.tag()->setTitle(guesser.title());
tag->setTitle(guesser.title());
if(!guesser.artist().isNull())
d->fileHandle.tag()->setArtist(guesser.artist());
tag->setArtist(guesser.artist());
if(!guesser.album().isNull())
d->fileHandle.tag()->setAlbum(guesser.album());
tag->setAlbum(guesser.album());
if(!guesser.track().isNull())
d->fileHandle.tag()->setTrack(guesser.track().toInt());
tag->setTrack(guesser.track().toInt());
if(!guesser.comment().isNull())
d->fileHandle.tag()->setComment(guesser.comment());
tag->setComment(guesser.comment());
d->fileHandle.tag()->save();
refresh();
TagTransactionManager::instance()->changeTagOnItem(this, tag);
break;
}
case TagGuesser::MusicBrainz:
......@@ -161,7 +162,6 @@ QValueVector<int> PlaylistItem::cachedWidths() const
void PlaylistItem::refresh()
{
m_collectionItem->refresh();
repaint();
}
void PlaylistItem::refreshFromDisk()
......
......@@ -77,6 +77,8 @@ public:
Playlist *playlist() const;
virtual CollectionListItem *collectionItem() const { return m_collectionItem; }
/**
* The widths of items are cached when they're updated for us in computations
* in the "weighted" listview column width mode.
......@@ -139,8 +141,6 @@ protected:
bool isValid() const;
virtual CollectionListItem *collectionItem() const { return m_collectionItem; }
struct Data : public KShared
{
Data() {}
......
......@@ -32,6 +32,10 @@ class Tag
friend class FileHandle;
public:
Tag(const QString &fileName);
/**
* Create an empty tag. Used in FileHandle for cache restoration.
*/
Tag(const QString &fileName, bool);
bool save();
......@@ -43,6 +47,8 @@ public:
int year() const { return m_year; }
QString comment() const { return m_comment; }
QString fileName() const { return m_fileName; }
void setTitle(const QString &value) { m_title = value; }
void setArtist(const QString &value) { m_artist = value; }
void setAlbum(const QString &value) { m_album = value; }
......@@ -64,10 +70,6 @@ public:
CacheDataStream &read(CacheDataStream &s);
private:
/**
* Create an empty tag. Used in FileHandle for cache restoration.
*/
Tag(const QString &fileName, bool);
void setup(TagLib::File *file);
QString m_fileName;
......
......@@ -18,6 +18,7 @@
#include "playlistitem.h"
#include "tag.h"
#include "actioncollection.h"
#include "tagtransactionmanager.h"
#include <kcombobox.h>
#include <klineedit.h>
......@@ -634,95 +635,45 @@ void TagEditor::save(const PlaylistItemList &list)
++it;
QFileInfo newFile(item->file().fileInfo().dirPath() + QDir::separator() +
m_fileNameBox->text());
QFileInfo directory(item->file().fileInfo().dirPath());
QString fileName = item->file().fileInfo().dirPath() + QDir::separator() +
m_fileNameBox->text();
if(list.count() > 1)
fileName = item->file().fileInfo().absFilePath();
// If (the new file is writable or the new file doesn't exist and
// it's directory is writable) and the old file is writable...
// If not we'll append it to errorFiles to tell the user which
// files we couldn't write to.
if(item &&
item->file().tag() &&
item->file().fileInfo().isWritable())
{
// If the file name in the box doesn't match the current file
// name...
Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName);
if(list.count() == 1 &&
item->file().fileInfo().fileName() != newFile.fileName())
{
// We're definitely trying to rename a file, so if it exists
// but isn't writable, or it doesn't exist but its directory
// isn't writable, we have an error.
if((newFile.exists() && !newFile.isWritable()) ||
(!newFile.exists() && !directory.isWritable()) &&
item)
{
errorFiles.append(newFile.filePath());
continue;
}
// Rename the file if it doesn't exist or the user says
// that it's ok.
if(!newFile.exists() ||
KMessageBox::warningYesNo(
this,
i18n("This file already exists.\nDo you want to replace it?"),
i18n("File Exists")) == KMessageBox::Yes)
{
QDir currentDir;
currentDir.rename(item->file().absFilePath(), newFile.filePath());
item->file().setFile(newFile.filePath());
}
}
// A bit more ugliness. If there are multiple files that are
// being modified, they each have a "enabled" checkbox that
// says if that field is to be respected for the multiple
// files. We have to check to see if that is enabled before
// each field that we write.
if(m_enableBoxes[m_artistNameBox]->isOn())
item->file().tag()->setArtist(m_artistNameBox->currentText());
if(m_enableBoxes[m_trackNameBox]->isOn())
item->file().tag()->setTitle(m_trackNameBox->text());
if(m_enableBoxes[m_albumNameBox]->isOn())
item->file().tag()->setAlbum(m_albumNameBox->currentText());
if(m_enableBoxes[m_trackSpin]->isOn()) {
if(m_trackSpin->text().isEmpty())
m_trackSpin->setValue(0);
item->file().tag()->setTrack(m_trackSpin->value());
}
if(m_enableBoxes[m_yearSpin]->isOn()) {
if(m_yearSpin->text().isEmpty())
m_yearSpin->setValue(0);
item->file().tag()->setYear(m_yearSpin->value());
}
if(m_enableBoxes[m_commentBox]->isOn())
item->file().tag()->setComment(m_commentBox->text());
if(m_enableBoxes[m_genreBox]->isOn())
item->file().tag()->setGenre(m_genreBox->currentText());
item->file().tag()->save();
item->refresh();
item->playlist()->update();
// A bit more ugliness. If there are multiple files that are
// being modified, they each have a "enabled" checkbox that
// says if that field is to be respected for the multiple
// files. We have to check to see if that is enabled before
// each field that we write.
if(m_enableBoxes[m_artistNameBox]->isOn())
tag->setArtist(m_artistNameBox->currentText());
if(m_enableBoxes[m_trackNameBox]->isOn())
tag->setTitle(m_trackNameBox->text());
if(m_enableBoxes[m_albumNameBox]->isOn())
tag->setAlbum(m_albumNameBox->currentText());
if(m_enableBoxes[m_trackSpin]->isOn()) {
if(m_trackSpin->text().isEmpty())
m_trackSpin->setValue(0);
tag->setTrack(m_trackSpin->value());
}
if(m_enableBoxes[m_yearSpin]->isOn()) {
if(m_yearSpin->text().isEmpty())
m_yearSpin->setValue(0);
tag->setYear(m_yearSpin->value());
}
else if(item)
errorFiles.append(item->file().absFilePath());
if(m_enableBoxes[m_commentBox]->isOn())
tag->setComment(m_commentBox->text());
if(m_enableBoxes[m_genreBox]->isOn())
tag->setGenre(m_genreBox->currentText());
TagTransactionManager::instance()->changeTagOnItem(item, tag);
}
if(!errorFiles.isEmpty())
KMessageBox::detailedSorry(this,
i18n("Could not save to specified file(s)."),
i18n("Could Not Write to:\n") + errorFiles.join("\n"));
TagTransactionManager::instance()->commit();
CollectionList::instance()->dataChanged();
m_performingSave = false;
KApplication::restoreOverrideCursor();
......
......@@ -26,6 +26,7 @@
#include "tag.h"
#include "playlistitem.h"
#include "playlistsearch.h"
#include "tagtransactionmanager.h"
TreeViewItemPlaylist::TreeViewItemPlaylist(PlaylistCollection *collection,
const PlaylistSearch &search,
......@@ -68,7 +69,7 @@ void TreeViewItemPlaylist::retag(const QStringList &files, Playlist *donorPlayli
if(!item)
continue;
Tag *tag = item->file().tag();
Tag *tag = TagTransactionManager::duplicateTag(item->file().tag());
switch(m_columnType) {
case PlaylistItem::ArtistColumn:
tag->setArtist(name());
......@@ -86,24 +87,8 @@ void TreeViewItemPlaylist::retag(const QStringList &files, Playlist *donorPlayli
kdDebug() << "Unhandled column type editing " << *it << endl;
}
tag->save();
item->refresh();
DynamicPlaylist *dynPlaylist = dynamic_cast<DynamicPlaylist *>(donorPlaylist);
if(dynPlaylist) {
dynPlaylist->slotSetDirty();
static_cast<QWidget*>(dynPlaylist)->update();
}
else {
PlaylistItem *donorItem = item->itemForPlaylist(donorPlaylist);
if(donorItem)
donorItem->repaint();
}
kapp->processEvents();
TagTransactionManager::instance()->changeTagOnItem(item, tag);
}
dataChanged();
}
#include "treeviewitemplaylist.moc"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment