filerenamer.cpp 31.5 KB
Newer Older
1 2 3
/**
 * Copyright (C) 2004, 2007, 2009 Michael Pyne <mpyne@kde.org>
 * Copyright (C) 2003 Frerich Raabe <raabe@kde.org>
Arnold Dumas's avatar
Arnold Dumas committed
4
 * Copyright (C) 2014 Arnold Dumas <contact@arnolddumas.fr>
5 6 7 8 9 10 11 12 13 14 15 16 17
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
18

19 20
#include "filerenamer.h"

21
#include <algorithm>
22

Michael Pyne's avatar
Michael Pyne committed
23
#include <KUrlRequester>
24
#include <kiconloader.h>
Michael Pyne's avatar
Michael Pyne committed
25
#include <KLocalizedString>
Matthias Kretz's avatar
Matthias Kretz committed
26
#include <kio/job.h>
27 28
#include <kdesktopfile.h>
#include <kconfiggroup.h>
Michael Pyne's avatar
Michael Pyne committed
29
#include <KSharedConfig>
30
#include <klineedit.h>
31
#include <kmessagebox.h>
32

33
#include <QFile>
34
#include <QTimer>
35 36
#include <QCheckBox>
#include <QDir>
37 38
#include <QDialog>
#include <QDialogButtonBox>
39
#include <QUrl>
40
#include <QLabel>
41
#include <QSignalMapper>
Laurent Montel's avatar
Laurent Montel committed
42
#include <QPixmap>
Laurent Montel's avatar
Laurent Montel committed
43
#include <QFrame>
44
#include <QTreeWidget>
45
#include <QScrollBar>
46
#include <QPushButton>
Luca Beltrame's avatar
Luca Beltrame committed
47
#include <QHeaderView>
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"
Michael Pyne's avatar
Michael Pyne committed
56
#include "juk_debug.h"
57

58
class ConfirmationDialog : public QDialog
59 60 61
{
public:
    ConfirmationDialog(const QMap<QString, QString> &files,
62 63
                       QWidget *parent = nullptr)
        : QDialog(parent)
64
    {
Richard Lärkäng's avatar
Richard Lärkäng committed
65
        setModal(true);
66
        setWindowTitle(i18nc("warning about mass file rename", "Warning"));
Richard Lärkäng's avatar
Richard Lärkäng committed
67

68 69 70 71
        auto vboxLayout = new QVBoxLayout(this);
        auto hbox = new QWidget(this);
        auto hboxVLayout = new QVBoxLayout(hbox);
        vboxLayout->addWidget(hbox);
72 73

        QLabel *l = new QLabel(hbox);
74
        l->setPixmap(QIcon::fromTheme("dialog-warning").pixmap(KIconLoader::SizeLarge));
75
        hboxVLayout->addWidget(l);
76 77 78

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

81
        QTreeWidget *lv = new QTreeWidget(this);
82

83 84 85 86 87 88
        QStringList headers;
        headers << i18n("Original Name");
        headers << i18n("New Name");

        lv->setHeaderLabels(headers);
        lv->setRootIsDecorated(false);
89 90 91 92 93 94
        vboxLayout->addWidget(lv);

        auto buttonBox = new QDialogButtonBox(this);
        vboxLayout->addWidget(buttonBox);
        connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
95 96 97

        int lvHeight = 0;

Laurent Montel's avatar
Laurent Montel committed
98 99
        QMap<QString, QString>::ConstIterator it = files.constBegin();
        for(; it != files.constEnd(); ++it) {
100 101 102 103 104 105 106 107 108 109 110 111
            QTreeWidgetItem *item = new QTreeWidgetItem(lv);
            item->setText(0, it.key());

            if (it.key() != it.value()) {
                item->setText(1, it.value());
            }

            else {
                item->setText(1, i18n("No Change"));
            }

            lvHeight += lv->visualItemRect(item).height();
112 113 114
        }

        lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height();
115
        lv->setMinimumHeight(qMin(lvHeight, 400));
116

117
        resize(qMin(width(), 500), qMin(minimumHeight(), 400));
118 119

        show();
120 121
    }
};
122 123 124 125 126 127 128

//
// Implementation of ConfigCategoryReader
//

ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(),
    m_currentItem(0)
129
{
130
    KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
131

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

135
    // Set a default:
136

137 138
    if(categoryOrder.isEmpty())
        categoryOrder << Artist << Album << Title << Track;
139

Scott Wheeler's avatar
Scott Wheeler committed
140
    QList<int>::ConstIterator catIt = categoryOrder.constBegin();
141 142
    for(; catIt != categoryOrder.constEnd(); ++catIt)
    {
Scott Wheeler's avatar
Scott Wheeler committed
143
        int catCount = categoryCount[*catIt]++;
144 145
        TagType category = static_cast<TagType>(*catIt);
        CategoryID catId(category, catCount);
146

147 148 149
        m_options[catId] = TagRenamerOptions(catId);
        m_categoryOrder << catId;
    }
150

151
    m_folderSeparators.fill(false, m_categoryOrder.count() - 1);
152

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

Scott Wheeler's avatar
Scott Wheeler committed
155
    QList<int>::ConstIterator it = checkedSeparators.constBegin();
156
    for(; it != checkedSeparators.constEnd(); ++it) {
Scott Wheeler's avatar
Scott Wheeler committed
157 158
        if(*it < m_folderSeparators.count())
            m_folderSeparators[*it] = true;
159
    }
160

161
    m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music");
162
    m_separator = config.readEntry("Separator", " - ");
163
}
164

165 166 167
QString ConfigCategoryReader::categoryValue(TagType type) const
{
    if(!m_currentItem)
168
        return QString();
169 170 171 172 173

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

    switch(type) {
    case Track:
174
        return QString::number(tag->track());
175 176 177 178 179 180 181 182 183 184 185 186

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

    case Title:
        return tag->title();

    case Artist:
        return tag->artist();

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

188 189 190 191
    case Genre:
        return tag->genre();

    default:
192
        return QString();
193
    }
194
}
195

196
QString ConfigCategoryReader::prefix(const CategoryID &category) const
197
{
198
    return m_options[category].prefix();
199 200
}

201
QString ConfigCategoryReader::suffix(const CategoryID &category) const
202
{
203
    return m_options[category].suffix();
204 205
}

206
TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const
207
{
208
    return m_options[category].emptyAction();
209 210
}

211
QString ConfigCategoryReader::emptyText(const CategoryID &category) const
212
{
213 214 215
    return m_options[category].emptyText();
}

216
QList<CategoryID> ConfigCategoryReader::categoryOrder() const
217 218 219 220 221 222 223 224 225
{
    return m_categoryOrder;
}

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

Scott Wheeler's avatar
Scott Wheeler committed
226
QString ConfigCategoryReader::musicFolder() const
227
{
Scott Wheeler's avatar
Scott Wheeler committed
228
    return m_musicFolder;
229 230
}

Scott Wheeler's avatar
Scott Wheeler committed
231
int ConfigCategoryReader::trackWidth(int categoryNum) const
232
{
233
    return m_options[CategoryID(Track, categoryNum)].trackWidth();
234 235
}

Scott Wheeler's avatar
Scott Wheeler committed
236
bool ConfigCategoryReader::hasFolderSeparator(int index) const
237
{
238 239
    if(index >= m_folderSeparators.count())
        return false;
240 241 242
    return m_folderSeparators[index];
}

243
bool ConfigCategoryReader::isDisabled(const CategoryID &category) const
244 245 246 247
{
    return m_options[category].disabled();
}

248 249 250 251
//
// Implementation of FileRenamerWidget
//

252
FileRenamerWidget::FileRenamerWidget(QWidget *parent) :
Scott Wheeler's avatar
Scott Wheeler committed
253 254
    QWidget(parent),
    CategoryReaderInterface(),
255
    m_ui(new Ui::FileRenamerBase),
256
    m_exampleFromFile(false)
257
{
258
    m_ui->setupUi(this);
259

260 261 262 263 264 265 266
    // This must be created before createTagRows() is called.

    m_exampleDialog = new ExampleOptionsDialog(this);

    createTagRows();
    loadConfig();

267
    // Add correct text to combo box.
268
    m_ui->m_category->clear();
Scott Wheeler's avatar
Scott Wheeler committed
269
    for(int i = StartTag; i < NumTypes; ++i) {
270
        QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i));
271
        m_ui->m_category->addItem(category);
272
    }
273 274 275 276

    connect(m_exampleDialog, SIGNAL(signalShown()), SLOT(exampleDialogShown()));
    connect(m_exampleDialog, SIGNAL(signalHidden()), SLOT(exampleDialogHidden()));
    connect(m_exampleDialog, SIGNAL(dataChanged()), SLOT(dataSelected()));
Laurent Montel's avatar
Laurent Montel committed
277 278
    connect(m_exampleDialog, SIGNAL(fileChanged(QString)),
            this,            SLOT(fileSelected(QString)));
279 280
    connect(m_ui->dlgButtonBox, SIGNAL(accepted()), SIGNAL(accepted()));
    connect(m_ui->dlgButtonBox, SIGNAL(rejected()), SIGNAL(rejected()));
281 282

    exampleTextChanged();
283
}
284

285
void FileRenamerWidget::loadConfig()
286
{
Scott Wheeler's avatar
Scott Wheeler committed
287
    QList<int> checkedSeparators;
288
    KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
289

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

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

295
    foreach(int separator, checkedSeparators) {
296 297
        if(separator < m_folderSwitches.count())
            m_folderSwitches[separator]->setChecked(true);
298 299
    }

David Faure's avatar
David Faure committed
300
    QString path = config.readEntry("MusicFolder", "${HOME}/music");
301
    m_ui->m_musicFolder->setUrl(QUrl::fromLocalFile(path));
302 303 304
    m_ui->m_musicFolder->setMode(KFile::Directory |
                                 KFile::ExistingOnly |
                                 KFile::LocalOnly);
305

306
    m_ui->m_separator->setEditText(config.readEntry("Separator", " - "));
307
}
308

309
void FileRenamerWidget::saveConfig()
310
{
311
    KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
312 313
    QList<int> checkedSeparators;
    QList<int> categoryOrder;
314

Scott Wheeler's avatar
Scott Wheeler committed
315 316
    for(int i = 0; i < m_rows.count(); ++i) {
        int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order
317 318 319
        m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber);
        categoryOrder += m_rows[rowId].category.category;
    }
320

Scott Wheeler's avatar
Scott Wheeler committed
321
    for(int i = 0; i < m_folderSwitches.count(); ++i)
322 323 324 325 326
        if(m_folderSwitches[i]->isChecked() == true)
            checkedSeparators += i;

    config.writeEntry("CheckedDirSeparators", checkedSeparators);
    config.writeEntry("CategoryOrder", categoryOrder);
327 328
    config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path());
    config.writeEntry("Separator", m_ui->m_separator->currentText());
329 330

    config.sync();
331 332
}

333
FileRenamerWidget::~FileRenamerWidget()
334 335 336
{
}

Scott Wheeler's avatar
Scott Wheeler committed
337
int FileRenamerWidget::addRowCategory(TagType category)
338
{
339 340
    static QIcon up   = QIcon::fromTheme("go-up");
    static QIcon down = QIcon::fromTheme("go-down");
341 342

    // Find number of categories already of this type.
Scott Wheeler's avatar
Scott Wheeler committed
343 344
    int categoryCount = 0;
    for(int i = 0; i < m_rows.count(); ++i)
345 346 347 348 349 350 351
        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
352
    int id = row.position;
353

354 355 356
    QFrame *frame = new QFrame(m_mainFrame);
    QHBoxLayout *frameLayout = new QHBoxLayout(frame);
    frameLayout->setMargin(3);
357 358

    row.widget = frame;
Laurent Montel's avatar
Laurent Montel committed
359
    frame->setFrameShape(QFrame::Box);
360 361
    frame->setLineWidth(1);

362 363
    QBoxLayout *mainFrameLayout = static_cast<QBoxLayout *>(m_mainFrame->layout());
    mainFrameLayout->addWidget(frame, 1);
364

365 366 367
    QFrame *buttons = new QFrame(frame);
    QVBoxLayout *buttonLayout = new QVBoxLayout(buttons);
    frameLayout->addWidget(buttons);
Laurent Montel's avatar
Laurent Montel committed
368
    buttons->setFrameStyle(QFrame::Plain | QFrame::Box);
369 370
    buttons->setLineWidth(1);

371 372
    row.upButton = new QPushButton(buttons);
    row.downButton = new QPushButton(buttons);
373

374 375
    row.upButton->setIcon(up);
    row.downButton->setIcon(down);
376 377 378 379 380 381 382 383
    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);

384 385 386
    buttonLayout->addWidget(row.upButton);
    buttonLayout->addWidget(row.downButton);

387 388
    QString labelText = QString("<b>%1</b>").arg(TagRenamerOptions::tagTypeText(category));
    QLabel *label = new QLabel(labelText, frame);
389
    frameLayout->addWidget(label, 1);
Scott Wheeler's avatar
Scott Wheeler committed
390
    label->setAlignment(Qt::AlignCenter);
391

392 393 394
    QVBoxLayout *optionLayout = new QVBoxLayout;
    frameLayout->addLayout(optionLayout);

395
    row.enableButton = new QPushButton(i18nc("remove music genre from file renamer", "Remove"), frame);
396
    optionLayout->addWidget(row.enableButton);
397 398 399
    toggleMapper->connect(row.enableButton, SIGNAL(clicked()), SLOT(map()));
    toggleMapper->setMapping(row.enableButton, id);

400
    row.optionsButton = new QPushButton(i18nc("file renamer genre options", "Options"), frame);
401
    optionLayout->addWidget(row.optionsButton);
402 403 404 405 406 407 408 409
    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)
410
        m_ui->m_insertCategory->setEnabled(false);
411 412 413 414

    return id;
}

Scott Wheeler's avatar
Scott Wheeler committed
415
void FileRenamerWidget::moveSignalMappings(int oldId, int newId)
416 417 418 419 420 421 422
{
    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
423
bool FileRenamerWidget::removeRow(int id)
424 425
{
    if(id >= m_rows.count()) {
Michael Pyne's avatar
Michael Pyne committed
426
        qCWarning(JUK_LOG) << "Trying to remove row, but " << id << " is out-of-range.\n";
427 428 429 430
        return false;
    }

    if(m_rows.count() == 1) {
Michael Pyne's avatar
Michael Pyne committed
431
        qCCritical(JUK_LOG) << "Can't remove last row of File Renamer.\n";
432 433 434 435 436 437 438 439 440 441 442
        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
443
    int checkboxPosition = 0; // Remove first checkbox.
444 445 446 447 448 449 450 451 452 453 454 455 456

    // 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
457
    for(int i = 0; i < m_rows.count(); ++i) {
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
        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
475
    for(int i = id + 1; i < m_rows.count(); ++i)
476 477 478 479 480 481 482 483 484
        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.
485
    m_ui->m_insertCategory->setEnabled(true);
486 487 488 489 490 491 492 493

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

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

Stephan Kulow's avatar
Stephan Kulow committed
496
    QHBoxLayout *l = new QHBoxLayout(temp);
497 498 499

    QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp);
    m_folderSwitches.append(cb);
Scott Wheeler's avatar
Scott Wheeler committed
500
    l->addWidget(cb, 0, Qt::AlignCenter);
501 502 503 504 505 506 507 508
    cb->setChecked(false);

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

    temp->show();
}

509
void FileRenamerWidget::createTagRows()
510
{
511
    KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
512
    QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
513

514
    if(categoryOrder.isEmpty())
515
        categoryOrder << Artist << Album << Title << Track;
516

517 518 519
    // Setup arrays.
    m_rows.reserve(categoryOrder.count());
    m_folderSwitches.reserve(categoryOrder.count() - 1);
520

521
    mapper       = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
522
    mapper->setObjectName( QLatin1String("signal mapper" ));
523
    toggleMapper = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
524
    toggleMapper->setObjectName( QLatin1String("toggle mapper" ));
525
    upMapper     = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
526
    upMapper->setObjectName( QLatin1String("up button mapper" ));
527
    downMapper   = new QSignalMapper(this);
Laurent Montel's avatar
Laurent Montel committed
528
    downMapper->setObjectName( QLatin1String("down button mapper" ));
529 530

    connect(mapper,       SIGNAL(mapped(int)), SLOT(showCategoryOption(int)));
531
    connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int)));
532 533 534
    connect(upMapper,     SIGNAL(mapped(int)), SLOT(moveItemUp(int)));
    connect(downMapper,   SIGNAL(mapped(int)), SLOT(moveItemDown(int)));

535 536 537
    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
538

539 540 541
    QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame);
    frameLayout->setMargin(10);
    frameLayout->setSpacing(5);
542 543

    // OK, the deal with the categoryOrder variable is that we need to create
544 545 546 547
    // 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).
548

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

551 552
    for(; it != categoryOrder.constEnd(); ++it) {
        if(*it < StartTag || *it >= NumTypes) {
Michael Pyne's avatar
Michael Pyne committed
553
            qCCritical(JUK_LOG) << "Invalid category encountered in file renamer configuration.\n";
554 555
            continue;
        }
556

557
        if(m_rows.count() == MAX_CATEGORIES) {
Michael Pyne's avatar
Michael Pyne committed
558
            qCCritical(JUK_LOG) << "Maximum number of File Renamer tags reached, bailing.\n";
559
            break;
560 561
        }

562 563 564
        TagType i = static_cast<TagType>(*it);

        addRowCategory(i);
565

566 567 568
        // Insert the directory separator checkbox if this isn't the last
        // item.

Scott Wheeler's avatar
Scott Wheeler committed
569
        QList<int>::ConstIterator dup(it);
570 571 572 573

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

576 577 578 579 580 581
    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)
582
        m_ui->m_insertCategory->setEnabled(false);
583 584
}

585
void FileRenamerWidget::exampleTextChanged()
586
{
587
    // Just use .mp3 as an example
588
    if(m_exampleFromFile && (m_exampleFile.isEmpty() ||
589 590
                             !FileHandle(m_exampleFile).tag()->isValid()))
    {
591
        m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags."));
592 593 594
        return;
    }

595
    m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3");
596 597
}

598
QString FileRenamerWidget::fileCategoryValue(TagType category) const
599
{
600 601
    FileHandle file(m_exampleFile);
    Tag *tag = file.tag();
602 603 604

    switch(category) {
    case Track:
605
        return QString::number(tag->track());
606 607 608 609 610

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

    case Title:
611
        return tag->title();
612 613

    case Artist:
614
        return tag->artist();
615 616

    case Album:
617
        return tag->album();
618 619

    case Genre:
620
        return tag->genre();
621 622

    default:
623
        return QString();
624
    }
625 626
}

627
QString FileRenamerWidget::categoryValue(TagType category) const
628
{
629 630 631 632
    if(m_exampleFromFile)
        return fileCategoryValue(category);

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

634 635
    switch (category) {
    case Track:
636
        return example->m_exampleTrack->text();
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653

    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:
654
        return QString();
655
    }
656
}
657

658
QList<CategoryID> FileRenamerWidget::categoryOrder() const
659
{
660
    QList<CategoryID> list;
661 662

    // Iterate in GUI row order.
Scott Wheeler's avatar
Scott Wheeler committed
663 664
    for(int i = 0; i < m_rows.count(); ++i) {
        int rowId = idOfPosition(i);
665 666
        list += m_rows[rowId].category;
    }
667

668
    return list;
669 670
}

Scott Wheeler's avatar
Scott Wheeler committed
671
bool FileRenamerWidget::hasFolderSeparator(int index) const
672
{
673 674
    if(index >= m_folderSwitches.count())
        return false;
675 676
    return m_folderSwitches[index]->isChecked();
}
677

Scott Wheeler's avatar
Scott Wheeler committed
678
void FileRenamerWidget::moveItem(int id, MovementDirection direction)
679
{
680
    QWidget *l = m_rows[id].widget;
Scott Wheeler's avatar
Scott Wheeler committed
681 682 683
    int bottom = m_rows.count() - 1;
    int pos = m_rows[id].position;
    int newPos = (direction == MoveUp) ? pos - 1 : pos + 1;
684

685 686
    // Item we're moving can't go further down after this.

687 688
    if((pos == (bottom - 1) && direction == MoveDown) ||
       (pos == bottom && direction == MoveUp))
689
    {
Scott Wheeler's avatar
Scott Wheeler committed
690 691
        int idBottomRow = idOfPosition(bottom);
        int idAboveBottomRow = idOfPosition(bottom - 1);
692 693 694

        m_rows[idBottomRow].downButton->setEnabled(true);
        m_rows[idAboveBottomRow].downButton->setEnabled(false);
695 696 697 698 699
    }

    // 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
700 701
        int idTopItem = idOfPosition(0);
        int idBelowTopItem = idOfPosition(1);
702 703 704

        m_rows[idTopItem].upButton->setEnabled(true);
        m_rows[idBelowTopItem].upButton->setEnabled(false);
705 706 707 708
    }

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

Scott Wheeler's avatar
Scott Wheeler committed
709
    int idSwitchWith = idOfPosition(newPos);
710
    QWidget *w = m_rows[idSwitchWith].widget;
711 712 713

    // Update the table of widget rows.

714
    std::swap(m_rows[id].position, m_rows[idSwitchWith].position);
715 716 717 718

    // 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
719
    QBoxLayout *layout = dynamic_cast<QBoxLayout *>(m_mainFrame->layout());
Dirk Mueller's avatar
Dirk Mueller committed
720
    if ( !layout )
Stephan Kulow's avatar
Stephan Kulow committed
721
        return;
722

723
    layout->removeWidget(l);
724
    layout->insertWidget(2 * newPos, l);
725 726 727 728

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

729
    layout->removeWidget(w);
730
    layout->insertWidget(2 * pos, w);
731 732 733 734 735
    layout->invalidate();

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

Scott Wheeler's avatar
Scott Wheeler committed
736
int FileRenamerWidget::idOfPosition(int position) const
737
{
738
    if(position >= m_rows.count()) {
Michael Pyne's avatar
Michael Pyne committed
739
        qCCritical(JUK_LOG) << "Search for position " << position << " out-of-range.\n";
Scott Wheeler's avatar
Scott Wheeler committed
740
        return -1;
741 742
    }

Scott Wheeler's avatar
Scott Wheeler committed
743
    for(int i = 0; i < m_rows.count(); ++i)
744 745
        if(m_rows[i].position == position)
            return i;
746

747
    qCCritical(JUK_LOG) << "Unable to find identifier for position " << position;
Scott Wheeler's avatar
Scott Wheeler committed
748
    return -1;
749 750
}

Scott Wheeler's avatar
Scott Wheeler committed
751
int FileRenamerWidget::findIdentifier(const CategoryID &category) const
752
{
Scott Wheeler's avatar
Scott Wheeler committed
753
    for(int index = 0; index < m_rows.count(); ++index)
754
        if(m_rows[index].category == category)
755 756
            return index;

Michael Pyne's avatar
Michael Pyne committed
757
    qCCritical(JUK_LOG) << "Unable to find match for category " <<
758
        TagRenamerOptions::tagTypeText(category.category) <<
759
        ", number " << category.categoryNumber;
760 761

    return MAX_CATEGORIES;
762 763 764 765
}

void FileRenamerWidget::enableAllUpButtons()
{
Scott Wheeler's avatar
Scott Wheeler committed
766
    for(int i = 0; i < m_rows.count(); ++i)
767 768 769 770 771
        m_rows[i].upButton->setEnabled(true);
}

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

776
void FileRenamerWidget::showCategoryOption(int id)
777
{
778
    TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber);
779 780

    if(dialog->exec() == QDialog::Accepted) {
781
        m_rows[id].options = dialog->options();
782
        exampleTextChanged();
783
    }
784 785

    delete dialog;
786 787
}

788
void FileRenamerWidget::moveItemUp(int id)
789
{
Scott Wheeler's avatar
Scott Wheeler committed
790
    moveItem(id, MoveUp);
791 792
}

793
void FileRenamerWidget::moveItemDown(int id)
794
{
Scott Wheeler's avatar
Scott Wheeler committed
795
    moveItem(id, MoveDown);
796 797
}

798
void FileRenamerWidget::toggleExampleDialog()
799
{
Stephan Kulow's avatar
Stephan Kulow committed
800
    m_exampleDialog->setHidden(!m_exampleDialog->isHidden());
801 802
}

803
void FileRenamerWidget::insertCategory()
804
{
805 806
    TagType category = static_cast<TagType>(m_ui->m_category->currentIndex());
    if(m_ui->m_category->currentIndex() < 0 || category >= NumTypes) {
Michael Pyne's avatar
Michael Pyne committed
807
        qCCritical(JUK_LOG) << "Trying to add unknown category somehow.\n";
808 809 810 811 812
        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
813
    int idBottom = idOfPosition(m_rows.count() - 1);
814 815 816 817 818
    m_rows[idBottom].downButton->setEnabled(true);

    addFolderSeparatorCheckbox();

    // Identifier of new row.
Scott Wheeler's avatar
Scott Wheeler committed
819
    int id = addRowCategory(category);
820 821 822 823 824

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

    m_mainFrame->layout()->invalidate();
825
    m_ui->m_mainView->update();
826 827 828 829

    // Now update according to the code in loadConfig().
    m_rows[id].options = TagRenamerOptions(m_rows[id].category);
    exampleTextChanged();
830 831 832 833
}

void FileRenamerWidget::exampleDialogShown()
{
834
    m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog"));
835 836 837 838
}

void FileRenamerWidget::exampleDialogHidden()
{
839
    m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog"));
840 841 842 843 844
}

void FileRenamerWidget::fileSelected(const QString &file)
{
    m_exampleFromFile = true;
845
    m_exampleFile = file;
846 847 848 849 850 851 852 853 854 855 856
    exampleTextChanged();
}

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

QString FileRenamerWidget::separator() const
{
857
    return m_ui->m_separator->currentText();
858
}
859

Scott Wheeler's avatar
Scott Wheeler committed
860
QString FileRenamerWidget::musicFolder() const
861
{
862
    return m_ui->m_musicFolder->url().path();
863 864
}

865
void FileRenamerWidget::slotRemoveRow(int id)
866
{
867 868
    // Remove the given identified row.
    if(!removeRow(id))
869
        qCCritical(JUK_LOG) << "Unable to remove row " << id;
870 871
}

872 873 874
//
// Implementation of FileRenamer
//
875 876

FileRenamer::FileRenamer()
877
{
878
}
879

880 881 882 883 884 885 886
void FileRenamer::rename(PlaylistItem *item)
{
    PlaylistItemList list;
    list.append(item);

    rename(list);
}
887

888 889 890
void FileRenamer::rename(const PlaylistItemList &items)
{
    ConfigCategoryReader reader;
891 892 893
    QStringList errorFiles;
    QMap<QString, QString> map;
    QMap<QString, PlaylistItem *> itemMap;
894

Laurent Montel's avatar
Laurent Montel committed
895
    for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) {
896
        reader.setPlaylistItem(*it);
897
        QString oldFile = (*it)->file().absFilePath();
898
        QString extension = (*it)->file().fileInfo().suffix();
899
        QString newFile = fileName(reader) + '.' + extension;
900 901 902 903 904 905

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

907
    if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted)
908 909
        return;

Luigi Toscano's avatar
Luigi Toscano committed
910
    QApplication::setOverrideCursor(Qt::WaitCursor);
Laurent Montel's avatar
Laurent Montel committed
911 912
    for(QMap<QString, QString>::ConstIterator it = map.constBegin();
        it != map.constEnd(); ++it)
913
    {
914 915
        if(moveFile(it.key(), it.value())) {
            itemMap[it.key()]->setFile(it.value());
916
            itemMap[it.key()]->refresh();
917

918
            setFolderIcon(QUrl::fromLocalFile(it.value()), itemMap[it.key()]);
919
        }
920
        else
921
            errorFiles << i18n("%1 to %2", it.key(), it.value());
922 923

        processEvents();
924
    }
Luigi Toscano's avatar
Luigi Toscano committed
925
    QApplication::restoreOverrideCursor();
926 927

    if(!errorFiles.isEmpty())
928
        KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles);
929 930
}

931
bool FileRenamer::moveFile(const QString &src, const QString &dest)