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

tageditor.cpp 28.1 KB
Newer Older
1 2
/***************************************************************************
    begin                : Sat Sep 7 2002
3
    copyright            : (C) 2002 - 2004 by Scott Wheeler
4
    email                : wheeler@kde.org
5 6 7 8 9 10 11 12 13 14 15
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

16 17 18 19
#include "tageditor.h"
#include "collectionlist.h"
#include "playlistitem.h"
#include "tag.h"
20
#include "actioncollection.h"
21
#include "tagtransactionmanager.h"
22

23 24 25
#include <kcombobox.h>
#include <klineedit.h>
#include <knuminput.h>
26
#include <ktextedit.h>
27
#include <kmessagebox.h>
28
#include <kconfig.h>
29 30
#include <klocale.h>
#include <kdebug.h>
31
#include <kiconloader.h>
32 33
#include <kicon.h>
#include <kshortcut.h>
34

35

36
#include <QLabel>
37
#include <qapplication.h>
38 39 40
#include <QCheckBox>
#include <QLayout>
#include <QDir>
Scott Wheeler's avatar
Scott Wheeler committed
41
#include <qvalidator.h>
42
#include <QToolTip>
43
#include <qeventloop.h>
Laurent Montel's avatar
Laurent Montel committed
44 45 46 47 48 49 50
#include <q3dict.h>
#include <QEvent>
#include <QKeyEvent>
#include <QHBoxLayout>
#include <QBoxLayout>
#include <QShowEvent>
#include <QVBoxLayout>
Stephan Kulow's avatar
Stephan Kulow committed
51
#include <ktoggleaction.h>
52

53
#include <id3v1genres.h>
54
#include <kactioncollection.h>
55
#include <kconfiggroup.h>
56

Scott Wheeler's avatar
Scott Wheeler committed
57 58
#undef KeyRelease

Scott Wheeler's avatar
Scott Wheeler committed
59 60 61 62
class FileNameValidator : public QValidator
{
public:
    FileNameValidator(QObject *parent, const char *name = 0) :
63
        QValidator(parent)
Tim Beaulen's avatar
Tim Beaulen committed
64 65 66
    {
        setObjectName(name);
    }
Scott Wheeler's avatar
Scott Wheeler committed
67 68 69

    virtual void fixup(QString &s) const
    {
70
        s.remove('/');
Scott Wheeler's avatar
Scott Wheeler committed
71 72 73 74
    }

    virtual State validate(QString &s, int &) const
    {
Stephan Kulow's avatar
Stephan Kulow committed
75
        if(s.contains('/'))
76 77
           return Invalid;
        return Acceptable;
Scott Wheeler's avatar
Scott Wheeler committed
78 79 80
    }
};

81
#if 0 // Qt4 porting: this needs to be in the ::event() for m_fileNameBox (i.e. it needs a KLineEdit-derived class)
82 83 84 85
class FileBoxToolTip : public QToolTip
{
public:
    FileBoxToolTip(TagEditor *editor, QWidget *widget) :
86
        QToolTip(widget), m_editor(editor) {}
87 88 89
protected:
    virtual void maybeTip(const QPoint &)
    {
90
        tip(parentWidget()->rect(), m_editor->items().first()->file().absFilePath());
91 92 93 94
    }
private:
    TagEditor *m_editor;
};
95
#endif
96

97 98 99 100
class FixedHLayout : public QHBoxLayout
{
public:
    FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) :
Stephan Kulow's avatar
Stephan Kulow committed
101 102 103 104 105 106 107
        QHBoxLayout(parent),
        m_width(-1)
    {
        setMargin( margin );
        setSpacing( spacing );
        setObjectName( name );
    }
108
    FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) :
Stephan Kulow's avatar
Stephan Kulow committed
109 110 111 112 113 114 115
        QHBoxLayout(),
        m_width(-1)
    {
        parentLayout->addItem( this );
        setSpacing( spacing );
        setObjectName( name );
    }
116 117
    void setWidth(int w = -1)
    {
118
        m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w;
119 120 121
    }
    virtual QSize minimumSize() const
    {
122 123 124
        QSize s = QHBoxLayout::minimumSize();
        s.setWidth(m_width);
        return s;
125 126 127 128 129
    }
private:
    int m_width;
};

130 131 132 133
class CollectionObserver : public PlaylistObserver
{
public:
    CollectionObserver(TagEditor *parent) :
134 135
        PlaylistObserver(CollectionList::instance()),
        m_parent(parent)
136 137 138 139 140
    {
    }

    virtual void updateData()
    {
141 142
        if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible())
            m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems());
143 144 145 146 147 148 149 150
    }

    virtual void updateCurrent() {}

private:
    TagEditor *m_parent;
};

151 152 153 154
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

Dirk Mueller's avatar
Dirk Mueller committed
155 156
TagEditor::TagEditor(QWidget *parent) :
    QWidget(parent),
157
    m_currentPlaylist(0),
158 159
    m_observer(0),
    m_performingSave(false)
160
{
161
    setupActions();
162 163
    setupLayout();
    readConfig();
164
    m_dataChanged = false;
165
    m_collectionChanged = false;
166 167 168 169
}

TagEditor::~TagEditor()
{
170
    delete m_observer;
171 172 173
    saveConfig();
}

174 175 176 177 178
void TagEditor::setupObservers()
{
    m_observer = new CollectionObserver(this);
}

179 180 181 182
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

183
void TagEditor::slotSetItems(const PlaylistItemList &list)
184
{
185
    if(m_performingSave)
186
        return;
187

188 189 190 191 192
    // Store the playlist that we're setting because saveChangesPrompt
    // can delete the PlaylistItems in list.

    Playlist *itemPlaylist = 0;
    if(!list.isEmpty())
193
        itemPlaylist = list.first()->playlist();
194 195 196

    bool hadPlaylist = m_currentPlaylist != 0;

197
    saveChangesPrompt();
198 199

    if(m_currentPlaylist) {
200 201
        disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)),
                   this, SLOT(slotItemRemoved(PlaylistItem *)));
202 203
    }

204
    if(hadPlaylist && !m_currentPlaylist || !itemPlaylist) {
205 206
        m_currentPlaylist = 0;
        m_items.clear();
207 208
    }
    else {
209
        m_currentPlaylist = itemPlaylist;
210

211
        // We can't use list here, it may not be valid
212

213
        m_items = itemPlaylist->selectedItems();
214
    }
215 216

    if(m_currentPlaylist) {
217 218 219
        connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)),
                this, SLOT(slotItemRemoved(PlaylistItem *)));
        connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved()));
220 221
    }

222
    if(isVisible())
223
        slotRefresh();
224
    else
225
        m_collectionChanged = true;
226 227
}

228
void TagEditor::slotRefresh()
229
{
230
    // This method takes the list of currently selected m_items and tries to
231 232
    // figure out how to show that in the tag editor.  The current strategy --
    // the most common case -- is to just process the first item.  Then we
233
    // check after that to see if there are other m_items and adjust accordingly.
234

235
    if(m_items.isEmpty() || !m_items.first()->file().tag()) {
236 237 238
        slotClear();
        setEnabled(false);
        return;
239
    }
240

241 242
    setEnabled(true);

243
    PlaylistItem *item = m_items.first();
244 245

    Q_ASSERT(item);
246

247
    Tag *tag = item->file().tag();
248

249 250
    QFileInfo fi(item->file().absFilePath());
    if(!fi.isWritable() && m_items.count() == 1)
251
        setEnabled(false);
252

253
    m_artistNameBox->setEditText(tag->artist());
254
    m_trackNameBox->setText(tag->title());
255
    m_albumNameBox->setEditText(tag->album());
256

257
    m_fileNameBox->setText(item->file().fileInfo().fileName());
258 259
    //// TODO: new FileBoxToolTip(this, m_fileNameBox);

260
    m_bitrateBox->setText(QString::number(tag->bitrate()));
261 262
    m_lengthBox->setText(tag->lengthString());

Tim Beaulen's avatar
Tim Beaulen committed
263 264
    if(m_genreList.indexOf(tag->genre()) >= 0)
        m_genreBox->setCurrentIndex(m_genreList.indexOf(tag->genre()) + 1);
265
    else {
Stephan Kulow's avatar
Stephan Kulow committed
266
        m_genreBox->setCurrentIndex(0);
267
        m_genreBox->setEditText(tag->genre());
268
    }
269

270
    m_trackSpin->setValue(tag->track());
271
    m_yearSpin->setValue(tag->year());
272

273
    m_commentBox->setPlainText(tag->comment());
274

275
    // Start at the second item, since we've already processed the first.
276

277 278 279 280
    PlaylistItemList::Iterator it = m_items.begin();
    ++it;

    // If there is more than one item in the m_items that we're dealing with...
281

282
    if(it != m_items.end()) {
283

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 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
        Q3ValueListIterator<QWidget *> hideIt = m_hideList.begin();
        for(; hideIt != m_hideList.end(); ++hideIt)
            (*hideIt)->hide();

        BoxMap::Iterator boxIt = m_enableBoxes.begin();
        for(; boxIt != m_enableBoxes.end(); boxIt++) {
            (*boxIt)->setChecked(true);
            (*boxIt)->show();
        }

        // Yep, this is ugly.  Loop through all of the files checking to see
        // if their fields are the same.  If so, by default, enable their
        // checkbox.

        // Also, if there are more than 50 m_items, don't scan all of them.

        if(m_items.count() > 50) {
            m_enableBoxes[m_artistNameBox]->setChecked(false);
            m_enableBoxes[m_trackNameBox]->setChecked(false);
            m_enableBoxes[m_albumNameBox]->setChecked(false);
            m_enableBoxes[m_genreBox]->setChecked(false);
            m_enableBoxes[m_trackSpin]->setChecked(false);
            m_enableBoxes[m_yearSpin]->setChecked(false);
            m_enableBoxes[m_commentBox]->setChecked(false);
        }
        else {
            for(; it != m_items.end(); ++it) {
                tag = (*it)->file().tag();

                if(tag) {

                    if(m_artistNameBox->currentText() != tag->artist() &&
                       m_enableBoxes.contains(m_artistNameBox))
                    {
                        m_artistNameBox->lineEdit()->clear();
                        m_enableBoxes[m_artistNameBox]->setChecked(false);
                    }
                    if(m_trackNameBox->text() != tag->title() &&
                       m_enableBoxes.contains(m_trackNameBox))
                    {
                        m_trackNameBox->clear();
                        m_enableBoxes[m_trackNameBox]->setChecked(false);
                    }
                    if(m_albumNameBox->currentText() != tag->album() &&
                       m_enableBoxes.contains(m_albumNameBox))
                    {
                        m_albumNameBox->lineEdit()->clear();
                        m_enableBoxes[m_albumNameBox]->setChecked(false);
                    }
                    if(m_genreBox->currentText() != tag->genre() &&
                       m_enableBoxes.contains(m_genreBox))
                    {
                        m_genreBox->lineEdit()->clear();
                        m_enableBoxes[m_genreBox]->setChecked(false);
                    }
                    if(m_trackSpin->value() != tag->track() &&
                       m_enableBoxes.contains(m_trackSpin))
                    {
                        m_trackSpin->setValue(0);
                        m_enableBoxes[m_trackSpin]->setChecked(false);
                    }
                    if(m_yearSpin->value() != tag->year() &&
                       m_enableBoxes.contains(m_yearSpin))
                    {
                        m_yearSpin->setValue(0);
                        m_enableBoxes[m_yearSpin]->setChecked(false);
                    }
351
                    if(m_commentBox->toPlainText() != tag->comment() &&
352 353 354 355 356 357 358 359
                       m_enableBoxes.contains(m_commentBox))
                    {
                        m_commentBox->clear();
                        m_enableBoxes[m_commentBox]->setChecked(false);
                    }
                }
            }
        }
360
    }
361
    else {
362
        // Clean up in the case that we are only handling one item.
363

364 365 366
        Q3ValueListIterator<QWidget *> showIt = m_hideList.begin();
        for(; showIt != m_hideList.end(); ++showIt)
            (*showIt)->show();
Scott Wheeler's avatar
Scott Wheeler committed
367

368 369 370 371 372
        BoxMap::iterator boxIt = m_enableBoxes.begin();
        for(; boxIt != m_enableBoxes.end(); boxIt++) {
            (*boxIt)->setChecked(true);
            (*boxIt)->hide();
        }
373 374
    }
    m_dataChanged = false;
375 376
}

377
void TagEditor::slotClear()
378
{
379 380 381
    m_artistNameBox->lineEdit()->clear();
    m_trackNameBox->clear();
    m_albumNameBox->lineEdit()->clear();
Stephan Kulow's avatar
Stephan Kulow committed
382
    m_genreBox->setCurrentIndex(0);
383 384 385 386 387
    m_fileNameBox->clear();
    m_trackSpin->setValue(0);
    m_yearSpin->setValue(0);
    m_lengthBox->clear();
    m_bitrateBox->clear();
388
    m_commentBox->clear();
389 390
}

391
void TagEditor::slotUpdateCollection()
392
{
393
    if(isVisible())
394
        updateCollection();
395
    else
396
        m_collectionChanged = true;
397 398 399 400 401
}

void TagEditor::updateCollection()
{
    m_collectionChanged = false;
402

403 404 405
    CollectionList *list = CollectionList::instance();

    if(!list)
406 407
        return;

408
    QStringList artistList = list->uniqueSet(CollectionList::Artists);
409
    artistList.sort();
410
    m_artistNameBox->clear();
Stephan Kulow's avatar
Stephan Kulow committed
411
    m_artistNameBox->addItems(artistList);
412 413 414
    m_artistNameBox->completionObject()->setItems(artistList);

    QStringList albumList = list->uniqueSet(CollectionList::Albums);
415
    albumList.sort();
416
    m_albumNameBox->clear();
Stephan Kulow's avatar
Stephan Kulow committed
417
    m_albumNameBox->addItems(albumList);
418
    m_albumNameBox->completionObject()->setItems(albumList);
419

420
    // Merge the list of genres found in tags with the standard ID3v1 set.
421

422
    StringHash genreHash;
423 424

    m_genreList = list->uniqueSet(CollectionList::Genres);
425

426
    for(QStringList::ConstIterator it = m_genreList.begin(); it != m_genreList.end(); ++it)
427
        genreHash.insert(*it);
428 429 430

    TagLib::StringList genres = TagLib::ID3v1::genreList();

431
    for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
432
        genreHash.insert(TStringToQString((*it)));
433

434
    m_genreList = genreHash.values();
435 436
    m_genreList.sort();

437
    m_genreBox->clear();
438
    m_genreBox->addItem(QString());
Stephan Kulow's avatar
Stephan Kulow committed
439
    m_genreBox->addItems(m_genreList);
440
    m_genreBox->completionObject()->setItems(m_genreList);
441 442 443 444 445 446
}

////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

447
void TagEditor::readConfig()
448
{
449
    // combo box completion modes
450

451 452
    KConfigGroup config(KGlobal::config(), "TagEditor");
    if(m_artistNameBox && m_albumNameBox) {
Stephan Kulow's avatar
compile  
Stephan Kulow committed
453 454 455
        readCompletionMode(config, m_artistNameBox, "ArtistNameBoxMode");
        readCompletionMode(config, m_albumNameBox, "AlbumNameBoxMode");
        readCompletionMode(config, m_genreBox, "GenreBoxMode");
456
    }
457

458
    bool show = config.readEntry("Show", false);
459
    ActionCollection::action<KToggleAction>("showEditor")->setChecked(show);
Tim Beaulen's avatar
Tim Beaulen committed
460
    setVisible(show);
461

462
    TagLib::StringList genres = TagLib::ID3v1::genreList();
463

464
    for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
465
        m_genreList.append(TStringToQString((*it)));
466
    m_genreList.sort();
467

468
    m_genreBox->clear();
469
    m_genreBox->addItem(QString::null);
Stephan Kulow's avatar
Stephan Kulow committed
470
    m_genreBox->addItems(m_genreList);
471 472 473
    m_genreBox->completionObject()->setItems(m_genreList);
}

Stephan Kulow's avatar
compile  
Stephan Kulow committed
474
void TagEditor::readCompletionMode(const KConfigGroup &config, KComboBox *box, const QString &key)
475 476
{
    KGlobalSettings::Completion mode =
Stephan Kulow's avatar
compile  
Stephan Kulow committed
477
        KGlobalSettings::Completion(config.readEntry(key, (int)KGlobalSettings::CompletionAuto));
478 479

    box->setCompletionMode(mode);
480
}
481

482 483
void TagEditor::saveConfig()
{
484
    // combo box completion modes
485

486 487 488
    KConfigGroup config(KGlobal::config(), "TagEditor");

    if(m_artistNameBox && m_albumNameBox) {
489 490 491
        config.writeEntry("ArtistNameBoxMode", (int)m_artistNameBox->completionMode());
        config.writeEntry("AlbumNameBoxMode", (int)m_albumNameBox->completionMode());
        config.writeEntry("GenreBoxMode", (int)m_genreBox->completionMode());
492
    }
493
    config.writeEntry("Show", ActionCollection::action<KToggleAction>("showEditor")->isChecked());
494
}
495

496 497
void TagEditor::setupActions()
{
498 499
    KToggleAction *show = new KToggleAction(KIcon("edit"), i18n("Show &Tag Editor"), this);
    ActionCollection::actions()->addAction("showEditor", show);
Dirk Mueller's avatar
Dirk Mueller committed
500
    show->setCheckedState(KGuiItem(i18n("Hide &Tag Editor")));
501 502
    connect(show, SIGNAL(toggled(bool)), this, SLOT(setShown(bool)));

503
    KAction *act = new KAction(KIcon("document-save"), i18n("&Save"), this);
504
    ActionCollection::actions()->addAction("saveItem", act);
505 506
    act->setShortcut(Qt::CTRL + Qt::Key_T);
    connect(act, SIGNAL(triggered(bool)), SLOT(slotSave()));
507 508
}

509 510
void TagEditor::setupLayout()
{
511
    static const int horizontalSpacing = 12;
512
    static const int verticalSpacing = 2;
513

Stephan Kulow's avatar
Stephan Kulow committed
514 515 516
    QHBoxLayout *layout = new QHBoxLayout(this);
    layout->setSpacing(horizontalSpacing);
    layout->setMargin(6);
517 518 519 520

    //////////////////////////////////////////////////////////////////////////////
    // define two columns of the bottem layout
    //////////////////////////////////////////////////////////////////////////////
Stephan Kulow's avatar
Stephan Kulow committed
521 522 523 524 525 526
    QVBoxLayout *leftColumnLayout = new QVBoxLayout();
    layout->addItem( leftColumnLayout );
    leftColumnLayout->setSpacing(verticalSpacing);
    QVBoxLayout *rightColumnLayout = new QVBoxLayout();
    layout->addItem( rightColumnLayout );
    rightColumnLayout->setSpacing(verticalSpacing);
527 528 529 530 531 532 533 534

    layout->setStretchFactor(leftColumnLayout, 2);
    layout->setStretchFactor(rightColumnLayout, 3);

    //////////////////////////////////////////////////////////////////////////////
    // put stuff in the left column -- all of the field names are class wide
    //////////////////////////////////////////////////////////////////////////////
    { // just for organization
535 536 537 538 539

        m_artistNameBox = new KComboBox( true, this );
        m_artistNameBox->setObjectName( "artistNameBox" );
        m_artistNameBox->setCompletionMode(KGlobalSettings::CompletionAuto);
        addItem(i18n("&Artist name:"), m_artistNameBox, leftColumnLayout, "personal");
540

541 542
        m_trackNameBox = new KLineEdit(this);
        m_trackNameBox->setObjectName( "trackNameBox" );
543
        addItem(i18n("&Track name:"), m_trackNameBox, leftColumnLayout, "media-playback-start");
544

545 546 547 548
        m_albumNameBox = new KComboBox( true, this );
        m_albumNameBox->setObjectName( "albumNameBox" );
        m_albumNameBox->setCompletionMode(KGlobalSettings::CompletionAuto);
        addItem(i18n("Album &name:"), m_albumNameBox, leftColumnLayout, "cdrom_unmount");
549

Laurent Montel's avatar
Laurent Montel committed
550 551
        m_genreBox = new KComboBox( true, this );
        m_genreBox->setObjectName( "genreBox" );
552
        addItem(i18n("&Genre:"), m_genreBox, leftColumnLayout, "knotify");
553 554

        // this fills the space at the bottem of the left column
Scott Wheeler's avatar
Scott Wheeler committed
555
        leftColumnLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum,
556
                                                  QSizePolicy::Expanding));
557 558 559 560 561 562
    }
    //////////////////////////////////////////////////////////////////////////////
    // put stuff in the right column
    //////////////////////////////////////////////////////////////////////////////
    { // just for organization

Stephan Kulow's avatar
Stephan Kulow committed
563 564 565
        QHBoxLayout *fileNameLayout = new QHBoxLayout();
	rightColumnLayout->addItem(fileNameLayout);
	fileNameLayout->setSpacing(horizontalSpacing);
Scott Wheeler's avatar
Scott Wheeler committed
566

567 568
        m_fileNameBox = new KLineEdit(this);
        m_fileNameBox->setObjectName( "fileNameBox" );
569
        m_fileNameBox->setValidator(new FileNameValidator(m_fileNameBox));
Scott Wheeler's avatar
Scott Wheeler committed
570

571 572
        QLabel *fileNameIcon = new QLabel(this);
        fileNameIcon->setPixmap(SmallIcon("sound"));
Stephan Kulow's avatar
Stephan Kulow committed
573 574 575
        QLabel * tmp = new QLabel(i18n("&File name:"), this);
        tmp->setBuddy( m_fileNameBox );
        QWidget *fileNameLabel = addHidden(tmp);
576 577 578 579 580 581 582

        fileNameLayout->addWidget(addHidden(fileNameIcon));
        fileNameLayout->addWidget(fileNameLabel);
        fileNameLayout->setStretchFactor(fileNameIcon, 0);
        fileNameLayout->setStretchFactor(fileNameLabel, 0);
        fileNameLayout->insertStretch(-1, 1);
        rightColumnLayout->addWidget(addHidden(m_fileNameBox));
583 584

        { // lay out the track row
585
            FixedHLayout *trackRowLayout = new FixedHLayout(rightColumnLayout,
586 587
                                                            horizontalSpacing);

588 589
            m_trackSpin = new KIntSpinBox(0, 9999, 1, 0, this);
            m_trackSpin->setObjectName( "trackSpin" );
590 591 592 593 594 595
            addItem(i18n("T&rack:"), m_trackSpin, trackRowLayout);
            m_trackSpin->installEventFilter(this);

            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
                                                    QSizePolicy::Minimum));

596 597
            m_yearSpin = new KIntSpinBox(0, 9999, 1, 0, this );
            m_yearSpin->setObjectName( "yearSpin" );
598 599 600 601 602 603 604
            addItem(i18n("&Year:"), m_yearSpin, trackRowLayout);
            m_yearSpin->installEventFilter(this);

            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
                                                    QSizePolicy::Minimum));

            trackRowLayout->addWidget(addHidden(new QLabel(i18n("Length:"), this)));
605 606
            m_lengthBox = new KLineEdit(this);
            m_lengthBox->setObjectName( "lengthBox" );
607 608 609 610 611 612 613 614 615 616 617
            // addItem(i18n("Length:"), m_lengthBox, trackRowLayout);
            m_lengthBox->setMinimumWidth(fontMetrics().width("00:00") + trackRowLayout->spacing());
            m_lengthBox->setMaximumWidth(50);
            m_lengthBox->setAlignment(Qt::AlignRight);
            m_lengthBox->setReadOnly(true);
            trackRowLayout->addWidget(addHidden(m_lengthBox));

            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
                                                    QSizePolicy::Minimum));

            trackRowLayout->addWidget(addHidden(new QLabel(i18n("Bitrate:"), this)));
618 619
            m_bitrateBox = new KLineEdit(this);
            m_bitrateBox->setObjectName( "bitrateBox" );
620 621 622 623 624 625 626 627
            // addItem(i18n("Bitrate:"), m_bitrateBox, trackRowLayout);
            m_bitrateBox->setMinimumWidth(fontMetrics().width("000") + trackRowLayout->spacing());
            m_bitrateBox->setMaximumWidth(50);
            m_bitrateBox->setAlignment(Qt::AlignRight);
            m_bitrateBox->setReadOnly(true);
            trackRowLayout->addWidget(addHidden(m_bitrateBox));

            trackRowLayout->setWidth();
628 629
        }

630
        m_commentBox = new KTextEdit(this);
David Faure's avatar
David Faure committed
631
        m_commentBox->setObjectName( "commentBox" );
632
        addItem(i18n("&Comment:"), m_commentBox, rightColumnLayout, "edit");
633
        fileNameLabel->setMinimumHeight(m_trackSpin->height());
634

635 636
    }

Scott Wheeler's avatar
Scott Wheeler committed
637
    connect(m_artistNameBox, SIGNAL(textChanged(const QString&)),
638
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
639 640

    connect(m_trackNameBox, SIGNAL(textChanged(const QString&)),
641
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
642 643

    connect(m_albumNameBox, SIGNAL(textChanged(const QString&)),
644
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
645 646

    connect(m_genreBox, SIGNAL(activated(int)),
647
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
648 649

    connect(m_genreBox, SIGNAL(textChanged(const QString&)),
650
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
651 652

    connect(m_fileNameBox, SIGNAL(textChanged(const QString&)),
653
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
654 655

    connect(m_yearSpin, SIGNAL(valueChanged(int)),
656
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
657 658

    connect(m_trackSpin, SIGNAL(valueChanged(int)),
659
            this, SLOT(slotDataChanged()));
Scott Wheeler's avatar
Scott Wheeler committed
660 661

    connect(m_commentBox, SIGNAL(textChanged()),
662
            this, SLOT(slotDataChanged()));
663 664 665 666
}

void TagEditor::save(const PlaylistItemList &list)
{
667
    if(!list.isEmpty() && m_dataChanged) {
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684

        KApplication::setOverrideCursor(Qt::WaitCursor);
        m_dataChanged = false;
        m_performingSave = true;

        // The list variable can become corrupted if the playlist holding its
        // items dies, which is possible as we edit tags.  So we need to copy
        // the end marker.

        PlaylistItemList::ConstIterator end = list.end();

        for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberatly missing */ ) {

            // Process items before we being modifying tags, as the dynamic
            // playlists will try to modify the file we edit if the tag changes
            // due to our alterations here.

685
            qApp->processEvents(QEventLoop::ExcludeUserInput);
686 687 688 689 690 691 692 693 694

            PlaylistItem *item = *it;

            // The playlist can be deleted from under us if this is the last
            // item and we edit it so that it doesn't match the search, which
            // means we can't increment the iterator, so let's do it now.

            ++it;

Tim Beaulen's avatar
Tim Beaulen committed
695
            QString fileName = item->file().fileInfo().path() + QDir::separator() +
696 697
                               m_fileNameBox->text();
            if(list.count() > 1)
Tim Beaulen's avatar
Tim Beaulen committed
698
                fileName = item->file().fileInfo().absoluteFilePath();
699 700 701 702 703 704 705 706 707

            Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName);

            // A bit more ugliness.  If there are multiple files that are
            // being modified, they each have a "enabled" checkbox that
            // says if that field is to be respected for the multiple
            // files.  We have to check to see if that is enabled before
            // each field that we write.

Dirk Mueller's avatar
Dirk Mueller committed
708
            if(m_enableBoxes[m_artistNameBox]->isChecked())
709
                tag->setArtist(m_artistNameBox->currentText());
Dirk Mueller's avatar
Dirk Mueller committed
710
            if(m_enableBoxes[m_trackNameBox]->isChecked())
711
                tag->setTitle(m_trackNameBox->text());
Dirk Mueller's avatar
Dirk Mueller committed
712
            if(m_enableBoxes[m_albumNameBox]->isChecked())
713
                tag->setAlbum(m_albumNameBox->currentText());
Dirk Mueller's avatar
Dirk Mueller committed
714
            if(m_enableBoxes[m_trackSpin]->isChecked()) {
715 716 717 718
                if(m_trackSpin->text().isEmpty())
                    m_trackSpin->setValue(0);
                tag->setTrack(m_trackSpin->value());
            }
Dirk Mueller's avatar
Dirk Mueller committed
719
            if(m_enableBoxes[m_yearSpin]->isChecked()) {
720 721 722 723
                if(m_yearSpin->text().isEmpty())
                    m_yearSpin->setValue(0);
                tag->setYear(m_yearSpin->value());
            }
Dirk Mueller's avatar
Dirk Mueller committed
724
            if(m_enableBoxes[m_commentBox]->isChecked())
725
                tag->setComment(m_commentBox->toPlainText());
726

Dirk Mueller's avatar
Dirk Mueller committed
727
            if(m_enableBoxes[m_genreBox]->isChecked())
728 729 730 731 732 733 734 735 736
                tag->setGenre(m_genreBox->currentText());

            TagTransactionManager::instance()->changeTagOnItem(item, tag);
        }

        TagTransactionManager::instance()->commit();
        CollectionList::instance()->dataChanged();
        m_performingSave = false;
        KApplication::restoreOverrideCursor();
737 738 739 740 741
    }
}

void TagEditor::saveChangesPrompt()
{
742
    if(!isVisible() || !m_dataChanged || m_items.isEmpty())
743
        return;
744 745 746

    QStringList files;

Laurent Montel's avatar
Laurent Montel committed
747
    for(PlaylistItemList::Iterator it = m_items.begin(); it != m_items.end(); ++it)
748
        files.append((*it)->file().absFilePath());
749 750

    if(KMessageBox::questionYesNoList(this,
751 752 753
                                      i18n("Do you want to save your changes to:\n"),
                                      files,
                                      i18n("Save Changes"),
Aaron J. Seigo's avatar
build  
Aaron J. Seigo committed
754 755
                                      KStandardGuiItem::save(),
                                      KStandardGuiItem::discard(),
756
                                      "tagEditor_showSaveChangesBox") == KMessageBox::Yes)
757
    {
758
        save(m_items);
759 760 761
    }
}

762
void TagEditor::addItem(const QString &text, QWidget *item, QBoxLayout *layout, const QString &iconName)
763 764
{
    if(!item || !layout)
765
        return;
766

Stephan Kulow's avatar
Stephan Kulow committed
767 768 769 770
    QLabel *label = new QLabel(text, this);
    label->setBuddy( item );
    QLabel *iconLabel = new QLabel(0, this);
    iconLabel->setBuddy( item );
771

772
    if(!iconName.isNull())
773 774
        iconLabel->setPixmap(SmallIcon(iconName));

775 776 777 778 779 780
    QCheckBox *enableBox = new QCheckBox(i18n("Enable"), this);
    enableBox->setChecked(true);

    label->setMinimumHeight(enableBox->height());

    if(layout->direction() == QBoxLayout::LeftToRight) {
781
        layout->addWidget(iconLabel);
782 783 784
        layout->addWidget(label);
        layout->addWidget(item);
        layout->addWidget(enableBox);
785 786
    }
    else {
Stephan Kulow's avatar
Stephan Kulow committed
787 788
        QHBoxLayout *l = new QHBoxLayout();
        layout->addItem(l);
789 790 791 792

        l->addWidget(iconLabel);
        l->addWidget(label);
        l->setStretchFactor(label, 1);
793

794
        l->insertStretch(-1, 1);
795

796 797
        l->addWidget(enableBox);
        l->setStretchFactor(enableBox, 0);
798

799
        layout->addWidget(item);
800 801 802 803 804
    }

    enableBox->hide();

    connect(enableBox, SIGNAL(toggled(bool)), item, SLOT(setEnabled(bool)));
805
    m_enableBoxes.insert(item, enableBox);
806 807
}

808 809
void TagEditor::showEvent(QShowEvent *e)
{
810
    if(m_collectionChanged) {
811 812
        updateCollection();
        slotRefresh();
813
    }
814

815 816 817
    QWidget::showEvent(e);
}

818 819
bool TagEditor::eventFilter(QObject *watched, QEvent *e)
{
820
    QKeyEvent *ke = static_cast<QKeyEvent*>(e);
Tim Beaulen's avatar
Tim Beaulen committed
821
    if(watched->inherits("QSpinBox") && e->type() == QEvent::KeyRelease && ke->modifiers() == 0)
822
        slotDataChanged();
823 824 825 826

    return false;
}

827 828 829 830
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////

831
void TagEditor::slotDataChanged(bool c)
832
{
833
    m_dataChanged = c;
834 835
}

836 837 838 839
void TagEditor::slotItemRemoved(PlaylistItem *item)
{
    m_items.remove(item);
    if(m_items.isEmpty())
840
        slotRefresh();
841 842
}

843 844 845
void TagEditor::slotPlaylistDestroyed(Playlist *p)
{
    if(m_currentPlaylist == p) {
846 847
        m_currentPlaylist = 0;
        slotSetItems(PlaylistItemList());
848 849 850
    }
}

851
#include "tageditor.moc"
852 853

// vim: set et sw=4 tw=0 sta: