filerenamer.cpp 32.6 KB
Newer Older
1 2
/***************************************************************************
    begin                : Thu Oct 28 2004
3
    copyright            : (C) 2004, 2007 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
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>
27
#include <kio/netaccess.h>
28 29
#include <kdesktopfile.h>
#include <kconfiggroup.h>
30
#include <kglobal.h>
31
//#include <klineedit.h>
32
#include <klocale.h>
33
#include <kpushbutton.h>
34 35
#include <kapplication.h>
#include <kmessagebox.h>
36
#include <kvbox.h>
37
#include <khbox.h>
38

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

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

59
class ConfirmationDialog : public KDialog
60 61 62 63
{
public:
    ConfirmationDialog(const QMap<QString, QString> &files,
                       QWidget *parent = 0, const char *name = 0)
64
        : KDialog(parent)
65
    {
66 67 68 69 70 71 72
        setObjectName(name);
        setModal(true);
        setCaption(i18n("Warning"));
        setButtons(Ok | Cancel);

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

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

        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
82
        K3ListView *lv = new K3ListView(vbox);
83 84 85 86 87 88 89 90

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

        int lvHeight = 0;

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

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

//
// Implementation of ConfigCategoryReader
//

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

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

115
    // Set a default:
116

117 118
    if(categoryOrder.isEmpty())
        categoryOrder << Artist << Album << Title << Track;
119

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

127 128 129
        m_options[catId] = TagRenamerOptions(catId);
        m_categoryOrder << catId;
    }
130

131
    m_folderSeparators.fill(false, m_categoryOrder.count() - 1);
132

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

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

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

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

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

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

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

    case Title:
        return tag->title();

    case Artist:
        return tag->artist();

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

168 169 170 171
    case Genre:
        return tag->genre();

    default:
172
        return QString();
173
    }
174
}
175

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

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

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

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

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

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

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

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

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

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

228 229 230 231
//
// Implementation of FileRenamerWidget
//

232
FileRenamerWidget::FileRenamerWidget(QWidget *parent) :
Scott Wheeler's avatar
Scott Wheeler committed
233 234 235
    QWidget(parent),
    Ui::FileRenamerBase(),
    CategoryReaderInterface(),
236
    m_exampleFromFile(false)
237
{
238 239
    kDebug(65432) << k_funcinfo << endl;

Tim Beaulen's avatar
Tim Beaulen committed
240 241
    setupUi(this);

242
    QLabel *temp = new QLabel(0);
243 244 245
    QPalette palette;
    palette.setColor(m_exampleText->backgroundRole(), temp->palette().color(backgroundRole()));
    m_exampleText->setPalette(palette);
246 247
    delete temp;

David Faure's avatar
David Faure committed
248
#ifdef __GNUC__
Scott Wheeler's avatar
Scott Wheeler committed
249
    #warning Repair this.
David Faure's avatar
David Faure committed
250
#endif
Scott Wheeler's avatar
Scott Wheeler committed
251
    /* layout()->setMargin(0); */ // We'll be wrapped by KDialogBase
252

253 254 255 256 257 258 259
    // This must be created before createTagRows() is called.

    m_exampleDialog = new ExampleOptionsDialog(this);

    createTagRows();
    loadConfig();

260 261
    // Add correct text to combo box.
    m_category->clear();
Scott Wheeler's avatar
Scott Wheeler committed
262
    for(int i = StartTag; i < NumTypes; ++i) {
263
        QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i));
264
        m_category->addItem(category);
265
    }
266 267 268 269 270 271 272

    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 &)));

273 274 275 276 277
    connect(m_separator, SIGNAL(textChanged(const QString &)), this, SLOT(exampleTextChanged()));
    connect(m_musicFolder, SIGNAL(textChanged(const QString &)), this, SLOT(exampleTextChanged()));
    connect(m_showExample, SIGNAL(clicked()), this, SLOT(toggleExampleDialog()));
    connect(m_insertCategory, SIGNAL(clicked()), this, SLOT(insertCategory()));

278
    exampleTextChanged();
279
}
280

281
void FileRenamerWidget::loadConfig()
282
{
283
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
284
    QList<int> checkedSeparators;
285 286
    KConfigGroup config(KGlobal::config(), "FileRenamer");

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

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

Stephan Kulow's avatar
Stephan Kulow committed
292

Scott Wheeler's avatar
Scott Wheeler committed
293 294 295 296
    for(QList<int>::ConstIterator it = checkedSeparators.begin();
        it != checkedSeparators.end(); ++it)
    {
        int separator = *it;
297 298
        if(separator < m_folderSwitches.count())
            m_folderSwitches[separator]->setChecked(true);
299 300
    }

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

304
    m_separator->setItemText(m_separator->currentIndex(), config.readEntry("Separator", " - "));
305
}
306

307
void FileRenamerWidget::saveConfig()
308
{
309
    kDebug(65432) << k_funcinfo << endl;
310
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
311 312
    QList<int> checkedSeparators;
    QList<int> categoryOrder;
313

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

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

    config.writeEntry("CheckedDirSeparators", checkedSeparators);
    config.writeEntry("CategoryOrder", categoryOrder);
David Faure's avatar
David Faure committed
326
    config.writePathEntry("MusicFolder", m_musicFolder->url().path());
327 328 329
    config.writeEntry("Separator", m_separator->currentText());

    config.sync();
330 331
}

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

Scott Wheeler's avatar
Scott Wheeler committed
336
int FileRenamerWidget::addRowCategory(TagType category)
337
{
338
    kDebug(65432) << k_funcinfo << endl;
339 340
    static QIcon up   = SmallIcon("go-up");
    static QIcon down = SmallIcon("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

Laurent Montel's avatar
Laurent Montel committed
354
    KHBox *frame = new KHBox(m_mainFrame);
355 356 357
    QPalette palette;
    palette.setColor(frame->backgroundRole(), frame->palette().color(backgroundRole()).dark(110));
    frame->setPalette(palette);
358 359

    row.widget = frame;
Laurent Montel's avatar
Laurent Montel committed
360
    frame->setFrameShape(QFrame::Box);
361 362 363 364 365
    frame->setLineWidth(1);
    frame->setMargin(3);

    m_mainFrame->setStretchFactor(frame, 1);

Laurent Montel's avatar
Laurent Montel committed
366
    KVBox *buttons = new KVBox(frame);
Laurent Montel's avatar
Laurent Montel committed
367
    buttons->setFrameStyle(QFrame::Plain | QFrame::Box);
368 369 370 371 372
    buttons->setLineWidth(1);

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

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

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

Laurent Montel's avatar
Laurent Montel committed
388
    KVBox *options = new KVBox(frame);
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    row.enableButton = new KPushButton(i18n("Remove"), options);
    toggleMapper->connect(row.enableButton, SIGNAL(clicked()), SLOT(map()));
    toggleMapper->setMapping(row.enableButton, id);

    row.optionsButton = new KPushButton(i18n("Options"), options);
    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)
        m_insertCategory->setEnabled(false);

    return id;
}

Scott Wheeler's avatar
Scott Wheeler committed
407
void FileRenamerWidget::moveSignalMappings(int oldId, int newId)
408
{
409
    kDebug(65432) << k_funcinfo << endl;
410 411 412 413 414 415
    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
416
bool FileRenamerWidget::removeRow(int id)
417
{
418
    kDebug(65432) << k_funcinfo << endl;
419
    if(id >= m_rows.count()) {
Scott Wheeler's avatar
Scott Wheeler committed
420
        kWarning(65432) << "Trying to remove row, but " << id << " is out-of-range.\n";
421 422 423 424
        return false;
    }

    if(m_rows.count() == 1) {
425
        kError(65432) << "Can't remove last row of File Renamer.\n";
426 427 428 429 430 431 432 433 434 435 436
        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
437
    int checkboxPosition = 0; // Remove first checkbox.
438 439 440 441 442 443 444 445 446 447 448 449 450

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

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

void FileRenamerWidget::addFolderSeparatorCheckbox()
{
487
    kDebug(65432) << k_funcinfo << endl;
488
    QWidget *temp = new QWidget(m_mainFrame);
Stephan Kulow's avatar
Stephan Kulow committed
489
    QHBoxLayout *l = new QHBoxLayout(temp);
490 491 492

    QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp);
    m_folderSwitches.append(cb);
Scott Wheeler's avatar
Scott Wheeler committed
493
    l->addWidget(cb, 0, Qt::AlignCenter);
494 495 496 497 498 499 500 501
    cb->setChecked(false);

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

    temp->show();
}

502
void FileRenamerWidget::createTagRows()
503
{
504
    kDebug(65432) << k_funcinfo << endl;
505
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Scott Wheeler's avatar
Scott Wheeler committed
506
    QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
507

508
    if(categoryOrder.isEmpty())
509
        categoryOrder << Artist << Album << Artist << Title << Track;
510

511 512 513
    // Setup arrays.
    m_rows.reserve(categoryOrder.count());
    m_folderSwitches.reserve(categoryOrder.count() - 1);
514

515 516 517 518 519 520 521 522
    mapper       = new QSignalMapper(this);
    mapper->setObjectName("signal mapper");
    toggleMapper = new QSignalMapper(this);
    toggleMapper->setObjectName("toggle mapper");
    upMapper     = new QSignalMapper(this);
    upMapper->setObjectName("up button mapper");
    downMapper   = new QSignalMapper(this);
    downMapper->setObjectName("down button mapper");
523 524

    connect(mapper,       SIGNAL(mapped(int)), SLOT(showCategoryOption(int)));
525
    connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int)));
526 527 528
    connect(upMapper,     SIGNAL(mapped(int)), SLOT(moveItemUp(int)));
    connect(downMapper,   SIGNAL(mapped(int)), SLOT(moveItemDown(int)));

Laurent Montel's avatar
Laurent Montel committed
529
    m_mainFrame = new KVBox(m_mainView->viewport());
Scott Wheeler's avatar
Scott Wheeler committed
530 531 532
    m_mainFrame->setMargin(10);
    m_mainFrame->setSpacing(5);

533
    m_mainView->addChild(m_mainFrame);
Laurent Montel's avatar
Laurent Montel committed
534
    m_mainView->setResizePolicy(Q3ScrollView::AutoOneFit);
535 536

    // OK, the deal with the categoryOrder variable is that we need to create
537 538 539 540
    // 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).
541

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

544 545
    for(; it != categoryOrder.constEnd(); ++it) {
        if(*it < StartTag || *it >= NumTypes) {
546
            kError(65432) << "Invalid category encountered in file renamer configuration.\n";
547 548
            continue;
        }
549

550
        if(m_rows.count() == MAX_CATEGORIES) {
551
            kError(65432) << "Maximum number of File Renamer tags reached, bailing.\n";
552
            break;
553 554
        }

555 556 557
        TagType i = static_cast<TagType>(*it);

        addRowCategory(i);
558

559 560 561
        // Insert the directory separator checkbox if this isn't the last
        // item.

Scott Wheeler's avatar
Scott Wheeler committed
562
        QList<int>::ConstIterator dup(it);
563 564 565 566

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

569 570 571 572 573 574 575
    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)
        m_insertCategory->setEnabled(false);
576 577
}

578
void FileRenamerWidget::exampleTextChanged()
579
{
580
    kDebug(65432) << k_funcinfo << endl;
581
    // Just use .mp3 as an example
582
#if 0
583
    if(m_exampleFromFile && (m_exampleFile.isEmpty() ||
584 585
                             !FileHandle(m_exampleFile).tag()->isValid()))
    {
586 587 588 589
        m_exampleText->setText(i18n("No file selected, or selected file has no tags."));
        return;
    }

590
    m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3");
591
#endif
592 593
}

594
QString FileRenamerWidget::fileCategoryValue(TagType category) const
595
{
596
    kDebug(65432) << k_funcinfo << endl;
597 598
    FileHandle file(m_exampleFile);
    Tag *tag = file.tag();
599 600 601

    switch(category) {
    case Track:
602
        return QString::number(tag->track());
603 604 605 606 607

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

    case Title:
608
        return tag->title();
609 610

    case Artist:
611
        return tag->artist();
612 613

    case Album:
614
        return tag->album();
615 616

    case Genre:
617
        return tag->genre();
618 619

    default:
620
        return QString();
621
    }
622 623
}

624
QString FileRenamerWidget::categoryValue(TagType category) const
625
{
626
    kDebug(65432) << k_funcinfo << endl;
627 628 629 630
    if(m_exampleFromFile)
        return fileCategoryValue(category);

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

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

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

656
QList<CategoryID> FileRenamerWidget::categoryOrder() const
657
{
658
    kDebug(65432) << k_funcinfo << endl;
659
    QList<CategoryID> list;
660 661

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

667
    return list;
668 669
}

Scott Wheeler's avatar
Scott Wheeler committed
670
bool FileRenamerWidget::hasFolderSeparator(int index) const
671
{
672
    kDebug(65432) << k_funcinfo << endl;
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
    kDebug(65432) << k_funcinfo << endl;
681
    QWidget *l = m_rows[id].widget;
Scott Wheeler's avatar
Scott Wheeler committed
682 683 684
    int bottom = m_rows.count() - 1;
    int pos = m_rows[id].position;
    int newPos = (direction == MoveUp) ? pos - 1 : pos + 1;
685

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

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

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

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

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

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

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

    // Update the table of widget rows.

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

    // 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
720
    QBoxLayout *layout = dynamic_cast<QBoxLayout *>(m_mainFrame->layout());
Dirk Mueller's avatar
Dirk Mueller committed
721
    if ( !layout )
Stephan Kulow's avatar
Stephan Kulow committed
722
        return;
David Faure's avatar
David Faure committed
723
#ifdef __GNUC__
Stephan Kulow's avatar
Stephan Kulow committed
724
#warning double check if that still works with Qt4s layout
David Faure's avatar
David Faure committed
725
#endif
726

727
    layout->removeWidget(l);
728
    layout->insertWidget(2 * newPos, l);
729 730 731 732

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

733
    layout->removeWidget(w);
734
    layout->insertWidget(2 * pos, w);
735 736 737 738 739
    layout->invalidate();

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

Scott Wheeler's avatar
Scott Wheeler committed
740
int FileRenamerWidget::idOfPosition(int position) const
741
{
742
    kDebug(65432) << k_funcinfo << endl;
743
    if(position >= m_rows.count()) {
744
        kError(65432) << "Search for position " << position << " out-of-range.\n";
Scott Wheeler's avatar
Scott Wheeler committed
745
        return -1;
746 747
    }

Scott Wheeler's avatar
Scott Wheeler committed
748
    for(int i = 0; i < m_rows.count(); ++i)
749 750
        if(m_rows[i].position == position)
            return i;
751

752
    kError(65432) << "Unable to find identifier for position " << position << endl;
Scott Wheeler's avatar
Scott Wheeler committed
753
    return -1;
754 755
}

Scott Wheeler's avatar
Scott Wheeler committed
756
int FileRenamerWidget::findIdentifier(const CategoryID &category) const
757
{
758
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
759
    for(int index = 0; index < m_rows.count(); ++index)
760
        if(m_rows[index].category == category)
761 762
            return index;

763
    kError(65432) << "Unable to find match for category " <<
764 765 766 767
        TagRenamerOptions::tagTypeText(category.category) <<
        ", number " << category.categoryNumber << endl;

    return MAX_CATEGORIES;
768 769 770 771
}

void FileRenamerWidget::enableAllUpButtons()
{
772
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
773
    for(int i = 0; i < m_rows.count(); ++i)
774 775 776 777 778
        m_rows[i].upButton->setEnabled(true);
}

void FileRenamerWidget::enableAllDownButtons()
{
779
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
780
    for(int i = 0; i < m_rows.count(); ++i)
781 782 783
        m_rows[i].downButton->setEnabled(true);
}

784
void FileRenamerWidget::showCategoryOption(int id)
785
{
786
    kDebug(65432) << k_funcinfo << endl;
787
    TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber);
788 789

    if(dialog->exec() == QDialog::Accepted) {
790
        m_rows[id].options = dialog->options();
791
        exampleTextChanged();
792
    }
793 794

    delete dialog;
795 796
}

797
void FileRenamerWidget::moveItemUp(int id)
798
{
799
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
800
    moveItem(id, MoveUp);
801 802
}

803
void FileRenamerWidget::moveItemDown(int id)
804
{
805
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
806
    moveItem(id, MoveDown);
807 808
}

809
void FileRenamerWidget::toggleExampleDialog()
810
{
811
    kDebug(65432) << k_funcinfo << endl;
Stephan Kulow's avatar
Stephan Kulow committed
812
    m_exampleDialog->setHidden(!m_exampleDialog->isHidden());
813 814
}

815
void FileRenamerWidget::insertCategory()
816
{
817
    kDebug(65432) << k_funcinfo << endl;
818 819
    TagType category = TagRenamerOptions::tagFromCategoryText(m_category->currentText());
    if(category == Unknown) {
820
        kError(65432) << "Trying to add unknown category somehow.\n";
821 822 823 824 825
        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
826
    int idBottom = idOfPosition(m_rows.count() - 1);
827 828 829 830 831
    m_rows[idBottom].downButton->setEnabled(true);

    addFolderSeparatorCheckbox();

    // Identifier of new row.
Scott Wheeler's avatar
Scott Wheeler committed
832
    int id = addRowCategory(category);
833 834 835 836 837 838 839 840 841 842

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

    m_mainFrame->layout()->invalidate();
    m_mainView->update();

    // Now update according to the code in loadConfig().
    m_rows[id].options = TagRenamerOptions(m_rows[id].category);
    exampleTextChanged();
843 844 845 846
}

void FileRenamerWidget::exampleDialogShown()
{
847
    kDebug(65432) << k_funcinfo << endl;
848 849 850 851 852
    m_showExample->setText(i18n("Hide Renamer Test Dialog"));
}

void FileRenamerWidget::exampleDialogHidden()
{
853
    kDebug(65432) << k_funcinfo << endl;
854 855 856 857 858
    m_showExample->setText(i18n("Show Renamer Test Dialog"));
}

void FileRenamerWidget::fileSelected(const QString &file)
{
859
    kDebug(65432) << k_funcinfo << endl;
860
    m_exampleFromFile = true;
861
    m_exampleFile = file;
862 863 864 865 866
    exampleTextChanged();
}

void FileRenamerWidget::dataSelected()
{
867
    kDebug(65432) << k_funcinfo << endl;
868 869 870 871 872 873
    m_exampleFromFile = false;
    exampleTextChanged();
}

QString FileRenamerWidget::separator() const
{
874
    kDebug(65432) << k_funcinfo << endl;
875 876
    return m_separator->currentText();
}
877

Scott Wheeler's avatar
Scott Wheeler committed
878
QString FileRenamerWidget::musicFolder() const
879
{
880
    kDebug(65432) << k_funcinfo << endl;
David Faure's avatar
David Faure committed
881
    return m_musicFolder->url().path();
882 883
}

884
void FileRenamerWidget::slotRemoveRow(int id)
885
{
886
    kDebug(65432) << k_funcinfo << endl;
887 888
    // Remove the given identified row.
    if(!removeRow(id))
889
        kError(65432) << "Unable to remove row " << id << endl;
890 891
}

892 893 894
//
// Implementation of FileRenamer
//
895 896

FileRenamer::FileRenamer()
897
{
898
}
899

900 901
void FileRenamer::rename(PlaylistItem *item)
{
902
    kDebug(65432) << k_funcinfo << endl;
903 904 905 906 907
    PlaylistItemList list;
    list.append(item);

    rename(list);
}
908

909 910
void FileRenamer::rename(const PlaylistItemList &items)
{
911
    kDebug(65432) << k_funcinfo << endl;
912
    ConfigCategoryReader reader;
913 914 915
    QStringList errorFiles;
    QMap<QString, QString> map;
    QMap<QString, PlaylistItem *> itemMap;
916

917 918
    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
        reader.setPlaylistItem(*it);
919
        QString oldFile = (*it)->file().absFilePath();
920
        QString extension = (*it)->file().fileInfo().suffix();
921
        QString newFile = fileName(reader) + '.' + extension;
922 923 924 925 926 927

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

929
    if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted)
930 931
        return;

932
    KApplication::setOverrideCursor(Qt::waitCursor);
933 934 935
    for(QMap<QString, QString>::ConstIterator it = map.begin();
        it != map.end(); ++it)
    {
936 937
        if(moveFile(it.key(), it.value())) {
            itemMap[it.key()]->setFile(it.value());
938
            itemMap[it.key()]->refresh();
939

940
            setFolderIcon(it.value(), itemMap[it.key()]);
941
        }
942
        else
943
            errorFiles << i18n("%1 to %2", it.key(), it.value());
944 945

        processEvents();
946
    }
947 948 949
    KApplication::restoreOverrideCursor();

    if(!errorFiles.isEmpty())
950
        KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles);
951 952
}

953
bool FileRenamer::moveFile(const QString &src, const QString &dest)
954
{
955
    kDebug(65432) << k_funcinfo << endl;
Scott Wheeler's avatar
Scott Wheeler committed
956
    kDebug(65432) << "Moving file " << src << " to " << dest << endl;
957 958

    if(src == dest)
959
        return false;
960

961
    // Escape URL.
962 963
    KUrl srcURL = KUrl(src);
    KUrl dstURL = KUrl(dest);
964 965 966 967 968 969 970 971 972 973

    // 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
974
    KUrl dir = dstURL;
975
    dir.setFileName(QString());
976 977 978

    // Create the directory.
    if(!KStandardDirs::exists(dir.path()))
979
        if(!KStandardDirs::makeDir(dir.path())) {
980
            kError() << "Unable to create directory " << dir.path() << endl;
981
            return false;
982
        }
983

984 985
    // Move the file.
    return KIO::NetAccess::file_move(srcURL, dstURL);
986
}
987

Scott Wheeler's avatar
Scott Wheeler committed
988
void FileRenamer::setFolderIcon(const KUrl &dst, const PlaylistItem *item)
989
{
990
    kDebug(65432) << k_funcinfo << endl;
991 992 993 994 995 996
    if(item->file().tag()->album().isEmpty() ||
       !item->file().coverInfo()->hasCover())
    {
        return;
    }

Scott Wheeler's avatar
Scott Wheeler committed
997
    KUrl dstURL = dst;
998 999 1000 1001
    dstURL.cleanPath();

    // Split path, and go through each path element.  If a path element has
    // the album information, set its folder icon.
1002
    QStringList elements = dstURL.directory().split("/", QString::SkipEmptyParts);
1003 1004 1005
    QString path;

    for(QStringList::ConstIterator it = elements.begin(); it != elements.end(); ++it) {
1006
        path.append('/' + (*it));
1007

Scott Wheeler's avatar
Scott Wheeler committed
1008
        kDebug() << "Checking path: " << path << endl;
Stephan Kulow's avatar
Stephan Kulow committed
1009
        if((*it).contains(item->file().tag()->album() ) &&
1010 1011 1012 1013
           !QFile::exists(path + "/.directory"))
        {
            // Seems to be a match, let's set the folder icon for the current
            // path.  First we should write out the file.
1014

1015 1016 1017
            QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail);
            thumb.save(path + "/.juk-thumbnail.png", "PNG");

1018 1019
            KDesktopFile dirFile(path + "/.directory");
            KConfigGroup desktopGroup(dirFile.desktopGroup());
1020

1021 1022 1023
            if(!desktopGroup.hasKey("Icon")) {
                desktopGroup.writePathEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path));
                dirFile.sync();
1024 1025 1026 1027 1028 1029 1030
            }

            return;
        }
    }
}

1031 1032 1033 1034
/**
 * Returns iterator pointing to the last item enabled in the given list with
 * a non-empty value (or is required to be included).
 */
1035
QList<CategoryID>::ConstIterator lastEnabledItem(const QList<CategoryID> &list,
1036 1037
                                                   const CategoryReaderInterface &interface)
{
1038
    kDebug(65432) << k_funcinfo << endl;
1039 1040
    QList<CategoryID>::ConstIterator it = list.constBegin();
    QList<CategoryID>::ConstIterator last = list.constEnd();
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052

    for(; it != list.constEnd(); ++it) {
        if(interface.isRequired(*it) || (!interface.isDisabled(*it) &&
              !interface.categoryValue((*it).category).isEmpty()))
        {
            last = it;
        }
    }

    return last;
}

1053
QString FileRenamer::fileName(const CategoryReaderInterface &interface)
1054
{
1055
    kDebug(65432) << k_funcinfo << endl;
1056
    const QList<CategoryID> categoryOrder = interface.categoryOrder();
1057
    const QString separator = interface.separator();
Scott Wheeler's avatar
Scott Wheeler committed
1058
    const QString folder = interface.musicFolder();
1059
    QList<CategoryID>::ConstIterator lastEnabled;
Scott Wheeler's avatar
Scott Wheeler committed
1060
    int i = 0;
1061
    QStringList list;
1062
    QChar dirSeparator = QChar(QDir::separator());
1063

1064 1065 1066 1067
    // Use lastEnabled to properly handle folder separators.
    lastEnabled = lastEnabledItem(categoryOrder, interface);
    bool pastLast = false; // Toggles to true once we've passed lastEnabled.

1068
    for(QList<CategoryID>::ConstIterator it = categoryOrder.begin();
1069 1070 1071 1072 1073
            it != categoryOrder.end();
            ++it, ++i)
    {
        if(it == lastEnabled)
            pastLast = true;
1074

1075
        if(interface.isDisabled(*it))
1076
            continue;
1077

1078 1079 1080 1081 1082 1083
        QString value = interface.value(*it);

        // The user can use the folder separator checkbox to add folders, so don't allow
        // slashes that slip in to accidentally create new folders.  Should we filter this
        // back out when showing it in the GUI?
        value.replace('/', "%2f");
1084

1085 1086
        if(!pastLast && interface.hasFolderSeparator(i))
            value.append(dirSeparator);
1087

1088
        if(interface.isRequired(*it) || !value.isEmpty())
1089 1090 1091 1092 1093 1094 1095
            list.append(value);
    }

    // Construct a single string representation, handling strings ending in
    // '/' specially

    QString result;
1096

1097
    for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) {
1098 1099
        result += *it;

1100 1101 1102 1103 1104 1105
        ++it; // Manually advance iterator to check for end-of-list.

        // Add separator unless at a directory boundary
        if(it != list.constEnd() &&
           !(*it).startsWith(dirSeparator) && // Check beginning of next item.
           !result.endsWith(dirSeparator))
1106 1107 1108 1109
        {
            result += separator;
        }
    }
1110

Scott Wheeler's avatar