filerenamer.cpp 30.9 KB
Newer Older
1 2
/***************************************************************************
    begin                : Thu Oct 28 2004
3
    copyright            : (C) 2004, 2007, 2009 by Michael Pyne
4 5 6 7 8 9 10 11 12 13 14 15 16
                         : (c) 2003 Frerich Raabe <raabe@kde.org>
    email                : michael.pyne@kdemail.net
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

17 18
#include "filerenamer.h"

19
#include <algorithm>
20

Scott Wheeler's avatar
build  
Scott Wheeler committed
21
#include <kdebug.h>
22
#include <kurl.h>
23
#include <kurlrequester.h>
24
#include <kiconloader.h>
25
#include <knuminput.h>
26
#include <kstandarddirs.h>
Matthias Kretz's avatar
Matthias Kretz committed
27
#include <kio/job.h>
28
#include <kio/netaccess.h>
29 30
#include <kdesktopfile.h>
#include <kconfiggroup.h>
31
#include <kglobal.h>
32
#include <klineedit.h>
33
#include <klocale.h>
34
#include <kpushbutton.h>
35 36
#include <kapplication.h>
#include <kmessagebox.h>
37
#include <kvbox.h>
38

39
#include <QFile>
40
#include <QTimer>
41 42 43
#include <QCheckBox>
#include <QDir>
#include <QLabel>
44
#include <QSignalMapper>
Laurent Montel's avatar
Laurent Montel committed
45
#include <QPixmap>
Laurent Montel's avatar
Laurent Montel committed
46
#include <QFrame>
47
#include <Q3Header>
48

49
#include "tag.h"
50
#include "filerenameroptions.h"
51 52 53
#include "filehandle.h"
#include "exampleoptions.h"
#include "playlistitem.h"
54
#include "playlist.h" // processEvents()
55
#include "coverinfo.h"
56

Richard Lärkäng's avatar
Richard Lärkäng committed
57
class ConfirmationDialog : public KDialog
58 59 60 61
{
public:
    ConfirmationDialog(const QMap<QString, QString> &files,
                       QWidget *parent = 0, const char *name = 0)
Richard Lärkäng's avatar
Richard Lärkäng committed
62
        : KDialog(parent)
63
    {
Richard Lärkäng's avatar
Richard Lärkäng committed
64 65
        setObjectName(name);
        setModal(true);
66
        setCaption(i18nc("warning about mass file rename", "Warning"));
Richard Lärkäng's avatar
Richard Lärkäng committed
67 68 69 70
        setButtons(Ok | Cancel);

        KVBox *vbox = new KVBox(this);
        setMainWidget(vbox);
Scott Wheeler's avatar
Scott Wheeler committed
71
        KVBox *hbox = new KVBox(vbox);
72 73

        QLabel *l = new QLabel(hbox);
74
        l->setPixmap(SmallIcon("dialog-warning", 32));
75 76 77 78 79

        l = new QLabel(i18n("You are about to rename the following files. "
                            "Are you sure you want to continue?"), hbox);
        hbox->setStretchFactor(l, 1);

Laurent Montel's avatar
Laurent Montel committed
80
        K3ListView *lv = new K3ListView(vbox);
81 82 83 84 85 86

        lv->addColumn(i18n("Original Name"));
        lv->addColumn(i18n("New Name"));

        int lvHeight = 0;

Laurent Montel's avatar
Laurent Montel committed
87 88
        QMap<QString, QString>::ConstIterator it = files.constBegin();
        for(; it != files.constEnd(); ++it) {
89 90
            K3ListViewItem *i = it.key() != it.value()
                ? new K3ListViewItem(lv, it.key(), it.value())
Laurent Montel's avatar
Laurent Montel committed
91
                : new K3ListViewItem(lv, it.key(), i18n("No Change"));
92 93 94 95
            lvHeight += i->height();
        }

        lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height();
96 97
        lv->setMinimumHeight(qMin(lvHeight, 400));
        resize(qMin(width(), 500), qMin(minimumHeight(), 400));
98 99
    }
};
100 101 102 103 104 105 106

//
// Implementation of ConfigCategoryReader
//

ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(),
    m_currentItem(0)
107
{
108
    KConfigGroup config(KGlobal::config(), "FileRenamer");
109

Scott Wheeler's avatar
Scott Wheeler committed
110 111
    QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
    int categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered.
112

113
    // Set a default:
114

115 116
    if(categoryOrder.isEmpty())
        categoryOrder << Artist << Album << Title << Track;
117

Scott Wheeler's avatar
Scott Wheeler committed
118
    QList<int>::ConstIterator catIt = categoryOrder.constBegin();
119 120
    for(; catIt != categoryOrder.constEnd(); ++catIt)
    {
Scott Wheeler's avatar
Scott Wheeler committed
121
        int catCount = categoryCount[*catIt]++;
122 123
        TagType category = static_cast<TagType>(*catIt);
        CategoryID catId(category, catCount);
124

125 126 127
        m_options[catId] = TagRenamerOptions(catId);
        m_categoryOrder << catId;
    }
128

129
    m_folderSeparators.fill(false, m_categoryOrder.count() - 1);
130

Scott Wheeler's avatar
Scott Wheeler committed
131
    QList<int> checkedSeparators = config.readEntry("CheckedDirSeparators", QList<int>());
132

Scott Wheeler's avatar
Scott Wheeler committed
133
    QList<int>::ConstIterator it = checkedSeparators.constBegin();
134
    for(; it != checkedSeparators.constEnd(); ++it) {
Scott Wheeler's avatar
Scott Wheeler committed
135 136
        if(*it < m_folderSeparators.count())
            m_folderSeparators[*it] = true;
137
    }
138

139
    m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music");
140
    m_separator = config.readEntry("Separator", " - ");
141
}
142

143 144 145
QString ConfigCategoryReader::categoryValue(TagType type) const
{
    if(!m_currentItem)
146
        return QString();
147 148 149 150 151

    Tag *tag = m_currentItem->file().tag();

    switch(type) {
    case Track:
152
        return QString::number(tag->track());
153 154 155 156 157 158 159 160 161 162 163 164

    case Year:
        return QString::number(tag->year());

    case Title:
        return tag->title();

    case Artist:
        return tag->artist();

    case Album:
        return tag->album();
165

166 167 168 169
    case Genre:
        return tag->genre();

    default:
170
        return QString();
171
    }
172
}
173

174
QString ConfigCategoryReader::prefix(const CategoryID &category) const
175
{
176
    return m_options[category].prefix();
177 178
}

179
QString ConfigCategoryReader::suffix(const CategoryID &category) const
180
{
181
    return m_options[category].suffix();
182 183
}

184
TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const
185
{
186
    return m_options[category].emptyAction();
187 188
}

189
QString ConfigCategoryReader::emptyText(const CategoryID &category) const
190
{
191 192 193
    return m_options[category].emptyText();
}

194
QList<CategoryID> ConfigCategoryReader::categoryOrder() const
195 196 197 198 199 200 201 202 203
{
    return m_categoryOrder;
}

QString ConfigCategoryReader::separator() const
{
    return m_separator;
}

Scott Wheeler's avatar
Scott Wheeler committed
204
QString ConfigCategoryReader::musicFolder() const
205
{
Scott Wheeler's avatar
Scott Wheeler committed
206
    return m_musicFolder;
207 208
}

Scott Wheeler's avatar
Scott Wheeler committed
209
int ConfigCategoryReader::trackWidth(int categoryNum) const
210
{
211
    return m_options[CategoryID(Track, categoryNum)].trackWidth();
212 213
}

Scott Wheeler's avatar
Scott Wheeler committed
214
bool ConfigCategoryReader::hasFolderSeparator(int index) const
215
{
216 217
    if(index >= m_folderSeparators.count())
        return false;
218 219 220
    return m_folderSeparators[index];
}

221
bool ConfigCategoryReader::isDisabled(const CategoryID &category) const
222 223 224 225
{
    return m_options[category].disabled();
}

226 227 228 229
//
// Implementation of FileRenamerWidget
//

230
FileRenamerWidget::FileRenamerWidget(QWidget *parent) :
Scott Wheeler's avatar
Scott Wheeler committed
231 232
    QWidget(parent),
    CategoryReaderInterface(),
233
    m_ui(new Ui::FileRenamerBase),
234
    m_exampleFromFile(false)
235
{
236
    m_ui->setupUi(this);
237

238 239 240 241 242 243 244
    // This must be created before createTagRows() is called.

    m_exampleDialog = new ExampleOptionsDialog(this);

    createTagRows();
    loadConfig();

245
    // Add correct text to combo box.
246
    m_ui->m_category->clear();
Scott Wheeler's avatar
Scott Wheeler committed
247
    for(int i = StartTag; i < NumTypes; ++i) {
248
        QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i));
249
        m_ui->m_category->addItem(category);
250
    }
251 252 253 254 255 256 257 258

    connect(m_exampleDialog, SIGNAL(signalShown()), SLOT(exampleDialogShown()));
    connect(m_exampleDialog, SIGNAL(signalHidden()), SLOT(exampleDialogHidden()));
    connect(m_exampleDialog, SIGNAL(dataChanged()), SLOT(dataSelected()));
    connect(m_exampleDialog, SIGNAL(fileChanged(const QString &)),
            this,            SLOT(fileSelected(const QString &)));

    exampleTextChanged();
259
}
260

261
void FileRenamerWidget::loadConfig()
262
{
Scott Wheeler's avatar
Scott Wheeler committed
263
    QList<int> checkedSeparators;
264 265
    KConfigGroup config(KGlobal::config(), "FileRenamer");

Scott Wheeler's avatar
Scott Wheeler committed
266
    for(int i = 0; i < m_rows.count(); ++i)
267 268
        m_rows[i].options = TagRenamerOptions(m_rows[i].category);

Scott Wheeler's avatar
Scott Wheeler committed
269
    checkedSeparators = config.readEntry("CheckedDirSeparators", QList<int>());
270

271
    foreach(int separator, checkedSeparators) {
272 273
        if(separator < m_folderSwitches.count())
            m_folderSwitches[separator]->setChecked(true);
274 275
    }

David Faure's avatar
David Faure committed
276
    QString path = config.readEntry("MusicFolder", "${HOME}/music");
277
    m_ui->m_musicFolder->setPath(path);
278

279
    m_ui->m_separator->setEditText(config.readEntry("Separator", " - "));
280
}
281

282
void FileRenamerWidget::saveConfig()
283
{
284
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
285 286
    QList<int> checkedSeparators;
    QList<int> categoryOrder;
287

Scott Wheeler's avatar
Scott Wheeler committed
288 289
    for(int i = 0; i < m_rows.count(); ++i) {
        int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order
290 291 292
        m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber);
        categoryOrder += m_rows[rowId].category.category;
    }
293

Scott Wheeler's avatar
Scott Wheeler committed
294
    for(int i = 0; i < m_folderSwitches.count(); ++i)
295 296 297 298 299
        if(m_folderSwitches[i]->isChecked() == true)
            checkedSeparators += i;

    config.writeEntry("CheckedDirSeparators", checkedSeparators);
    config.writeEntry("CategoryOrder", categoryOrder);
300 301
    config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path());
    config.writeEntry("Separator", m_ui->m_separator->currentText());
302 303

    config.sync();
304 305
}

306
FileRenamerWidget::~FileRenamerWidget()
307 308 309
{
}

Scott Wheeler's avatar
Scott Wheeler committed
310
int FileRenamerWidget::addRowCategory(TagType category)
311
{
312 313
    static QIcon up   = SmallIcon("go-up");
    static QIcon down = SmallIcon("go-down");
314 315

    // Find number of categories already of this type.
Scott Wheeler's avatar
Scott Wheeler committed
316 317
    int categoryCount = 0;
    for(int i = 0; i < m_rows.count(); ++i)
318 319 320 321 322 323 324
        if(m_rows[i].category.category == category)
            ++categoryCount;

    Row row;

    row.category = CategoryID(category, categoryCount);
    row.position = m_rows.count();
Scott Wheeler's avatar
Scott Wheeler committed
325
    int id = row.position;
326

327 328 329
    QFrame *frame = new QFrame(m_mainFrame);
    QHBoxLayout *frameLayout = new QHBoxLayout(frame);
    frameLayout->setMargin(3);
330 331

    row.widget = frame;
Laurent Montel's avatar
Laurent Montel committed
332
    frame->setFrameShape(QFrame::Box);
333 334
    frame->setLineWidth(1);

335 336
    QBoxLayout *mainFrameLayout = static_cast<QBoxLayout *>(m_mainFrame->layout());
    mainFrameLayout->addWidget(frame, 1);
337

338 339 340
    QFrame *buttons = new QFrame(frame);
    QVBoxLayout *buttonLayout = new QVBoxLayout(buttons);
    frameLayout->addWidget(buttons);
Laurent Montel's avatar
Laurent Montel committed
341
    buttons->setFrameStyle(QFrame::Plain | QFrame::Box);
342 343 344 345 346
    buttons->setLineWidth(1);

    row.upButton = new KPushButton(buttons);
    row.downButton = new KPushButton(buttons);

347 348
    row.upButton->setIcon(up);
    row.downButton->setIcon(down);
349 350 351 352 353 354 355 356
    row.upButton->setFlat(true);
    row.downButton->setFlat(true);

    upMapper->connect(row.upButton, SIGNAL(clicked()), SLOT(map()));
    upMapper->setMapping(row.upButton, id);
    downMapper->connect(row.downButton, SIGNAL(clicked()), SLOT(map()));
    downMapper->setMapping(row.downButton, id);

357 358 359
    buttonLayout->addWidget(row.upButton);
    buttonLayout->addWidget(row.downButton);

360 361
    QString labelText = QString("<b>%1</b>").arg(TagRenamerOptions::tagTypeText(category));
    QLabel *label = new QLabel(labelText, frame);
362
    frameLayout->addWidget(label, 1);
Scott Wheeler's avatar
Scott Wheeler committed
363
    label->setAlignment(Qt::AlignCenter);
364

365 366 367 368 369
    QVBoxLayout *optionLayout = new QVBoxLayout;
    frameLayout->addLayout(optionLayout);

    row.enableButton = new KPushButton(i18nc("remove music genre from file renamer", "Remove"), frame);
    optionLayout->addWidget(row.enableButton);
370 371 372
    toggleMapper->connect(row.enableButton, SIGNAL(clicked()), SLOT(map()));
    toggleMapper->setMapping(row.enableButton, id);

373 374
    row.optionsButton = new KPushButton(i18nc("file renamer genre options", "Options"), frame);
    optionLayout->addWidget(row.optionsButton);
375 376 377 378 379 380 381 382
    mapper->connect(row.optionsButton, SIGNAL(clicked()), SLOT(map()));
    mapper->setMapping(row.optionsButton, id);

    row.widget->show();
    m_rows.append(row);

    // Disable add button if there's too many rows.
    if(m_rows.count() == MAX_CATEGORIES)
383
        m_ui->m_insertCategory->setEnabled(false);
384 385 386 387

    return id;
}

Scott Wheeler's avatar
Scott Wheeler committed
388
void FileRenamerWidget::moveSignalMappings(int oldId, int newId)
389 390 391 392 393 394 395
{
    mapper->setMapping(m_rows[oldId].optionsButton, newId);
    downMapper->setMapping(m_rows[oldId].downButton, newId);
    upMapper->setMapping(m_rows[oldId].upButton, newId);
    toggleMapper->setMapping(m_rows[oldId].enableButton, newId);
}

Scott Wheeler's avatar
Scott Wheeler committed
396
bool FileRenamerWidget::removeRow(int id)
397 398
{
    if(id >= m_rows.count()) {
399
        kWarning() << "Trying to remove row, but " << id << " is out-of-range.\n";
400 401 402 403
        return false;
    }

    if(m_rows.count() == 1) {
404
        kError() << "Can't remove last row of File Renamer.\n";
405 406 407 408 409 410 411 412 413 414 415
        return false;
    }

    // Remove widget.  Don't delete it since it appears QSignalMapper may still need it.
    m_rows[id].widget->deleteLater();
    m_rows[id].widget = 0;
    m_rows[id].enableButton = 0;
    m_rows[id].upButton = 0;
    m_rows[id].optionsButton = 0;
    m_rows[id].downButton = 0;

Scott Wheeler's avatar
Scott Wheeler committed
416
    int checkboxPosition = 0; // Remove first checkbox.
417 418 419 420 421 422 423 424 425 426 427 428 429

    // If not the first row, remove the checkbox before it.
    if(m_rows[id].position > 0)
        checkboxPosition = m_rows[id].position - 1;

    // The checkbox is contained within a layout widget, so the layout
    // widget is the one the needs to die.
    delete m_folderSwitches[checkboxPosition]->parent();
    m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]);

    // Go through all the rows and if they have the same category and a
    // higher categoryNumber, decrement the number.  Also update the
    // position identifier.
Scott Wheeler's avatar
Scott Wheeler committed
430
    for(int i = 0; i < m_rows.count(); ++i) {
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
        if(i == id)
            continue; // Don't mess with ourself.

        if((m_rows[id].category.category == m_rows[i].category.category) &&
           (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber))
        {
            --m_rows[i].category.categoryNumber;
        }

        // Items are moving up.
        if(m_rows[id].position < m_rows[i].position)
            --m_rows[i].position;
    }

    // Every row after the one we delete will have a different identifier, since
    // the identifier is simply its index into m_rows.  So we need to re-do the
    // signal mappings for the affected rows.
Scott Wheeler's avatar
Scott Wheeler committed
448
    for(int i = id + 1; i < m_rows.count(); ++i)
449 450 451 452 453 454 455 456 457
        moveSignalMappings(i, i - 1);

    m_rows.erase(&m_rows[id]);

    // Make sure we update the buttons of affected rows.
    m_rows[idOfPosition(0)].upButton->setEnabled(false);
    m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false);

    // We can insert another row now, make sure GUI is updated to match.
458
    m_ui->m_insertCategory->setEnabled(true);
459 460 461 462 463 464 465 466

    QTimer::singleShot(0, this, SLOT(exampleTextChanged()));
    return true;
}

void FileRenamerWidget::addFolderSeparatorCheckbox()
{
    QWidget *temp = new QWidget(m_mainFrame);
467 468
    m_mainFrame->layout()->addWidget(temp);

Stephan Kulow's avatar
Stephan Kulow committed
469
    QHBoxLayout *l = new QHBoxLayout(temp);
470 471 472

    QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp);
    m_folderSwitches.append(cb);
Scott Wheeler's avatar
Scott Wheeler committed
473
    l->addWidget(cb, 0, Qt::AlignCenter);
474 475 476 477 478 479 480 481
    cb->setChecked(false);

    connect(cb, SIGNAL(toggled(bool)),
            SLOT(exampleTextChanged()));

    temp->show();
}

482
void FileRenamerWidget::createTagRows()
483
{
484
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
485
    QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
486

487
    if(categoryOrder.isEmpty())
488
        categoryOrder << Artist << Album << Title << Track;
489

490 491 492
    // Setup arrays.
    m_rows.reserve(categoryOrder.count());
    m_folderSwitches.reserve(categoryOrder.count() - 1);
493

494
    mapper       = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
495
    mapper->setObjectName( QLatin1String("signal mapper" ));
496
    toggleMapper = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
497
    toggleMapper->setObjectName( QLatin1String("toggle mapper" ));
498
    upMapper     = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
499
    upMapper->setObjectName( QLatin1String("up button mapper" ));
500
    downMapper   = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
501
    downMapper->setObjectName( QLatin1String("down button mapper" ));
502 503

    connect(mapper,       SIGNAL(mapped(int)), SLOT(showCategoryOption(int)));
504
    connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int)));
505 506 507
    connect(upMapper,     SIGNAL(mapped(int)), SLOT(moveItemUp(int)));
    connect(downMapper,   SIGNAL(mapped(int)), SLOT(moveItemDown(int)));

508 509 510
    m_mainFrame = new QFrame(m_ui->m_mainView);
    m_ui->m_mainView->setWidget(m_mainFrame);
    m_ui->m_mainView->setWidgetResizable(true);
Scott Wheeler's avatar
Scott Wheeler committed
511

512 513 514
    QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame);
    frameLayout->setMargin(10);
    frameLayout->setSpacing(5);
515 516

    // OK, the deal with the categoryOrder variable is that we need to create
517 518 519 520
    // the rows in the order that they were saved in (the order given by categoryOrder).
    // The signal mappers operate according to the row identifier.  To find the position of
    // a row given the identifier, use m_rows[id].position.  To find the id of a given
    // position, use idOfPosition(position).
521

Scott Wheeler's avatar
Scott Wheeler committed
522
    QList<int>::ConstIterator it = categoryOrder.constBegin();
523

524 525
    for(; it != categoryOrder.constEnd(); ++it) {
        if(*it < StartTag || *it >= NumTypes) {
526
            kError() << "Invalid category encountered in file renamer configuration.\n";
527 528
            continue;
        }
529

530
        if(m_rows.count() == MAX_CATEGORIES) {
531
            kError() << "Maximum number of File Renamer tags reached, bailing.\n";
532
            break;
533 534
        }

535 536 537
        TagType i = static_cast<TagType>(*it);

        addRowCategory(i);
538

539 540 541
        // Insert the directory separator checkbox if this isn't the last
        // item.

Scott Wheeler's avatar
Scott Wheeler committed
542
        QList<int>::ConstIterator dup(it);
543 544 545 546

        // Check for last item
        if(++dup != categoryOrder.constEnd())
            addFolderSeparatorCheckbox();
547
    }
548

549 550 551 552 553 554
    m_rows.first().upButton->setEnabled(false);
    m_rows.last().downButton->setEnabled(false);

    // If we have maximum number of categories already, don't let the user
    // add more.
    if(m_rows.count() >= MAX_CATEGORIES)
555
        m_ui->m_insertCategory->setEnabled(false);
556 557
}

558
void FileRenamerWidget::exampleTextChanged()
559
{
560
    // Just use .mp3 as an example
561
    if(m_exampleFromFile && (m_exampleFile.isEmpty() ||
562 563
                             !FileHandle(m_exampleFile).tag()->isValid()))
    {
564
        m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags."));
565 566 567
        return;
    }

568
    m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3");
569 570
}

571
QString FileRenamerWidget::fileCategoryValue(TagType category) const
572
{
573 574
    FileHandle file(m_exampleFile);
    Tag *tag = file.tag();
575 576 577

    switch(category) {
    case Track:
578
        return QString::number(tag->track());
579 580 581 582 583

    case Year:
        return QString::number(tag->year());

    case Title:
584
        return tag->title();
585 586

    case Artist:
587
        return tag->artist();
588 589

    case Album:
590
        return tag->album();
591 592

    case Genre:
593
        return tag->genre();
594 595

    default:
596
        return QString();
597
    }
598 599
}

600
QString FileRenamerWidget::categoryValue(TagType category) const
601
{
602 603 604 605
    if(m_exampleFromFile)
        return fileCategoryValue(category);

    const ExampleOptions *example = m_exampleDialog->widget();
606

607 608
    switch (category) {
    case Track:
609
        return example->m_exampleTrack->text();
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626

    case Year:
        return example->m_exampleYear->text();

    case Title:
        return example->m_exampleTitle->text();

    case Artist:
        return example->m_exampleArtist->text();

    case Album:
        return example->m_exampleAlbum->text();

    case Genre:
        return example->m_exampleGenre->text();

    default:
627
        return QString();
628
    }
629
}
630

631
QList<CategoryID> FileRenamerWidget::categoryOrder() const
632
{
633
    QList<CategoryID> list;
634 635

    // Iterate in GUI row order.
Scott Wheeler's avatar
Scott Wheeler committed
636 637
    for(int i = 0; i < m_rows.count(); ++i) {
        int rowId = idOfPosition(i);
638 639
        list += m_rows[rowId].category;
    }
640

641
    return list;
642 643
}

Scott Wheeler's avatar
Scott Wheeler committed
644
bool FileRenamerWidget::hasFolderSeparator(int index) const
645
{
646 647
    if(index >= m_folderSwitches.count())
        return false;
648 649
    return m_folderSwitches[index]->isChecked();
}
650

Scott Wheeler's avatar
Scott Wheeler committed
651
void FileRenamerWidget::moveItem(int id, MovementDirection direction)
652
{
653
    QWidget *l = m_rows[id].widget;
Scott Wheeler's avatar
Scott Wheeler committed
654 655 656
    int bottom = m_rows.count() - 1;
    int pos = m_rows[id].position;
    int newPos = (direction == MoveUp) ? pos - 1 : pos + 1;
657

658 659
    // Item we're moving can't go further down after this.

660 661
    if((pos == (bottom - 1) && direction == MoveDown) ||
       (pos == bottom && direction == MoveUp))
662
    {
Scott Wheeler's avatar
Scott Wheeler committed
663 664
        int idBottomRow = idOfPosition(bottom);
        int idAboveBottomRow = idOfPosition(bottom - 1);
665 666 667

        m_rows[idBottomRow].downButton->setEnabled(true);
        m_rows[idAboveBottomRow].downButton->setEnabled(false);
668 669 670 671 672
    }

    // We're moving the top item, do some button switching.

    if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) {
Scott Wheeler's avatar
Scott Wheeler committed
673 674
        int idTopItem = idOfPosition(0);
        int idBelowTopItem = idOfPosition(1);
675 676 677

        m_rows[idTopItem].upButton->setEnabled(true);
        m_rows[idBelowTopItem].upButton->setEnabled(false);
678 679 680 681
    }

    // This is the item we're swapping with.

Scott Wheeler's avatar
Scott Wheeler committed
682
    int idSwitchWith = idOfPosition(newPos);
683
    QWidget *w = m_rows[idSwitchWith].widget;
684 685 686

    // Update the table of widget rows.

687
    std::swap(m_rows[id].position, m_rows[idSwitchWith].position);
688 689 690 691

    // Move the item two spaces above/below its previous position.  It has to
    // be 2 spaces because of the checkbox.

Stephan Kulow's avatar
Stephan Kulow committed
692
    QBoxLayout *layout = dynamic_cast<QBoxLayout *>(m_mainFrame->layout());
Dirk Mueller's avatar
Dirk Mueller committed
693
    if ( !layout )
Stephan Kulow's avatar
Stephan Kulow committed
694
        return;
695

696
    layout->removeWidget(l);
697
    layout->insertWidget(2 * newPos, l);
698 699 700 701

    // Move the top item two spaces in the opposite direction, for a similar
    // reason.

702
    layout->removeWidget(w);
703
    layout->insertWidget(2 * pos, w);
704 705 706 707 708
    layout->invalidate();

    QTimer::singleShot(0, this, SLOT(exampleTextChanged()));
}

Scott Wheeler's avatar
Scott Wheeler committed
709
int FileRenamerWidget::idOfPosition(int position) const
710
{
711
    if(position >= m_rows.count()) {
712
        kError() << "Search for position " << position << " out-of-range.\n";
Scott Wheeler's avatar
Scott Wheeler committed
713
        return -1;
714 715
    }

Scott Wheeler's avatar
Scott Wheeler committed
716
    for(int i = 0; i < m_rows.count(); ++i)
717 718
        if(m_rows[i].position == position)
            return i;
719

720
    kError() << "Unable to find identifier for position " << position << endl;
Scott Wheeler's avatar
Scott Wheeler committed
721
    return -1;
722 723
}

Scott Wheeler's avatar
Scott Wheeler committed
724
int FileRenamerWidget::findIdentifier(const CategoryID &category) const
725
{
Scott Wheeler's avatar
Scott Wheeler committed
726
    for(int index = 0; index < m_rows.count(); ++index)
727
        if(m_rows[index].category == category)
728 729
            return index;

730
    kError() << "Unable to find match for category " <<
731 732 733 734
        TagRenamerOptions::tagTypeText(category.category) <<
        ", number " << category.categoryNumber << endl;

    return MAX_CATEGORIES;
735 736 737 738
}

void FileRenamerWidget::enableAllUpButtons()
{
Scott Wheeler's avatar
Scott Wheeler committed
739
    for(int i = 0; i < m_rows.count(); ++i)
740 741 742 743 744
        m_rows[i].upButton->setEnabled(true);
}

void FileRenamerWidget::enableAllDownButtons()
{
Scott Wheeler's avatar
Scott Wheeler committed
745
    for(int i = 0; i < m_rows.count(); ++i)
746 747 748
        m_rows[i].downButton->setEnabled(true);
}

749
void FileRenamerWidget::showCategoryOption(int id)
750
{
751
    TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber);
752 753

    if(dialog->exec() == QDialog::Accepted) {
754
        m_rows[id].options = dialog->options();
755
        exampleTextChanged();
756
    }
757 758

    delete dialog;
759 760
}

761
void FileRenamerWidget::moveItemUp(int id)
762
{
Scott Wheeler's avatar
Scott Wheeler committed
763
    moveItem(id, MoveUp);
764 765
}

766
void FileRenamerWidget::moveItemDown(int id)
767
{
Scott Wheeler's avatar
Scott Wheeler committed
768
    moveItem(id, MoveDown);
769 770
}

771
void FileRenamerWidget::toggleExampleDialog()
772
{
Stephan Kulow's avatar
Stephan Kulow committed
773
    m_exampleDialog->setHidden(!m_exampleDialog->isHidden());
774 775
}

776
void FileRenamerWidget::insertCategory()
777
{
778
    TagType category = TagRenamerOptions::tagFromCategoryText(m_ui->m_category->currentText());
779
    if(category == Unknown) {
780
        kError() << "Trying to add unknown category somehow.\n";
781 782 783 784 785
        return;
    }

    // We need to enable the down button of the current bottom row since it
    // can now move down.
Scott Wheeler's avatar
Scott Wheeler committed
786
    int idBottom = idOfPosition(m_rows.count() - 1);
787 788 789 790 791
    m_rows[idBottom].downButton->setEnabled(true);

    addFolderSeparatorCheckbox();

    // Identifier of new row.
Scott Wheeler's avatar
Scott Wheeler committed
792
    int id = addRowCategory(category);
793 794 795 796 797

    // Set its down button to be disabled.
    m_rows[id].downButton->setEnabled(false);

    m_mainFrame->layout()->invalidate();
798
    m_ui->m_mainView->update();
799 800 801 802

    // Now update according to the code in loadConfig().
    m_rows[id].options = TagRenamerOptions(m_rows[id].category);
    exampleTextChanged();
803 804 805 806
}

void FileRenamerWidget::exampleDialogShown()
{
807
    m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog"));
808 809 810 811
}

void FileRenamerWidget::exampleDialogHidden()
{
812
    m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog"));
813 814 815 816 817
}

void FileRenamerWidget::fileSelected(const QString &file)
{
    m_exampleFromFile = true;
818
    m_exampleFile = file;
819 820 821 822 823 824 825 826 827 828 829
    exampleTextChanged();
}

void FileRenamerWidget::dataSelected()
{
    m_exampleFromFile = false;
    exampleTextChanged();
}

QString FileRenamerWidget::separator() const
{
830
    return m_ui->m_separator->currentText();
831
}
832

Scott Wheeler's avatar
Scott Wheeler committed
833
QString FileRenamerWidget::musicFolder() const
834
{
835
    return m_ui->m_musicFolder->url().path();
836 837
}

838
void FileRenamerWidget::slotRemoveRow(int id)
839
{
840 841
    // Remove the given identified row.
    if(!removeRow(id))
842
        kError() << "Unable to remove row " << id << endl;
843 844
}

845 846 847
//
// Implementation of FileRenamer
//
848 849

FileRenamer::FileRenamer()
850
{
851
}
852

853 854 855 856 857 858 859
void FileRenamer::rename(PlaylistItem *item)
{
    PlaylistItemList list;
    list.append(item);

    rename(list);
}
860

861 862 863
void FileRenamer::rename(const PlaylistItemList &items)
{
    ConfigCategoryReader reader;
864 865 866
    QStringList errorFiles;
    QMap<QString, QString> map;
    QMap<QString, PlaylistItem *> itemMap;
867

Laurent Montel's avatar
Laurent Montel committed
868
    for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) {
869
        reader.setPlaylistItem(*it);
870
        QString oldFile = (*it)->file().absFilePath();
871
        QString extension = (*it)->file().fileInfo().suffix();
872
        QString newFile = fileName(reader) + '.' + extension;
873 874 875 876 877 878

        if(oldFile != newFile) {
            map[oldFile] = newFile;
            itemMap[oldFile] = *it;
        }
    }
879

880
    if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted)
881 882
        return;

883
    KApplication::setOverrideCursor(Qt::waitCursor);
Laurent Montel's avatar
Laurent Montel committed
884 885
    for(QMap<QString, QString>::ConstIterator it = map.constBegin();
        it != map.constEnd(); ++it)
886
    {
887 888
        if(moveFile(it.key(), it.value())) {
            itemMap[it.key()]->setFile(it.value());
889
            itemMap[it.key()]->refresh();
890

891
            setFolderIcon(it.value(), itemMap[it.key()]);
892
        }
893
        else
894
            errorFiles << i18n("%1 to %2", it.key(), it.value());
895 896

        processEvents();
897
    }
898 899 900
    KApplication::restoreOverrideCursor();

    if(!errorFiles.isEmpty())
901
        KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles);
902 903
}

904
bool FileRenamer::moveFile(const QString &src, const QString &dest)
905
{
906
    kDebug() << "Moving file " << src << " to " << dest;
907 908

    if(src == dest)
909
        return false;
910

911
    // Escape URL.
912 913
    KUrl srcURL = KUrl(src);
    KUrl dstURL = KUrl(dest);
914 915 916 917 918 919 920 921 922 923

    // Clean it.
    srcURL.cleanPath();
    dstURL.cleanPath();

    // Make sure it is valid.
    if(!srcURL.isValid() || !dstURL.isValid())
        return false;

    // Get just the directory.
Scott Wheeler's avatar
Scott Wheeler committed
924
    KUrl dir = dstURL;
925
    dir.setFileName(QString());
926 927 928

    // Create the directory.
    if(!KStandardDirs::exists(dir.path()))
929
        if(!KStandardDirs::makeDir(dir.path())) {
930
            kError() << "Unable to create directory " << dir.path() << endl;
931
            return false;