playlist.cpp 58 KB
Newer Older
1 2
/**
 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
3
 * Copyright (C) 2008-2018 Michael Pyne <mpyne@kde.org>
4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * 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/>.
 */
17

18
#include "playlist.h"
19
#include "juk-exception.h"
20

Michael Pyne's avatar
Michael Pyne committed
21 22
#include <KLocalizedString>
#include <KSharedConfig>
Scott Wheeler's avatar
Scott Wheeler committed
23
#include <kconfig.h>
24
#include <kmessagebox.h>
25
#include <kiconloader.h>
26
#include <klineedit.h>
27
#include <kio/copyjob.h>
28 29 30
#include <kactioncollection.h>
#include <kconfiggroup.h>
#include <ktoolbarpopupaction.h>
Laurent Montel's avatar
Laurent Montel committed
31 32 33
#include <kactionmenu.h>
#include <ktoggleaction.h>
#include <kselectaction.h>
34 35

#include <QCursor>
36
#include <QDir>
37
#include <QDirIterator>
38
#include <QHash>
39 40
#include <QToolTip>
#include <QFile>
Michael Pyne's avatar
Michael Pyne committed
41
#include <QFileDialog>
42
#include <QHeaderView>
Laurent Montel's avatar
Laurent Montel committed
43 44 45
#include <QResizeEvent>
#include <QMouseEvent>
#include <QKeyEvent>
46
#include <QMimeData>
Michael Pyne's avatar
Michael Pyne committed
47
#include <QMenu>
48 49
#include <QTimer>
#include <QClipboard>
Laurent Montel's avatar
Laurent Montel committed
50 51 52
#include <QTextStream>
#include <QDropEvent>
#include <QDragEnterEvent>
53
#include <QPixmap>
54
#include <QStackedWidget>
55
#include <QScrollBar>
56
#include <QPainter>
57 58

#include <QtConcurrent>
59
#include <QFutureWatcher>
60

61 62
#include <id3v1genres.h>

63
#include <time.h>
64
#include <cmath>
65
#include <algorithm>
66

67
#include "directoryloader.h"
68
#include "playlistitem.h"
69
#include "playlistcollection.h"
70
#include "playlistsearch.h"
71
#include "playlistsharedsettings.h"
72
#include "mediafiles.h"
73
#include "collectionlist.h"
74
#include "filerenamer.h"
75
#include "actioncollection.h"
76
#include "tracksequencemanager.h"
77
#include "tag.h"
78
#include "upcomingplaylist.h"
79
#include "deletedialog.h"
80
#include "webimagefetcher.h"
81
#include "coverinfo.h"
82
#include "coverdialog.h"
83
#include "tagtransactionmanager.h"
84
#include "cache.h"
Michael Pyne's avatar
Michael Pyne committed
85
#include "juk_debug.h"
86

87 88
using namespace ActionCollection;

89 90 91 92 93 94
/**
 * Used to give every track added in the program a unique identifier. See
 * PlaylistItem
 */
quint32 g_trackID = 0;

95 96 97 98 99 100 101 102 103
/**
 * Just a shortcut of sorts.
 */

static bool manualResize()
{
    return action<KToggleAction>("resizeColumnsManually")->isChecked();
}

104
////////////////////////////////////////////////////////////////////////////////
105
// static members
106 107
////////////////////////////////////////////////////////////////////////////////

108 109 110 111 112
bool                    Playlist::m_visibleChanged = false;
bool                    Playlist::m_shuttingDown   = false;
PlaylistItemList        Playlist::m_history;
QVector<PlaylistItem *> Playlist::m_backMenuItems;
int                     Playlist::m_leftColumn     = 0;
113

114
////////////////////////////////////////////////////////////////////////////////
115
// public members
116 117
////////////////////////////////////////////////////////////////////////////////

118 119 120 121 122 123 124 125
Playlist::Playlist(
        bool delaySetup, const QString &name,
        PlaylistCollection *collection, const QString &iconName,
        int extraCols)
    : QTreeWidget(collection->playlistStack())
    , m_collection(collection)
    , m_playlistName(name)
    , m_fetcher(new WebImageFetcher(this))
126
{
127 128 129 130 131 132
    // Any added columns must precede normal ones, which are normally added
    // in setup()
    for(int i = 0; i < extraCols; ++i) {
        addColumn(i18n("JuK")); // Placeholder text
    }

133
    setup();
134 135 136 137 138 139 140 141 142 143 144 145

    // Some subclasses need to do even more handling but will remember to
    // call setupPlaylist
    if(!delaySetup) {
        collection->setupPlaylist(this, iconName);
    }
}

Playlist::Playlist(PlaylistCollection *collection, const QString &name,
                   const QString &iconName)
    : Playlist(false, name, collection, iconName, 0)
{
146 147
}

148
Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items,
149 150
                   const QString &name, const QString &iconName)
    : Playlist(false, name, collection, iconName, 0)
151 152 153 154 155
{
    createItems(items);
}

Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile,
156 157
                   const QString &iconName)
    : Playlist(true, QString(), collection, iconName, 0)
158
{
159
    m_fileName = playlistFile.canonicalFilePath();
160
    loadFile(m_fileName, playlistFile);
161 162 163
    collection->setupPlaylist(this, iconName);
}

164 165
Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns)
    : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns)
166
{
167 168 169 170
}

Playlist::~Playlist()
{
171 172 173 174 175 176
    // In some situations the dataChanged signal from clearItems will cause observers to
    // subsequently try to access a deleted item.  Since we're going away just remove all
    // observers.

    clearObservers();

177 178 179 180
    // clearItem() will take care of removing the items from the history,
    // so call clearItems() to make sure it happens.

    clearItems(items());
181

182
    if(!m_shuttingDown)
183
        m_collection->removePlaylist(this);
184 185
}

186 187
QString Playlist::name() const
{
188
    if(m_playlistName.isEmpty())
189
        return m_fileName.section(QDir::separator(), -1).section('.', 0, -2);
190
    else
191
        return m_playlistName;
192 193 194 195
}

FileHandle Playlist::currentFile() const
{
196
    return playingItem() ? playingItem()->file() : FileHandle();
197 198
}

199 200
void Playlist::playFirst()
{
201
    TrackSequenceManager::instance()->setNextItem(static_cast<PlaylistItem *>(
202
        *QTreeWidgetItemIterator(const_cast<Playlist *>(this), QTreeWidgetItemIterator::NotHidden)));
David Faure's avatar
David Faure committed
203
    action("forward")->trigger();
204 205
}

206 207 208 209
void Playlist::playNextAlbum()
{
    PlaylistItem *current = TrackSequenceManager::instance()->currentItem();
    if(!current)
210
        return; // No next album if we're not already playing.
211 212 213 214 215

    QString currentAlbum = current->file().tag()->album();
    current = TrackSequenceManager::instance()->nextItem();

    while(current && current->file().tag()->album() == currentAlbum)
216
        current = TrackSequenceManager::instance()->nextItem();
217 218

    TrackSequenceManager::instance()->setNextItem(current);
David Faure's avatar
David Faure committed
219
    action("forward")->trigger();
220 221
}

222 223
void Playlist::playNext()
{
224
    TrackSequenceManager::instance()->setCurrentPlaylist(this);
225
    setPlaying(TrackSequenceManager::instance()->nextItem());
226 227 228 229
}

void Playlist::stop()
{
230
    m_history.clear();
231
    setPlaying(nullptr);
232 233 234 235
}

void Playlist::playPrevious()
{
236
    if(!playingItem())
237
        return;
238 239 240

    bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked();

241
    PlaylistItem *previous = nullptr;
242 243

    if(random && !m_history.isEmpty()) {
244
        PlaylistItemList::Iterator last = m_history.end() - 1;
245
        previous = *last;
246
        m_history.erase(last);
247 248
    }
    else {
249 250
        m_history.clear();
        previous = TrackSequenceManager::instance()->previousItem();
251 252 253
    }

    if(!previous)
254
        previous = static_cast<PlaylistItem *>(playingItem()->itemAbove());
255

256
    setPlaying(previous, false);
257 258 259 260
}

void Playlist::setName(const QString &n)
{
261 262
    m_collection->addNameToDict(n);
    m_collection->removeNameFromDict(m_playlistName);
263

264 265 266 267
    m_playlistName = n;
    emit signalNameChanged(m_playlistName);
}

268
void Playlist::save()
269
{
270
    if(m_fileName.isEmpty())
271
        return saveAs();
272

273
    QFile file(m_fileName);
274

Laurent Montel's avatar
Laurent Montel committed
275
    if(!file.open(QIODevice::WriteOnly))
276
        return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName));
277

278 279 280 281
    QTextStream stream(&file);

    QStringList fileList = files();

282 283
    foreach(const QString &file, fileList)
        stream << file << endl;
284

285
    file.close();
286 287 288 289
}

void Playlist::saveAs()
{
290 291
    m_collection->removeFileFromDict(m_fileName);

292
    m_fileName = MediaFiles::savePlaylistDialog(name(), this);
293

294
    if(!m_fileName.isEmpty()) {
295
        m_collection->addFileToDict(m_fileName);
296

297 298 299 300
        // If there's no playlist name set, use the file name.
        if(m_playlistName.isEmpty())
            emit signalNameChanged(name());
        save();
301
    }
302 303
}

304
void Playlist::updateDeletedItem(PlaylistItem *item)
305
{
306
    m_members.remove(item->file().absFilePath());
307 308
    m_search.clearItem(item);

309
    m_history.removeAll(item);
310
}
311

312
void Playlist::clearItem(PlaylistItem *item)
313 314
{
    // Automatically updates internal structs via updateDeletedItem
315
    delete item;
316

317
    playlistItemsChanged();
318 319
}

320
void Playlist::clearItems(const PlaylistItemList &items)
321
{
322 323
    foreach(PlaylistItem *item, items)
        delete item;
324

325
    playlistItemsChanged();
326 327
}

328 329
PlaylistItem *Playlist::playingItem() // static
{
330
    return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front();
331 332
}

333
QStringList Playlist::files() const
334
{
335
    QStringList list;
336

337
    for(QTreeWidgetItemIterator it(const_cast<Playlist *>(this)); *it; ++it)
338
        list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath());
339

340
    return list;
341 342
}

343 344
PlaylistItemList Playlist::items()
{
345
    return items(QTreeWidgetItemIterator::IteratorFlag(0));
346 347
}

348
PlaylistItemList Playlist::visibleItems()
349
{
350
    return items(QTreeWidgetItemIterator::NotHidden);
351 352
}

353
PlaylistItemList Playlist::selectedItems()
354
{
355
    return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden);
356 357
}

358 359
PlaylistItem *Playlist::firstChild() const
{
360
    return static_cast<PlaylistItem *>(topLevelItem(0));
361 362
}

363 364 365 366 367
void Playlist::updateLeftColumn()
{
    int newLeftColumn = leftMostVisibleColumn();

    if(m_leftColumn != newLeftColumn) {
368 369
        updatePlaying();
        m_leftColumn = newLeftColumn;
370 371 372
    }
}

373
void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static
374
{
375
    m_visibleChanged = true;
376 377

    foreach(PlaylistItem *playlistItem, items)
378
        playlistItem->setHidden(!visible);
379 380
}

381 382 383 384 385
void Playlist::setSearch(const PlaylistSearch &s)
{
    m_search = s;

    if(!m_searchEnabled)
386
        return;
387 388 389

    setItemsVisible(s.matchedItems(), true);
    setItemsVisible(s.unmatchedItems(), false);
390 391

    TrackSequenceManager::instance()->iterator()->playlistChanged();
392 393 394 395 396
}

void Playlist::setSearchEnabled(bool enabled)
{
    if(m_searchEnabled == enabled)
397
        return;
398 399 400 401

    m_searchEnabled = enabled;

    if(enabled) {
402 403
        setItemsVisible(m_search.matchedItems(), true);
        setItemsVisible(m_search.unmatchedItems(), false);
404 405
    }
    else
406
        setItemsVisible(items(), true);
407 408
}

409 410 411
// Mostly seems to be for DynamicPlaylist
// TODO: See if this can't all be eliminated by making 'is-playing' a predicate
// of the playlist item itself
412 413
void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster)
{
414 415
    foreach(const Playlist *p, sources) {
        if(p->playing()) {
416
            CollectionListItem *base = playingItem()->collectionItem();
417 418
            for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) {
                PlaylistItem *item = static_cast<PlaylistItem *>(*itemIt);
419 420
                if(base == item->collectionItem()) {
                    item->setPlaying(true, setMaster);
421 422
                    PlaylistItemList playing = PlaylistItem::playingItems();
                    TrackSequenceManager::instance()->setCurrent(item);
423 424 425 426 427 428 429 430
                    return;
                }
            }
            return;
        }
    }
}

431 432 433 434
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

435 436
void Playlist::copy()
{
437
    PlaylistItemList items = selectedItems();
438
    QList<QUrl> urls;
439 440

    foreach(PlaylistItem *item, items) {
441
        urls << QUrl::fromLocalFile(item->file().absFilePath());
442 443 444
    }

    QMimeData *mimeData = new QMimeData;
445
    mimeData->setUrls(urls);
446 447

    QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
448 449 450 451
}

void Playlist::paste()
{
452 453 454
    addFilesFromMimeData(
        QApplication::clipboard()->mimeData(),
        static_cast<PlaylistItem *>(currentItem()));
455 456 457 458
}

void Playlist::clear()
{
459 460
    PlaylistItemList l = selectedItems();
    if(l.isEmpty())
461
        l = items();
462 463

    clearItems(l);
464 465
}

466 467 468 469
void Playlist::slotRefresh()
{
    PlaylistItemList l = selectedItems();
    if(l.isEmpty())
470
        l = visibleItems();
471

472
    QApplication::setOverrideCursor(Qt::WaitCursor);
473 474
    foreach(PlaylistItem *item, l) {
        item->refreshFromDisk();
475

476
        if(!item->file().tag() || !item->file().fileInfo().exists()) {
Michael Pyne's avatar
Michael Pyne committed
477
            qCDebug(JUK_LOG) << "Error while trying to refresh the tag.  "
478
                           << "This file has probably been removed.";
479
            delete item->collectionItem();
480
        }
481

482
        processEvents();
483
    }
484
    QApplication::restoreOverrideCursor();
485 486
}

487 488
void Playlist::slotRenameFile()
{
489
    FileRenamer renamer;
490
    PlaylistItemList items = selectedItems();
491 492

    if(items.isEmpty())
493 494
        return;

495 496
    emit signalEnableDirWatch(false);

497
    m_blockDataChanged = true;
498
    renamer.rename(items);
499
    m_blockDataChanged = false;
500
    playlistItemsChanged();
501

502
    emit signalEnableDirWatch(true);
503 504
}

505 506
void Playlist::slotViewCover()
{
507
    const PlaylistItemList items = selectedItems();
508 509
    if (items.isEmpty())
        return;
510 511
    foreach(const PlaylistItem *item, items)
        item->file().coverInfo()->popup();
512 513 514 515 516
}

void Playlist::slotRemoveCover()
{
    PlaylistItemList items = selectedItems();
517
    if(items.isEmpty())
518
        return;
519
    int button = KMessageBox::warningContinueCancel(this,
520
                                                    i18n("Are you sure you want to delete these covers?"),
521
                                                    QString(),
Laurent Montel's avatar
Laurent Montel committed
522
                                                    KGuiItem(i18n("&Delete Covers")));
523
    if(button == KMessageBox::Continue)
524
        refreshAlbums(items);
525 526
}

527 528 529 530 531
void Playlist::slotShowCoverManager()
{
    static CoverDialog *managerDialog = 0;

    if(!managerDialog)
532
        managerDialog = new CoverDialog(this);
533 534 535 536

    managerDialog->show();
}

537 538 539 540 541 542 543
void Playlist::slotAddCover(bool retrieveLocal)
{
    PlaylistItemList items = selectedItems();

    if(items.isEmpty())
        return;

544
    if(!retrieveLocal) {
545
        m_fetcher->setFile((*items.begin())->file());
546
        m_fetcher->searchCover();
547
        return;
548 549
    }

550 551 552 553 554 555
    QUrl file = QFileDialog::getOpenFileUrl(
        this, i18n("Select Cover Image File"),
        QUrl::fromLocalFile(QDir::home().path()),
        i18n("Images (*.png *.jpg)"), nullptr,
        0, QStringList() << QStringLiteral("file")
        );
556 557 558
    if(file.isEmpty())
        return;

559 560 561
    QString artist = items.front()->file().tag()->artist();
    QString album = items.front()->file().tag()->album();

562 563 564 565
    coverKey newId = CoverManager::addCover(file, artist, album);

    if(newId != CoverManager::NoMatch)
        refreshAlbums(items, newId);
566 567
}

568 569 570
// Called when image fetcher has added a new cover.
void Playlist::slotCoverChanged(int coverId)
{
Michael Pyne's avatar
Michael Pyne committed
571
    qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n";
572 573 574
    refreshAlbums(selectedItems(), coverId);
}

575
void Playlist::slotGuessTagInfo(TagGuesser::Type type)
576
{
577
    QApplication::setOverrideCursor(Qt::WaitCursor);
578
    const PlaylistItemList items = selectedItems();
579
    setDynamicListsFrozen(true);
580 581 582

    m_blockDataChanged = true;

583 584
    foreach(PlaylistItem *item, items) {
        item->guessTagInfo(type);
585
        processEvents();
586
    }
587 588 589 590 591

    // MusicBrainz queries automatically commit at this point.  What would
    // be nice is having a signal emitted when the last query is completed.

    if(type == TagGuesser::FileName)
592
        TagTransactionManager::instance()->commit();
593

594 595
    m_blockDataChanged = false;

596
    playlistItemsChanged();
597
    setDynamicListsFrozen(false);
598
    QApplication::restoreOverrideCursor();
599 600
}

601 602 603 604
void Playlist::slotReload()
{
    QFileInfo fileInfo(m_fileName);
    if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable())
605
        return;
606

607
    clearItems(items());
608 609 610
    loadFile(m_fileName, fileInfo);
}

611
void Playlist::slotWeightDirty(int column)
612 613
{
    if(column < 0) {
614
        m_weightDirty.clear();
615
        for(int i = 0; i < columnCount(); i++) {
Kacper Kasper's avatar
Kacper Kasper committed
616
            if(!isColumnHidden(i))
617 618 619
                m_weightDirty.append(i);
        }
        return;
620 621
    }

622
    if(!m_weightDirty.contains(column))
623
        m_weightDirty.append(column);
624 625
}

626 627
void Playlist::slotShowPlaying()
{
628
    if(!playingItem())
629
        return;
630

631
    Playlist *l = playingItem()->playlist();
632 633

    l->clearSelection();
634 635 636 637 638 639 640 641

    // Raise the playlist before selecting the items otherwise the tag editor
    // will not update when it gets the selectionChanged() notification
    // because it will think the user is choosing a different playlist but not
    // selecting a different item.

    m_collection->raise(l);

642
    l->setCurrentItem(playingItem());
Kacper Kasper's avatar
Kacper Kasper committed
643
    l->scrollToItem(playingItem());
644 645
}

646 647
void Playlist::slotColumnResizeModeChanged()
{
648 649 650 651 652 653 654
    if(manualResize()) {
        header()->setSectionResizeMode(QHeaderView::Interactive);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    } else {
        header()->setSectionResizeMode(QHeaderView::Fixed);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    }
655

656
    if(!manualResize())
657
        slotUpdateColumnWidths();
658 659 660 661

    SharedSettings::instance()->sync();
}

662
void Playlist::playlistItemsChanged()
663 664
{
    if(m_blockDataChanged)
665
        return;
666
    PlaylistInterface::playlistItemsChanged();
667 668
}

669
////////////////////////////////////////////////////////////////////////////////
670
// protected members
671 672
////////////////////////////////////////////////////////////////////////////////

673
void Playlist::removeFromDisk(const PlaylistItemList &items)
674 675 676
{
    if(isVisible() && !items.isEmpty()) {

677
        QStringList files;
678 679
        foreach(const PlaylistItem *item, items)
            files.append(item->file().absFilePath());
680

681
        DeleteDialog dialog(this);
682

683
        m_blockDataChanged = true;
684

685 686 687
        if(dialog.confirmDeleteList(files)) {
            bool shouldDelete = dialog.shouldDelete();
            QStringList errorFiles;
688

689 690
            foreach(PlaylistItem *item, items) {
                if(playingItem() == item)
David Faure's avatar
David Faure committed
691
                    action("forward")->trigger();
692

693
                QString removePath = item->file().absFilePath();
Michael Pyne's avatar
Michael Pyne committed
694
                QUrl removeUrl = QUrl::fromLocalFile(removePath);
Michael Pyne's avatar
Michael Pyne committed
695
                if((!shouldDelete && KIO::trash(removeUrl)->exec()) ||
696 697
                   (shouldDelete && QFile::remove(removePath)))
                {
698
                    delete item->collectionItem();
699 700
                }
                else
701
                    errorFiles.append(item->file().absFilePath());
702
            }
703

704 705 706 707 708 709 710
            if(!errorFiles.isEmpty()) {
                QString errorMsg = shouldDelete ?
                        i18n("Could not delete these files") :
                        i18n("Could not move these files to the Trash");
                KMessageBox::errorList(this, errorMsg, errorFiles);
            }
        }
711

712
        m_blockDataChanged = false;
713

714
        playlistItemsChanged();
715 716 717
    }
}

718 719
void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList)
{
720
    // direct call to ::items to avoid infinite loop, bug 402355
721
    clearItems(Playlist::items());
722
    createItems(itemList);
723 724
}

725
void Playlist::dragEnterEvent(QDragEnterEvent *e)
726
{
Michael Pyne's avatar
Michael Pyne committed
727
    if(CoverDrag::isCover(e->mimeData())) {
728
        setDropIndicatorShown(false);
729 730
        e->accept();
        return;
Michael Pyne's avatar
Michael Pyne committed
731 732
    }

733 734
    if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) {
        setDropIndicatorShown(true);
735
        e->acceptProposedAction();
736
    }
737 738
    else
        e->ignore();
739
}
Michael Pyne's avatar
Michael Pyne committed
740

741
void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after)
Michael Pyne's avatar
Michael Pyne committed
742
{
743 744 745
    if(!urls->hasUrls()) {
        return;
    }
Michael Pyne's avatar
Michael Pyne committed
746

747
    addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after);
748 749
}

750
bool Playlist::eventFilter(QObject *watched, QEvent *e)
751
{
752
    if(watched == header()) {
753 754 755
        switch(e->type()) {
        case QEvent::MouseMove:
        {
756
            if((static_cast<QMouseEvent *>(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton &&
757 758 759 760 761 762 763 764 765 766 767 768
                !action<KToggleAction>("resizeColumnsManually")->isChecked())
            {
                m_columnWidthModeChanged = true;

                action<KToggleAction>("resizeColumnsManually")->setChecked(true);
                slotColumnResizeModeChanged();
            }

            break;
        }
        case QEvent::MouseButtonPress:
        {
769
            if(static_cast<QMouseEvent *>(e)->button() == Qt::RightButton)
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787
                m_headerMenu->popup(QCursor::pos());

            break;
        }
        case QEvent::MouseButtonRelease:
        {
            if(m_columnWidthModeChanged) {
                m_columnWidthModeChanged = false;
                notifyUserColumnWidthModeChanged();
            }

            if(!manualResize() && m_widthsDirty)
                QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths()));
            break;
        }
        default:
            break;
        }
788 789
    }

790
    return QTreeWidget::eventFilter(watched, e);
791 792
}

793 794
void Playlist::keyPressEvent(QKeyEvent *event)
{
795
    if(event->key() == Qt::Key_Up) {
796 797
        const auto topItem = topLevelItem(0);
        if(topItem && topItem == currentItem()) {
798
            QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden);
799 800 801 802
            if(topItem == *visible) {
                emit signalMoveFocusAway();
                event->accept();
            }
803
        }
804
    }
805

806
    QTreeWidget::keyPressEvent(event);
807 808
}

809
QStringList Playlist::mimeTypes() const
810
{
811 812 813 814 815
    return QStringList("text/uri-list");
}

QMimeData* Playlist::mimeData(const QList<QTreeWidgetItem *> items) const
{
816
    QList<QUrl> urls;
817
    foreach(QTreeWidgetItem *item, items) {
818
        urls << QUrl::fromLocalFile(static_cast<PlaylistItem*>(item)->file().absFilePath());
819 820 821 822 823 824 825 826 827 828
    }

    QMimeData *urlDrag = new QMimeData();
    urlDrag->setUrls(urls);

    return urlDrag;
}

bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action)
{
829 830 831 832 833 834 835
    // TODO: Re-add DND
    Q_UNUSED(parent);
    Q_UNUSED(index);
    Q_UNUSED(data);
    Q_UNUSED(action);

    return false;
836 837 838 839 840
}

void Playlist::dropEvent(QDropEvent *e)
{
    QPoint vp = e->pos();
841
    PlaylistItem *item = static_cast<PlaylistItem *>(itemAt(vp));
842

843 844
    // First see if we're dropping a cover, if so we can get it out of the
    // way early.
Michael Pyne's avatar
Michael Pyne committed
845 846
    if(item && CoverDrag::isCover(e->mimeData())) {
        coverKey id = CoverDrag::idFromData(e->mimeData());
847 848 849 850 851

        // If the item we dropped on is selected, apply cover to all selected
        // items, otherwise just apply to the dropped item.

        if(item->isSelected()) {
852
            const PlaylistItemList selItems = selectedItems();
853 854 855
            foreach(PlaylistItem *playlistItem, selItems) {
                playlistItem->file().coverInfo()->setCoverId(id);
                playlistItem->refresh();
856 857 858 859 860 861 862 863
            }
        }
        else {
            item->file().coverInfo()->setCoverId(id);
            item->refresh();
        }

        return;
864 865
    }

Dirk Mueller's avatar
Dirk Mueller committed
866
    // When dropping on the toUpper half of an item, insert before this item.
867 868
    // This is what the user expects, and also allows the insertion at
    // top of the list
869

870
    QRect rect = visualItemRect(item);
871
    if(!item)
872 873
        item = static_cast<PlaylistItem *>(topLevelItem(topLevelItemCount() - 1));
    else if(vp.y() < rect.y() + rect.height() / 2)
874
        item = static_cast<PlaylistItem *>(item->itemAbove());
875

876 877
    m_blockDataChanged = true;

878
    if(e->source() == this) {
879

880
        // Since we're trying to arrange things manually, turn off sorting.
881

882
        sortItems(columnCount() + 1, Qt::AscendingOrder);
883

884
        const QList<QTreeWidgetItem *> items = QTreeWidget::selectedItems();
885

886
        foreach(QTreeWidgetItem *listViewItem, items) {
887
            if(!item) {
888

889 890
                // Insert the item at the top of the list.  This is a bit ugly,
                // but I don't see another way.
891

892 893
                takeItem(listViewItem);
                insertItem(listViewItem);
894
            }
895 896
            //else
            //    listViewItem->moveItem(item);
897

898
            item = static_cast<PlaylistItem *>(listViewItem);
899
        }
900
    }
901
    else
902
        addFilesFromMimeData(e->mimeData(), item);
903

904 905
    m_blockDataChanged = false;

906
    playlistItemsChanged();
907
    emit signalPlaylistItemsDropped(this);
908
    QTreeWidget::dropEvent(e);
909 910
}

911 912
void Playlist::showEvent(QShowEvent *e)
{
913
    if(m_applySharedSettings) {
914 915
        SharedSettings::instance()->apply(this);
        m_applySharedSettings = false;
916
    }
917

918
    QTreeWidget::showEvent(e);
919 920
}

921 922 923 924 925
void Playlist::applySharedSettings()
{
    m_applySharedSettings = true;
}

926 927 928 929 930
void Playlist::read(QDataStream &s)
{
    s >> m_playlistName
      >> m_fileName;

931 932 933 934
    // m_fileName is probably empty.
    if(m_playlistName.isEmpty())
        throw BICStreamException();

935
    // Do not sort. Add the files in the order they were saved.
Kacper Kasper's avatar
Kacper Kasper committed
936
    setSortingEnabled(false);
937

938 939 940
    QStringList files;
    s >> files;

941
    QTreeWidgetItem *after = 0;
942 943 944

    m_blockDataChanged = true;

Albert Astals Cid's avatar
Albert Astals Cid committed
945
    foreach(const QString &file, files) {
946 947 948
        if(file.isEmpty())
            throw BICStreamException();

949
        after = createItem(FileHandle(file), after);
950
    }
951

952 953
    m_blockDataChanged = false;

954
    playlistItemsChanged();
Michael Pyne's avatar
Michael Pyne committed
955
    m_collection->setupPlaylist(this, "audio-midi");
956 957
}

958
void Playlist::paintEvent(QPaintEvent *pe)
959 960 961
{
    // If there are columns that need to be updated, well, update them.

962 963
    if(!m_weightDirty.isEmpty() && !manualResize())
    {
964 965
        calculateColumnWeights();
        slotUpdateColumnWidths();
966 967
    }

968
    QTreeWidget::paintEvent(pe);
969 970
}

971
void Playlist::resizeEvent(QResizeEvent *re)
972 973 974 975
{
    // If the width of the view has changed, manually update the column
    // widths.

976
    if(re->size().width() != re->oldSize().width() && !manualResize())
977
        slotUpdateColumnWidths();
978

979
    QTreeWidget::resizeEvent(re);
980 981
}

982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000
// Reimplemented to show a visual indication of which of the view's playlist
// items is actually playing.
void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    PlaylistItem *item = static_cast<PlaylistItem *>(itemFromIndex(index));
    if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) {
        return QTreeWidget::drawRow(p, option, index);
    }

    // Seems that the view draws the background now so we have to do this
    // manually
    p->fillRect(option.rect, QPalette{}.midlight());

    QStyleOptionViewItem newOption {option};
    newOption.font.setBold(true);

    QTreeWidget::drawRow(p, newOption, index);
}

1001
void Playlist::insertItem(QTreeWidgetItem *item)
1002
{
1003
    QTreeWidget::insertTopLevelItem(0, item);
1004 1005
}

1006
void Playlist::takeItem(QTreeWidgetItem *item)
1007
{
Kacper Kasper's avatar
Kacper Kasper committed
1008
    int index = indexOfTopLevelItem(item);
1009
    QTreeWidget::takeTopLevelItem(index);
1010 1011
}

1012
void Playlist::addColumn(const QString &label, int)
1013
{
1014 1015
    m_columns.append(label);
    setHeaderLabels(m_columns);
1016 1017
}

1018
PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after)
1019
{
1020
    return createItem<PlaylistItem>(file, after);
1021 1022
}

1023
void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after)
1024
{
1025
    createItems<QVector, PlaylistItem, PlaylistItem>(siblings, after);
1026 1027
}

1028
void Playlist::addFiles(const QStringList &files, PlaylistItem *after)
1029
{
1030 1031 1032
    if(Q_UNLIKELY(files.isEmpty())) {
        return;
    }
1033

1034
    m_blockDataChanged = true;
1035
    setEnabled(false);
1036

1037
    QVector<QFuture<void>> pendingFutures;
1038
    for(const auto &file : files) {
1039 1040 1041 1042 1043 1044 1045
        // some files added here will launch threads that we must wait until
        // they're done to cleanup
        auto pendingResult = addUntypedFile(file, after);
        if(!pendingResult.isFinished()) {
            pendingFutures.push_back(pendingResult);
            ++m_itemsLoading;
        }
1046
    }
1047

1048 1049 1050
    // It's possible for no async threads to be launched, and also possible
    // for this function to be called while there were other threads in flight
    if(pendingFutures.isEmpty() && m_itemsLoading == 0) {
1051
        cleanupAfterAllFileLoadsCompleted();
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
        return;
    }

    // Build handlers for all the still-active loaders on the heap and then
    // return to the event loop.
    for(const auto &future : qAsConst(pendingFutures)) {
        auto loadWatcher = new QFutureWatcher<void>(this);
        loadWatcher->setFuture(future);

        connect(loadWatcher, &QFutureWatcher<void>::finished, this, [=]() {
                if(--m_itemsLoading == 0) {
                    cleanupAfterAllFileLoadsCompleted();
                }

                loadWatcher->deleteLater();
            });
1068
    }
1069 1070
}

1071
void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id)
1072
{
1073
    QList< QPair<QString, QString> > albums;
1074
    bool setAlbumCovers = items.count() == 1;
1075

1076 1077 1078
    foreach(const PlaylistItem *item, items) {
        QString artist = item->file().tag()->artist();
        QString album = item->file().tag()->album();
1079

1080
        if(!albums.contains(qMakePair(artist, album)))
1081
            albums.append(qMakePair(artist, album));
1082

1083
        item->file().coverInfo()->setCoverId(id);
1084
        if(setAlbumCovers)
1085
            item->file().coverInfo()->applyCoverToWholeAlbum(true);
1086 1087
    }

Laurent Montel's avatar
Laurent Montel committed
1088 1089
    for(QList< QPair<QString, QString> >::ConstIterator it = albums.constBegin();
        it != albums.constEnd(); ++it)
1090
    {
1091
        refreshAlbum((*it).first, (*it).second);
1092 1093 1094
    }
}