filerenamer.cpp 30.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/***************************************************************************
    begin                : Thu Oct 28 2004
    copyright            : (C) 2004 by Michael Pyne
                         : (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.                                   *
 *                                                                         *
 ***************************************************************************/

#include <algorithm>
18

Scott Wheeler's avatar
build  
Scott Wheeler committed
19
#include <kdebug.h>
20 21 22
#include <kcombobox.h>
#include <kurl.h>
#include <kurlrequester.h>
23
#include <kiconloader.h>
24 25
#include <knuminput.h>
#include <kstandarddirs.h>
26
#include <kio/netaccess.h>
27 28 29 30
#include <kconfigbase.h>
#include <kconfig.h>
#include <kglobal.h>
#include <klineedit.h>
31
#include <klocale.h>
32
#include <kpushbutton.h>
33 34
#include <kapplication.h>
#include <kmessagebox.h>
35
#include <ksimpleconfig.h>
36

37
#include <qfile.h>
Laurent Montel's avatar
Laurent Montel committed
38 39 40 41
#include <q3hbox.h>
#include <q3vbox.h>
#include <q3scrollview.h>
#include <qobject.h>
42 43 44 45 46 47 48
#include <qtimer.h>
#include <qregexp.h>
#include <qcheckbox.h>
#include <qdir.h>
#include <qlabel.h>
#include <qlayout.h>
#include <qsignalmapper.h>
Laurent Montel's avatar
Laurent Montel committed
49 50 51 52 53
#include <q3header.h>
//Added by qt3to4:
#include <QPixmap>
#include <Q3Frame>
#include <Q3ValueList>
54
#include <Q3HBoxLayout>
55

56 57 58 59 60
#include "tag.h"
#include "filehandle.h"
#include "filerenamer.h"
#include "exampleoptions.h"
#include "playlistitem.h"
61
#include "playlist.h"
62
#include "coverinfo.h"
63 64 65 66 67 68 69 70

class ConfirmationDialog : public KDialogBase
{
public:
    ConfirmationDialog(const QMap<QString, QString> &files,
                       QWidget *parent = 0, const char *name = 0)
        : KDialogBase(parent, name, true, i18n("Warning"), Ok | Cancel)
    {
71
        Q3VBox *vbox = makeVBoxMainWidget();
Laurent Montel's avatar
Laurent Montel committed
72
        Q3HBox *hbox = new Q3HBox(vbox);
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

        QLabel *l = new QLabel(hbox);
        l->setPixmap(SmallIcon("messagebox_warning", 32));

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

        KListView *lv = new KListView(vbox);

        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) {
            KListViewItem *i = it.key() != it.data()
                ? new KListViewItem(lv, it.key(), it.data())
                : new KListViewItem(lv, it.key(), i18n("No Change"));
            lvHeight += i->height();
        }

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

//
// Implementation of ConfigCategoryReader
//

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

111 112
    Q3ValueList<int> categoryOrder = config.readIntListEntry("CategoryOrder");
    unsigned categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered.
113

114
    // Set a default:
115

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

119 120 121 122 123 124
    Q3ValueList<int>::ConstIterator catIt = categoryOrder.constBegin();
    for(; catIt != categoryOrder.constEnd(); ++catIt)
    {
        unsigned catCount = categoryCount[*catIt]++;
        TagType category = static_cast<TagType>(*catIt);
        CategoryID catId(category, catCount);
125

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

130
    m_folderSeparators.resize(m_categoryOrder.count() - 1, false);
131

132
    Q3ValueList<int> checkedSeparators = config.readIntListEntry("CheckedDirSeparators");
133

134 135 136 137 138 139
    Q3ValueList<int>::ConstIterator it = checkedSeparators.constBegin();
    for(; it != checkedSeparators.constEnd(); ++it) {
        unsigned index = static_cast<unsigned>(*it);
        if(index < m_folderSeparators.count())
            m_folderSeparators[index] = true;
    }
140

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

145 146 147
QString ConfigCategoryReader::categoryValue(TagType type) const
{
    if(!m_currentItem)
148
        return QString::null;
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::null;
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
Q3ValueList<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
}

211
int ConfigCategoryReader::trackWidth(unsigned categoryNum) const
212
{
213
    return m_options[CategoryID(Track, categoryNum)].trackWidth();
214 215
}

216
bool ConfigCategoryReader::hasFolderSeparator(unsigned 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 233
FileRenamerWidget::FileRenamerWidget(QWidget *parent) :
    FileRenamerBase(parent), CategoryReaderInterface(),
234
    m_exampleFromFile(false)
235 236 237 238 239 240 241 242 243 244 245 246 247 248
{
    QLabel *temp = new QLabel(0);
    m_exampleText->setPaletteBackgroundColor(temp->paletteBackgroundColor());
    delete temp;

    layout()->setMargin(0); // We'll be wrapped by KDialogBase
    
    // This must be created before createTagRows() is called.

    m_exampleDialog = new ExampleOptionsDialog(this);

    createTagRows();
    loadConfig();

249 250 251 252 253
    // Add correct text to combo box.
    m_category->clear();
    for(unsigned i = StartTag; i < NumTypes; ++i) {
        QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i));
        m_category->insertItem(category);
254
    }
255 256 257 258 259 260 261 262

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

265
void FileRenamerWidget::loadConfig()
266
{
Laurent Montel's avatar
Laurent Montel committed
267
    Q3ValueList<int> checkedSeparators;
268 269
    KConfigGroup config(KGlobal::config(), "FileRenamer");

270
    for(unsigned i = 0; i < m_rows.count(); ++i)
271 272 273 274
        m_rows[i].options = TagRenamerOptions(m_rows[i].category);

    checkedSeparators = config.readIntListEntry("CheckedDirSeparators");

Laurent Montel's avatar
Laurent Montel committed
275
    Q3ValueList<int>::ConstIterator it = checkedSeparators.begin();
276
    for(; it != checkedSeparators.end(); ++it) {
277 278 279
        unsigned separator = static_cast<unsigned>(*it);
        if(separator < m_folderSwitches.count())
            m_folderSwitches[separator]->setChecked(true);
280 281
    }

282
    QString url = config.readPathEntry("MusicFolder", "${HOME}/music");
Scott Wheeler's avatar
Scott Wheeler committed
283
    m_musicFolder->setURL(url);
284 285

    m_separator->setCurrentText(config.readEntry("Separator", " - "));
286
}
287

288
void FileRenamerWidget::saveConfig()
289
{
290
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Laurent Montel's avatar
Laurent Montel committed
291 292
    Q3ValueList<int> checkedSeparators;
    Q3ValueList<int> categoryOrder;
293

294 295 296 297 298
    for(unsigned i = 0; i < m_rows.count(); ++i) {
        unsigned rowId = idOfPosition(i); // Write out in GUI order, not m_rows order
        m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber);
        categoryOrder += m_rows[rowId].category.category;
    }
299

300
    for(unsigned i = 0; i < m_folderSwitches.count(); ++i)
301 302 303 304 305
        if(m_folderSwitches[i]->isChecked() == true)
            checkedSeparators += i;

    config.writeEntry("CheckedDirSeparators", checkedSeparators);
    config.writeEntry("CategoryOrder", categoryOrder);
306
    config.writePathEntry("MusicFolder", m_musicFolder->url());
307 308 309
    config.writeEntry("Separator", m_separator->currentText());

    config.sync();
310 311
}

312
FileRenamerWidget::~FileRenamerWidget()
313 314 315
{
}

316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
unsigned FileRenamerWidget::addRowCategory(TagType category)
{
    static QPixmap up   = SmallIcon("up");
    static QPixmap down = SmallIcon("down");

    // Find number of categories already of this type.
    unsigned categoryCount = 0;
    for(unsigned i = 0; i < m_rows.count(); ++i)
        if(m_rows[i].category.category == category)
            ++categoryCount;

    Row row;

    row.category = CategoryID(category, categoryCount);
    row.position = m_rows.count();
    unsigned id = row.position;

    Q3HBox *frame = new Q3HBox(m_mainFrame);
    frame->setPaletteBackgroundColor(frame->paletteBackgroundColor().dark(110));

    row.widget = frame;
    frame->setFrameShape(Q3Frame::Box);
    frame->setLineWidth(1);
    frame->setMargin(3);

    m_mainFrame->setStretchFactor(frame, 1);

    Q3VBox *buttons = new Q3VBox(frame);
    buttons->setFrameStyle(Q3Frame::Plain | Q3Frame::Box);
    buttons->setLineWidth(1);

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

    row.upButton->setPixmap(up);
    row.downButton->setPixmap(down);
    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);
    label->setAlignment(AlignCenter);

    Q3VBox *options = new Q3VBox(frame);
    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;
}

void FileRenamerWidget::moveSignalMappings(unsigned oldId, unsigned newId)
{
    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);
}

bool FileRenamerWidget::removeRow(unsigned id)
{
    if(id >= m_rows.count()) {
        kdWarning(65432) << "Trying to remove row, but " << id << " is out-of-range.\n";
        return false;
    }

    if(m_rows.count() == 1) {
        kdError(65432) << "Can't remove last row of File Renamer.\n";
        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;

    unsigned checkboxPosition = 0; // Remove first checkbox.

    // 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.
    for(unsigned i = 0; i < m_rows.count(); ++i) {
        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.
    for(unsigned i = id + 1; i < m_rows.count(); ++i)
        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()
{
    QWidget *temp = new QWidget(m_mainFrame);
    Q3HBoxLayout *l = new Q3HBoxLayout(temp);

    QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp);
    m_folderSwitches.append(cb);
    l->addWidget(cb, 0, AlignCenter);
    cb->setChecked(false);

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

    temp->show();
}

476
void FileRenamerWidget::createTagRows()
477
{
478
    KConfigGroup config(KGlobal::config(), "FileRenamer");
Laurent Montel's avatar
Laurent Montel committed
479
    Q3ValueList<int> categoryOrder = config.readIntListEntry("CategoryOrder");
480

481
    if(categoryOrder.isEmpty())
482
        categoryOrder << Artist << Album << Artist << Title << Track;
483
    
484 485 486
    // Setup arrays.
    m_rows.reserve(categoryOrder.count());
    m_folderSwitches.reserve(categoryOrder.count() - 1);
487

488 489 490 491
    mapper       = new QSignalMapper(this, "signal mapper");
    toggleMapper = new QSignalMapper(this, "toggle mapper");
    upMapper     = new QSignalMapper(this, "up button mapper");
    downMapper   = new QSignalMapper(this, "down button mapper");
492 493

    connect(mapper,       SIGNAL(mapped(int)), SLOT(showCategoryOption(int)));
494
    connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int)));
495 496 497
    connect(upMapper,     SIGNAL(mapped(int)), SLOT(moveItemUp(int)));
    connect(downMapper,   SIGNAL(mapped(int)), SLOT(moveItemDown(int)));

Laurent Montel's avatar
Laurent Montel committed
498
    m_mainFrame = new Q3VBox(m_mainView->viewport());
Scott Wheeler's avatar
Scott Wheeler committed
499 500 501
    m_mainFrame->setMargin(10);
    m_mainFrame->setSpacing(5);

502
    m_mainView->addChild(m_mainFrame);
Laurent Montel's avatar
Laurent Montel committed
503
    m_mainView->setResizePolicy(Q3ScrollView::AutoOneFit);
504 505

    // OK, the deal with the categoryOrder variable is that we need to create
506 507 508 509
    // 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).
510

511
    Q3ValueList<int>::ConstIterator it = categoryOrder.constBegin();
512

513 514 515 516 517
    for(; it != categoryOrder.constEnd(); ++it) {
        if(*it < StartTag || *it >= NumTypes) {
            kdError(65432) << "Invalid category encountered in file renamer configuration.\n";
            continue;
        }
518

519 520 521
        if(m_rows.count() == MAX_CATEGORIES) {
            kdError(65432) << "Maximum number of File Renamer tags reached, bailing.\n";
            break;
522 523
        }

524 525 526
        TagType i = static_cast<TagType>(*it);

        addRowCategory(i);
527

528 529 530 531 532 533 534 535
        // Insert the directory separator checkbox if this isn't the last
        // item.

        Q3ValueList<int>::ConstIterator dup(it);

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

538 539 540 541 542 543 544
    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);
545 546
}

547
void FileRenamerWidget::exampleTextChanged()
548
{
549 550
    // Just use .mp3 as an example

551 552 553
    if(m_exampleFromFile && (m_exampleFile.isEmpty() || 
                             !FileHandle(m_exampleFile).tag()->isValid()))
    {
554 555 556 557
        m_exampleText->setText(i18n("No file selected, or selected file has no tags."));
        return;
    }

558
    m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3");
559 560
}

561
QString FileRenamerWidget::fileCategoryValue(TagType category) const
562
{
563 564
    FileHandle file(m_exampleFile);
    Tag *tag = file.tag();
565 566 567

    switch(category) {
    case Track:
568
        return QString::number(tag->track());
569 570 571 572 573

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

    case Title:
574
        return tag->title();
575 576

    case Artist:
577
        return tag->artist();
578 579

    case Album:
580
        return tag->album();
581 582

    case Genre:
583
        return tag->genre();
584 585

    default:
586
        return QString::null;
587
    }
588 589
}

590
QString FileRenamerWidget::categoryValue(TagType category) const
591
{
592 593 594 595
    if(m_exampleFromFile)
        return fileCategoryValue(category);

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

597 598
    switch (category) {
    case Track:
599
        return example->m_exampleTrack->text();
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616

    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:
617
        return QString::null;
618
    }
619
}
620

621
Q3ValueList<CategoryID> FileRenamerWidget::categoryOrder() const
622
{
623 624 625 626 627
    Q3ValueList<CategoryID> list;

    // Iterate in GUI row order.
    for(unsigned i = 0; i < m_rows.count(); ++i) {
        unsigned rowId = idOfPosition(i);
628

629 630
        list += m_rows[rowId].category;
    }
631

632
    return list;
633 634
}

635
bool FileRenamerWidget::hasFolderSeparator(unsigned index) const
636
{
637 638
    if(index >= m_folderSwitches.count())
        return false;
639 640
    return m_folderSwitches[index]->isChecked();
}
641

642
void FileRenamerWidget::moveItem(unsigned id, MovementDirection direction)
643
{
644 645 646 647
    QWidget *l = m_rows[id].widget;
    unsigned bottom = m_rows.count() - 1;
    unsigned pos = m_rows[id].position;
    unsigned newPos = (direction == MoveUp) ? pos - 1 : pos + 1;
648

649 650
    // Item we're moving can't go further down after this.

651 652
    if((pos == (bottom - 1) && direction == MoveDown) ||
       (pos == bottom && direction == MoveUp))
653
    {
654 655 656 657 658
        unsigned idBottomRow = idOfPosition(bottom);
        unsigned idAboveBottomRow = idOfPosition(bottom - 1);

        m_rows[idBottomRow].downButton->setEnabled(true);
        m_rows[idAboveBottomRow].downButton->setEnabled(false);
659 660 661 662 663
    }

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

    if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) {
664 665 666 667 668
        unsigned idTopItem = idOfPosition(0);
        unsigned idBelowTopItem = idOfPosition(1);

        m_rows[idTopItem].upButton->setEnabled(true);
        m_rows[idBelowTopItem].upButton->setEnabled(false);
669 670 671 672
    }

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

673 674
    unsigned idSwitchWith = idOfPosition(newPos);
    QWidget *w = m_rows[idSwitchWith].widget;
675 676 677

    // Update the table of widget rows.

678
    std::swap(m_rows[id].position, m_rows[idSwitchWith].position);
679 680 681 682

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

683
    Q3BoxLayout *layout = dynamic_cast<Q3BoxLayout *>(m_mainFrame->layout());
684 685

    layout->remove(l);
686
    layout->insertWidget(2 * newPos, l);
687 688 689 690 691

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

    layout->remove(w);
692
    layout->insertWidget(2 * pos, w);
693 694 695 696 697
    layout->invalidate();

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

698
unsigned FileRenamerWidget::idOfPosition(unsigned position) const
699
{
700 701 702 703 704 705 706 707
    if(position >= m_rows.count()) {
        kdError(65432) << "Search for position " << position << " out-of-range.\n";
        return static_cast<unsigned>(-1);
    }

    for(unsigned i = 0; i < m_rows.count(); ++i)
        if(m_rows[i].position == position)
            return i;
708

709 710
    kdError(65432) << "Unable to find identifier for position " << position << endl;
    return static_cast<unsigned>(-1);
711 712
}

713
unsigned FileRenamerWidget::findIdentifier(const CategoryID &category) const
714
{
715 716
    for(unsigned index = 0; index < m_rows.count(); ++index)
        if(m_rows[index].category == category)
717 718
            return index;

719 720 721 722 723
    kdError(65432) << "Unable to find match for category " <<
        TagRenamerOptions::tagTypeText(category.category) <<
        ", number " << category.categoryNumber << endl;

    return MAX_CATEGORIES;
724 725 726 727
}

void FileRenamerWidget::enableAllUpButtons()
{
728
    for(unsigned i = 0; i < m_rows.count(); ++i)
729 730 731 732 733
        m_rows[i].upButton->setEnabled(true);
}

void FileRenamerWidget::enableAllDownButtons()
{
734
    for(unsigned i = 0; i < m_rows.count(); ++i)
735 736 737
        m_rows[i].downButton->setEnabled(true);
}

738
void FileRenamerWidget::showCategoryOption(int id)
739
{
740
    TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber);
741 742

    if(dialog->exec() == QDialog::Accepted) {
743
        m_rows[id].options = dialog->options();
744
        exampleTextChanged();
745
    }
746 747

    delete dialog;
748 749
}

750
void FileRenamerWidget::moveItemUp(int id)
751
{
752
    moveItem(static_cast<unsigned>(id), MoveUp);
753 754
}

755
void FileRenamerWidget::moveItemDown(int id)
756
{
757
    moveItem(static_cast<unsigned>(id), MoveDown);
758 759
}

760
void FileRenamerWidget::toggleExampleDialog()
761
{
762
    m_exampleDialog->setShown(!m_exampleDialog->isShown());
763 764
}

765
void FileRenamerWidget::insertCategory()
766
{
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
    TagType category = TagRenamerOptions::tagFromCategoryText(m_category->currentText());
    if(category == Unknown) {
        kdError(65432) << "Trying to add unknown category somehow.\n";
        return;
    }

    // We need to enable the down button of the current bottom row since it
    // can now move down.
    unsigned idBottom = idOfPosition(m_rows.count() - 1);
    m_rows[idBottom].downButton->setEnabled(true);

    addFolderSeparatorCheckbox();

    // Identifier of new row.
    unsigned id = addRowCategory(category);

    // 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();
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
}

void FileRenamerWidget::exampleDialogShown()
{
    m_showExample->setText(i18n("Hide Renamer Test Dialog"));
}

void FileRenamerWidget::exampleDialogHidden()
{
    m_showExample->setText(i18n("Show Renamer Test Dialog"));
}

void FileRenamerWidget::fileSelected(const QString &file)
{
    m_exampleFromFile = true;
807
    m_exampleFile = file;
808 809 810 811 812 813 814 815 816 817 818 819 820
    exampleTextChanged();
}

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

QString FileRenamerWidget::separator() const
{
    return m_separator->currentText();
}
821

Scott Wheeler's avatar
Scott Wheeler committed
822
QString FileRenamerWidget::musicFolder() const
823
{
Scott Wheeler's avatar
Scott Wheeler committed
824
    return m_musicFolder->url();
825 826
}

827
void FileRenamerWidget::slotRemoveRow(int id)
828
{
829 830 831
    // Remove the given identified row.
    if(!removeRow(id))
        kdError(65432) << "Unable to remove row " << id << endl;
832 833
}

834 835 836
//
// Implementation of FileRenamer
//
837 838

FileRenamer::FileRenamer()
839
{
840
}
841

842 843 844 845 846 847 848
void FileRenamer::rename(PlaylistItem *item)
{
    PlaylistItemList list;
    list.append(item);

    rename(list);
}
849

850 851 852
void FileRenamer::rename(const PlaylistItemList &items)
{
    ConfigCategoryReader reader;
853 854 855
    QStringList errorFiles;
    QMap<QString, QString> map;
    QMap<QString, PlaylistItem *> itemMap;
856

857 858
    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
        reader.setPlaylistItem(*it);
859 860 861 862 863 864 865 866 867
        QString oldFile = (*it)->file().absFilePath();
        QString extension = (*it)->file().fileInfo().extension(false);
        QString newFile = fileName(reader) + "." + extension;

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

869
    if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted)
870 871
        return;

872
    KApplication::setOverrideCursor(Qt::waitCursor);
873 874 875
    for(QMap<QString, QString>::ConstIterator it = map.begin();
        it != map.end(); ++it)
    {
876 877 878
        if(moveFile(it.key(), it.data())) {
            itemMap[it.key()]->setFile(it.data());
            itemMap[it.key()]->refresh();
879 880

            setFolderIcon(it.data(), itemMap[it.key()]);
881
        }
882 883 884 885
        else
            errorFiles << i18n("%1 to %2").arg(it.key()).arg(it.data());

        processEvents();
886
    }
887 888 889
    KApplication::restoreOverrideCursor();

    if(!errorFiles.isEmpty())
890
        KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles);
891 892
}

893
bool FileRenamer::moveFile(const QString &src, const QString &dest)
894
{
895
    kdDebug(65432) << "Moving file " << src << " to " << dest << endl;
896 897

    if(src == dest)
898
        return false;
899

900
    // Escape URL.
901 902
    KURL srcURL = KURL::fromPathOrURL(src);
    KURL dstURL = KURL::fromPathOrURL(dest);
903 904 905 906 907 908 909 910 911 912

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

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

    // Get just the directory.
913
    KURL dir = dstURL;
914 915 916 917
    dir.setFileName(QString::null);

    // Create the directory.
    if(!KStandardDirs::exists(dir.path()))
918
        if(!KStandardDirs::makeDir(dir.path())) {
919
            kdError() << "Unable to create directory " << dir.path() << endl;
920
            return false;
921
        }
922

923 924
    // Move the file.
    return KIO::NetAccess::file_move(srcURL, dstURL);
925
}
926

927
void FileRenamer::setFolderIcon(const KURL &dst, const PlaylistItem *item)
928 929 930 931 932 933 934
{
    if(item->file().tag()->album().isEmpty() ||
       !item->file().coverInfo()->hasCover())
    {
        return;
    }

935
    KURL dstURL = dst;
936 937 938 939 940 941 942 943 944 945
    dstURL.cleanPath();

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

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

946
        kdDebug() << "Checking path: " << path << endl;
947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968
        if((*it).find(item->file().tag()->album()) != -1 &&
           !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.
            
            QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail);
            thumb.save(path + "/.juk-thumbnail.png", "PNG");

            KSimpleConfig config(path + "/.directory");
            config.setGroup("Desktop Entry");

            if(!config.hasKey("Icon")) {
                config.writeEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path));
                config.sync();
            }

            return;
        }
    }
}

969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
/**
 * Returns iterator pointing to the last item enabled in the given list with
 * a non-empty value (or is required to be included).
 */
Q3ValueList<CategoryID>::ConstIterator lastEnabledItem(const Q3ValueList<CategoryID> &list,
                                                   const CategoryReaderInterface &interface)
{
    Q3ValueList<CategoryID>::ConstIterator it = list.constBegin();
    Q3ValueList<CategoryID>::ConstIterator last = list.constEnd();

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

    return last;
}

990
QString FileRenamer::fileName(const CategoryReaderInterface &interface)
991
{
992
    const Q3ValueList<CategoryID> categoryOrder = interface.categoryOrder();
993
    const QString separator = interface.separator();
Scott Wheeler's avatar
Scott Wheeler committed
994
    const QString folder = interface.musicFolder();
995
    Q3ValueList<CategoryID>::ConstIterator lastEnabled;
996 997
    unsigned i = 0;
    QStringList list;
998
    QChar dirSeparator = QChar(QDir::separator());
999

1000 1001 1002 1003 1004 1005 1006 1007 1008 1009
    // Use lastEnabled to properly handle folder separators.
    lastEnabled = lastEnabledItem(categoryOrder, interface);
    bool pastLast = false; // Toggles to true once we've passed lastEnabled.

    for(Q3ValueList<CategoryID>::ConstIterator it = categoryOrder.begin();
            it != categoryOrder.end();
            ++it, ++i)
    {
        if(it == lastEnabled)
            pastLast = true;
1010

1011
        if(interface.isDisabled(*it))
1012
            continue;
1013

1014 1015 1016 1017 1018 1019
        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");
1020

1021 1022
        if(!pastLast && interface.hasFolderSeparator(i))
            value.append(dirSeparator);
1023

1024
        if(interface.isRequired(*it) || !value.isEmpty())
1025 1026 1027 1028 1029 1030 1031
            list.append(value);
    }

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

    QString result;
1032

1033
    for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) {
1034 1035
        result += *it;

1036 1037 1038 1039 1040 1041
        ++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))
1042 1043 1044 1045
        {
            result += separator;
        }
    }
1046

1047
    return QString(folder + dirSeparator + result);
1048 1049 1050 1051 1052
}

#include "filerenamer.moc"

// vim: set et sw=4 ts=8: