tageditor.cpp 20.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/**
 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
16

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

25
#include <kactioncollection.h>
26 27
#include <kcombobox.h>
#include <klineedit.h>
28
#include <ktextedit.h>
29
#include <kmessagebox.h>
Michael Pyne's avatar
Michael Pyne committed
30 31
#include <KSharedConfig>
#include <KConfigGroup>
32
#include <kiconloader.h>
33
#include <ktoggleaction.h>
Michael Pyne's avatar
Michael Pyne committed
34
#include <KLocalizedString>
35

Michael Pyne's avatar
Michael Pyne committed
36 37
#include <QAction>
#include <QIcon>
38
#include <QLabel>
39
#include <QApplication>
40 41
#include <QCheckBox>
#include <QDir>
42 43
#include <QValidator>
#include <QEventLoop>
Laurent Montel's avatar
Laurent Montel committed
44 45 46
#include <QKeyEvent>
#include <QHBoxLayout>
#include <QVBoxLayout>
47
#include <QSizePolicy>
48

49 50
#include <id3v1genres.h>

51 52
#undef KeyRelease

53
class FileNameValidator final : public QValidator
54 55 56
{
public:
    FileNameValidator(QObject *parent, const char *name = 0) :
57
        QValidator(parent)
Tim Beaulen's avatar
Tim Beaulen committed
58
    {
59
        setObjectName( QLatin1String( name ) );
Tim Beaulen's avatar
Tim Beaulen committed
60
    }
61

62
    virtual void fixup(QString &s) const override
63
    {
64
        s.remove('/');
65 66
    }

67
    virtual State validate(QString &s, int &) const override
68
    {
Stephan Kulow's avatar
Stephan Kulow committed
69
        if(s.contains('/'))
70 71
           return Invalid;
        return Acceptable;
72 73 74
    }
};

75
class FixedHLayout final : public QHBoxLayout
76 77 78
{
public:
    FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) :
Stephan Kulow's avatar
Stephan Kulow committed
79 80 81
        QHBoxLayout(parent),
        m_width(-1)
    {
Scott Wheeler's avatar
Scott Wheeler committed
82 83
        setMargin(margin);
        setSpacing(spacing);
Arnold Dumas's avatar
Arnold Dumas committed
84
        setObjectName(QLatin1String(name));
Stephan Kulow's avatar
Stephan Kulow committed
85
    }
86
    FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) :
Stephan Kulow's avatar
Stephan Kulow committed
87 88 89
        QHBoxLayout(),
        m_width(-1)
    {
Scott Wheeler's avatar
Scott Wheeler committed
90 91
        parentLayout->addItem(this);
        setSpacing(spacing);
Arnold Dumas's avatar
Arnold Dumas committed
92
        setObjectName(QLatin1String(name));
Stephan Kulow's avatar
Stephan Kulow committed
93
    }
94 95
    void setWidth(int w = -1)
    {
96
        m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w;
97
    }
98
    virtual QSize minimumSize() const override
99
    {
100 101 102
        QSize s = QHBoxLayout::minimumSize();
        s.setWidth(m_width);
        return s;
103 104 105 106 107
    }
private:
    int m_width;
};

108
class CollectionObserver final : public PlaylistObserver
109 110 111
{
public:
    CollectionObserver(TagEditor *parent) :
112 113
        PlaylistObserver(CollectionList::instance()),
        m_parent(parent)
114 115 116
    {
    }

117
    virtual void playlistItemDataHasChanged() override
118
    {
119 120
        if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible())
            m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems());
121 122 123 124 125 126
    }

private:
    TagEditor *m_parent;
};

127 128 129 130
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

Dirk Mueller's avatar
Dirk Mueller committed
131 132
TagEditor::TagEditor(QWidget *parent) :
    QWidget(parent),
133
    m_currentPlaylist(0),
134 135
    m_observer(0),
    m_performingSave(false)
136
{
137
    setupActions();
138 139
    setupLayout();
    readConfig();
140
    m_dataChanged = false;
141
    m_collectionChanged = false;
142 143

    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
144 145 146 147
}

TagEditor::~TagEditor()
{
148
    delete m_observer;
149 150 151
    saveConfig();
}

152 153 154 155 156
void TagEditor::setupObservers()
{
    m_observer = new CollectionObserver(this);
}

157 158 159 160
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

161
void TagEditor::slotSetItems(const PlaylistItemList &list)
162
{
163
    if(m_performingSave)
164
        return;
165

166 167 168 169 170
    // Store the playlist that we're setting because saveChangesPrompt
    // can delete the PlaylistItems in list.

    Playlist *itemPlaylist = 0;
    if(!list.isEmpty())
171
        itemPlaylist = list.first()->playlist();
172 173 174

    bool hadPlaylist = m_currentPlaylist != 0;

175
    saveChangesPrompt();
176 177

    if(m_currentPlaylist) {
Laurent Montel's avatar
Laurent Montel committed
178 179
        disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)),
                   this, SLOT(slotItemRemoved(PlaylistItem*)));
180 181
    }

182
    if((hadPlaylist && !m_currentPlaylist) || !itemPlaylist) {
183 184
        m_currentPlaylist = 0;
        m_items.clear();
185 186
    }
    else {
187
        m_currentPlaylist = itemPlaylist;
188

189
        // We can't use list here, it may not be valid
190

191
        m_items = itemPlaylist->selectedItems();
192
    }
193 194

    if(m_currentPlaylist) {
Laurent Montel's avatar
Laurent Montel committed
195 196
        connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)),
                this, SLOT(slotItemRemoved(PlaylistItem*)));
197
        connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved()));
198 199
    }

200
    if(isVisible())
201
        slotRefresh();
202
    else
203
        m_collectionChanged = true;
204 205
}

206
void TagEditor::slotRefresh()
207
{
208
    // This method takes the list of currently selected m_items and tries to
209 210
    // 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
211
    // check after that to see if there are other m_items and adjust accordingly.
212

213
    if(m_items.isEmpty() || !m_items.first()->file().tag()) {
214 215 216
        slotClear();
        setEnabled(false);
        return;
217
    }
218

219 220
    setEnabled(true);

221
    PlaylistItem *item = m_items.first();
222 223

    Q_ASSERT(item);
224

225
    Tag *tag = item->file().tag();
226

227 228
    QFileInfo fi(item->file().absFilePath());
    if(!fi.isWritable() && m_items.count() == 1)
229
        setEnabled(false);
230

231 232 233
    artistNameBox->setEditText(tag->artist());
    trackNameBox->setText(tag->title());
    albumNameBox->setEditText(tag->album());
234

235 236
    fileNameBox->setText(item->file().fileInfo().fileName());
    fileNameBox->setToolTip(item->file().absFilePath());
237

238 239
    bitrateBox->setText(QString::number(tag->bitrate()));
    lengthBox->setText(tag->lengthString());
240

Tim Beaulen's avatar
Tim Beaulen committed
241
    if(m_genreList.indexOf(tag->genre()) >= 0)
242
        genreBox->setCurrentIndex(m_genreList.indexOf(tag->genre()) + 1);
243
    else {
244 245
        genreBox->setCurrentIndex(0);
        genreBox->setEditText(tag->genre());
246
    }
247

248 249
    trackSpin->setValue(tag->track());
    yearSpin->setValue(tag->year());
250

251
    commentBox->setPlainText(tag->comment());
252

253
    // Start at the second item, since we've already processed the first.
254

255 256 257 258
    PlaylistItemList::Iterator it = m_items.begin();
    ++it;

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

260 261 262 263 264 265 266 267 268 269 270 271
    QList<QWidget *> disabledForMulti;

    disabledForMulti << fileNameLabel << fileNameBox << lengthLabel << lengthBox
                     << bitrateLabel << bitrateBox;

    foreach(QWidget *w, disabledForMulti) {
        w->setDisabled(m_items.size() > 1);
        if(m_items.size() > 1 && !w->inherits("QLabel"))
            QMetaObject::invokeMethod(w, "clear");
    }

    if(it != m_items.end()) {
272

273 274 275
        foreach(QCheckBox *box, m_enableBoxes) {
            box->setChecked(true);
            box->show();
276 277 278 279 280 281 282 283 284
        }

        // 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) {
285 286 287 288 289 290 291
            m_enableBoxes[artistNameBox]->setChecked(false);
            m_enableBoxes[trackNameBox]->setChecked(false);
            m_enableBoxes[albumNameBox]->setChecked(false);
            m_enableBoxes[genreBox]->setChecked(false);
            m_enableBoxes[trackSpin]->setChecked(false);
            m_enableBoxes[yearSpin]->setChecked(false);
            m_enableBoxes[commentBox]->setChecked(false);
292 293 294 295 296 297 298
        }
        else {
            for(; it != m_items.end(); ++it) {
                tag = (*it)->file().tag();

                if(tag) {

299 300
                    if(artistNameBox->currentText() != tag->artist() &&
                       m_enableBoxes.contains(artistNameBox))
301
                    {
302 303
                        artistNameBox->lineEdit()->clear();
                        m_enableBoxes[artistNameBox]->setChecked(false);
304
                    }
305 306
                    if(trackNameBox->text() != tag->title() &&
                       m_enableBoxes.contains(trackNameBox))
307
                    {
308 309
                        trackNameBox->clear();
                        m_enableBoxes[trackNameBox]->setChecked(false);
310
                    }
311 312
                    if(albumNameBox->currentText() != tag->album() &&
                       m_enableBoxes.contains(albumNameBox))
313
                    {
314 315
                        albumNameBox->lineEdit()->clear();
                        m_enableBoxes[albumNameBox]->setChecked(false);
316
                    }
317 318
                    if(genreBox->currentText() != tag->genre() &&
                       m_enableBoxes.contains(genreBox))
319
                    {
320 321
                        genreBox->lineEdit()->clear();
                        m_enableBoxes[genreBox]->setChecked(false);
322
                    }
323 324
                    if(trackSpin->value() != tag->track() &&
                       m_enableBoxes.contains(trackSpin))
325
                    {
326 327
                        trackSpin->setValue(0);
                        m_enableBoxes[trackSpin]->setChecked(false);
328
                    }
329 330
                    if(yearSpin->value() != tag->year() &&
                       m_enableBoxes.contains(yearSpin))
331
                    {
332 333
                        yearSpin->setValue(0);
                        m_enableBoxes[yearSpin]->setChecked(false);
334
                    }
335 336
                    if(commentBox->toPlainText() != tag->comment() &&
                       m_enableBoxes.contains(commentBox))
337
                    {
338 339
                        commentBox->clear();
                        m_enableBoxes[commentBox]->setChecked(false);
340 341 342 343
                    }
                }
            }
        }
344
    }
345
    else {
346 347 348
        foreach(QCheckBox *box, m_enableBoxes) {
            box->setChecked(true);
            box->hide();
349
        }
350 351
    }
    m_dataChanged = false;
352 353
}

354
void TagEditor::slotClear()
355
{
356 357 358 359 360 361 362 363 364 365 366
    artistNameBox->lineEdit()->clear();
    trackNameBox->clear();
    albumNameBox->lineEdit()->clear();
    genreBox->setCurrentIndex(0);
    fileNameBox->clear();
    fileNameBox->setToolTip(QString());
    trackSpin->setValue(0);
    yearSpin->setValue(0);
    lengthBox->clear();
    bitrateBox->clear();
    commentBox->clear();
367 368
}

369
void TagEditor::slotUpdateCollection()
370
{
371
    if(isVisible())
372
        updateCollection();
373
    else
374
        m_collectionChanged = true;
375 376 377 378 379
}

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

381 382 383
    CollectionList *list = CollectionList::instance();

    if(!list)
384 385
        return;

386
    QStringList artistList = list->uniqueSet(CollectionList::Artists);
387
    artistList.sort();
388 389 390
    artistNameBox->clear();
    artistNameBox->addItems(artistList);
    artistNameBox->completionObject()->setItems(artistList);
391 392

    QStringList albumList = list->uniqueSet(CollectionList::Albums);
393
    albumList.sort();
394 395 396
    albumNameBox->clear();
    albumNameBox->addItems(albumList);
    albumNameBox->completionObject()->setItems(albumList);
397

398
    // Merge the list of genres found in tags with the standard ID3v1 set.
399

400
    StringHash genreHash;
401 402

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

404 405
    foreach(const QString &genre, m_genreList)
        genreHash.insert(genre);
406 407 408

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

Laurent Montel's avatar
Laurent Montel committed
409
    for(TagLib::StringList::Iterator it = genres.begin(); it != genres.end(); ++it)
410
        genreHash.insert(TStringToQString((*it)));
411

412
    m_genreList = genreHash.values();
413 414
    m_genreList.sort();

415 416 417 418
    genreBox->clear();
    genreBox->addItem(QString());
    genreBox->addItems(m_genreList);
    genreBox->completionObject()->setItems(m_genreList);
419 420 421 422

    // We've cleared out the original entries of these list boxes, re-read
    // the current item if one is selected.
    slotRefresh();
423 424 425 426 427 428
}

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

429
void TagEditor::readConfig()
430
{
431
    // combo box completion modes
432

433
    KConfigGroup config(KSharedConfig::openConfig(), "TagEditor");
434 435 436 437
    if(artistNameBox && albumNameBox) {
        readCompletionMode(config, artistNameBox, "ArtistNameBoxMode");
        readCompletionMode(config, albumNameBox, "AlbumNameBoxMode");
        readCompletionMode(config, genreBox, "GenreBoxMode");
438
    }
439

440
    bool show = config.readEntry("Show", false);
441
    ActionCollection::action<KToggleAction>("showEditor")->setChecked(show);
Tim Beaulen's avatar
Tim Beaulen committed
442
    setVisible(show);
443

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

446
    for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
447
        m_genreList.append(TStringToQString((*it)));
448
    m_genreList.sort();
449

450 451 452 453
    genreBox->clear();
    genreBox->addItem(QString());
    genreBox->addItems(m_genreList);
    genreBox->completionObject()->setItems(m_genreList);
454 455
}

Stephan Kulow's avatar
Stephan Kulow committed
456
void TagEditor::readCompletionMode(const KConfigGroup &config, KComboBox *box, const QString &key)
457
{
458 459
    KCompletion::CompletionMode mode =
        KCompletion::CompletionMode(config.readEntry(key, (int)KCompletion::CompletionAuto));
460

461
    box->setCompletionMode(mode);
462
}
463

464 465
void TagEditor::saveConfig()
{
466
    // combo box completion modes
467

468
    KConfigGroup config(KSharedConfig::openConfig(), "TagEditor");
469

470 471 472 473
    if(artistNameBox && albumNameBox) {
        config.writeEntry("ArtistNameBoxMode", (int)artistNameBox->completionMode());
        config.writeEntry("AlbumNameBoxMode", (int)albumNameBox->completionMode());
        config.writeEntry("GenreBoxMode", (int)genreBox->completionMode());
474
    }
475
    config.writeEntry("Show", ActionCollection::action<KToggleAction>("showEditor")->isChecked());
476
}
477

478 479
void TagEditor::setupActions()
{
Michael Pyne's avatar
Michael Pyne committed
480
    KToggleAction *show = new KToggleAction(QIcon::fromTheme(QLatin1String("document-properties")),
481
                                            i18n("Show &Tag Editor"), this);
482
    ActionCollection::actions()->addAction("showEditor", show);
483
    connect(show, &QAction::toggled, this, &TagEditor::setVisible);
484

Michael Pyne's avatar
Michael Pyne committed
485
    QAction *act = new QAction(QIcon::fromTheme(QLatin1String( "document-save")), i18n("&Save"), this);
486
    ActionCollection::actions()->addAction("saveItem", act);
487 488 489
    ActionCollection::actions()->setDefaultShortcut(act,
            QKeySequence(Qt::CTRL + Qt::Key_T));
    connect(act, &QAction::triggered, this, &TagEditor::slotSave);
490 491
}

492 493
void TagEditor::setupLayout()
{
494 495
    setupUi(this);

496 497
    // Do some meta-programming to find the matching enable boxes

498 499 500 501 502 503 504 505 506 507 508 509 510 511
    for(auto enable : findChildren<QCheckBox *>(QRegExp("Enable$"))) {
        enable->hide(); // These are shown only when multiple items are being edited

        // Each enable checkbox is identified by having its objectName end in "Enable".
        // The corresponding widget to be adjusted is identified by assigning a custom
        // property in Qt Designer "associatedObjectName", the value of which is the name
        // for the widget to be enabled (or not).
        auto associatedVariantValue = enable->property("associatedObjectName");
        Q_ASSERT(associatedVariantValue.isValid());

        QWidget *associatedWidget = findChild<QWidget *>(associatedVariantValue.toString());
        Q_ASSERT(associatedWidget != nullptr);

        m_enableBoxes[associatedWidget] = enable;
512
    }
513

514 515
    // Make sure that the labels are as tall as the enable boxes so that the
    // layout doesn't jump around as the enable boxes are shown/hidden.
516

517
    for(auto label : findChildren<QLabel *>()) {
518 519 520 521 522
        if(m_enableBoxes.contains(label->buddy()))
            label->setMinimumHeight(m_enableBoxes[label->buddy()]->height());
    }

    tagEditorLayout->setColumnMinimumWidth(1, 200);
523 524 525 526
}

void TagEditor::save(const PlaylistItemList &list)
{
527
    if(!list.isEmpty() && m_dataChanged) {
528

529
        QApplication::setOverrideCursor(Qt::WaitCursor);
530 531 532 533 534 535 536 537 538
        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();

539
        for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberately missing */ ) {
540 541 542 543 544

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

545
            qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
546 547 548 549 550 551 552 553 554

            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
555
            QString fileName = item->file().fileInfo().path() + QDir::separator() +
556
                               fileNameBox->text();
557
            if(list.count() > 1)
Tim Beaulen's avatar
Tim Beaulen committed
558
                fileName = item->file().fileInfo().absoluteFilePath();
559 560 561 562 563 564 565 566 567

            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.

568 569 570 571 572 573 574 575 576 577
            if(m_enableBoxes[artistNameBox]->isChecked())
                tag->setArtist(artistNameBox->currentText());
            if(m_enableBoxes[trackNameBox]->isChecked())
                tag->setTitle(trackNameBox->text());
            if(m_enableBoxes[albumNameBox]->isChecked())
                tag->setAlbum(albumNameBox->currentText());
            if(m_enableBoxes[trackSpin]->isChecked()) {
                if(trackSpin->text().isEmpty())
                    trackSpin->setValue(0);
                tag->setTrack(trackSpin->value());
578
            }
579 580 581 582
            if(m_enableBoxes[yearSpin]->isChecked()) {
                if(yearSpin->text().isEmpty())
                    yearSpin->setValue(0);
                tag->setYear(yearSpin->value());
583
            }
584 585
            if(m_enableBoxes[commentBox]->isChecked())
                tag->setComment(commentBox->toPlainText());
586

587 588
            if(m_enableBoxes[genreBox]->isChecked())
                tag->setGenre(genreBox->currentText());
589 590 591 592 593

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

        TagTransactionManager::instance()->commit();
594
        CollectionList::instance()->playlistItemsChanged();
595
        m_performingSave = false;
596
        QApplication::restoreOverrideCursor();
597 598 599 600 601
    }
}

void TagEditor::saveChangesPrompt()
{
602
    if(!isVisible() || !m_dataChanged || m_items.isEmpty())
603
        return;
604 605 606

    QStringList files;

607 608
    foreach(const PlaylistItem *item, m_items)
        files.append(item->file().absFilePath());
609 610

    if(KMessageBox::questionYesNoList(this,
611 612 613
                                      i18n("Do you want to save your changes to:\n"),
                                      files,
                                      i18n("Save Changes"),
Aaron J. Seigo's avatar
Aaron J. Seigo committed
614 615
                                      KStandardGuiItem::save(),
                                      KStandardGuiItem::discard(),
616
                                      "tagEditor_showSaveChangesBox") == KMessageBox::Yes)
617
    {
618
        save(m_items);
619 620 621
    }
}

622 623
void TagEditor::showEvent(QShowEvent *e)
{
624
    if(m_collectionChanged) {
625
        updateCollection();
626
    }
627

628 629 630
    QWidget::showEvent(e);
}

631 632 633 634
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////

635
void TagEditor::slotDataChanged()
636
{
637
    m_dataChanged = true;
638 639
}

640 641
void TagEditor::slotItemRemoved(PlaylistItem *item)
{
642
    m_items.removeAll(item);
643
    if(m_items.isEmpty())
644
        slotRefresh();
645 646
}

647 648 649
void TagEditor::slotPlaylistDestroyed(Playlist *p)
{
    if(m_currentPlaylist == p) {
650 651
        m_currentPlaylist = 0;
        slotSetItems(PlaylistItemList());
652 653 654
    }
}

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