playlist.cpp 64.8 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
Poirt    
Laurent Montel committed
31
32
#include <kactionmenu.h>
#include <ktoggleaction.h>
33
34

#include <QCursor>
35
#include <QDesktopServices>
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>
43
#include <QList>
Laurent Montel's avatar
Laurent Montel committed
44
45
46
#include <QResizeEvent>
#include <QMouseEvent>
#include <QKeyEvent>
47
#include <QMimeData>
Michael Pyne's avatar
Michael Pyne committed
48
#include <QMenu>
49
50
#include <QTimer>
#include <QClipboard>
Laurent Montel's avatar
Laurent Montel committed
51
52
53
#include <QTextStream>
#include <QDropEvent>
#include <QDragEnterEvent>
54
#include <QPixmap>
55
#include <QSet>
56
#include <QStackedWidget>
57
#include <QScrollBar>
58
#include <QPainter>
59
60

#include <QtConcurrent>
61
#include <QFutureWatcher>
62

63
64
#include <id3v1genres.h>

65
#include <time.h>
66
#include <cmath>
Michael Pyne's avatar
Michael Pyne committed
67
#include <algorithm>
68
#include <random>
69
#include <utility>
70

71
72
73
74
75
76
#include "actioncollection.h"
#include "cache.h"
#include "collectionlist.h"
#include "coverdialog.h"
#include "coverinfo.h"
#include "deletedialog.h"
77
#include "directoryloader.h"
78
79
80
81
82
#include "filerenamer.h"
#include "iconsupport.h"
#include "juk_debug.h"
#include "juktag.h"
#include "mediafiles.h"
83
#include "playlistcollection.h"
84
#include "playlistitem.h"
Scott Wheeler's avatar
Scott Wheeler committed
85
#include "playlistsearch.h"
86
#include "playlistsharedsettings.h"
87
#include "tagtransactionmanager.h"
88
#include "upcomingplaylist.h"
89
#include "webimagefetcher.h"
90

91
using namespace ActionCollection; // ""_act and others
92

93
94
95
96
97
98
/**
 * Used to give every track added in the program a unique identifier. See
 * PlaylistItem
 */
quint32 g_trackID = 0;

99
100
101
102
103
104
/**
 * Just a shortcut of sorts.
 */

static bool manualResize()
{
105
    return "resizeColumnsManually"_act->isChecked();
106
107
}

108
////////////////////////////////////////////////////////////////////////////////
109
// static members
110
111
////////////////////////////////////////////////////////////////////////////////

112
113
114
115
116
bool                    Playlist::m_visibleChanged = false;
bool                    Playlist::m_shuttingDown   = false;
PlaylistItemList        Playlist::m_history;
QVector<PlaylistItem *> Playlist::m_backMenuItems;
int                     Playlist::m_leftColumn     = 0;
117

118
////////////////////////////////////////////////////////////////////////////////
119
// public members
120
121
////////////////////////////////////////////////////////////////////////////////

122
123
124
125
Playlist::Playlist(
        bool delaySetup, const QString &name,
        PlaylistCollection *collection, const QString &iconName,
        int extraCols)
126
127
128
129
130
  : QTreeWidget(collection->playlistStack())
  , m_collection(collection)
  , m_playlistName(name)
  , m_refillDebounce(new QTimer(this))
  , m_fetcher(new WebImageFetcher(this))
131
{
132
    setup(extraCols);
133

134
135
136
137
138
139
140
141
142
143
144
145
146
    // The timer soaks up repeated events that may cause the random play list
    // to be regenerated repeatedly.
    m_refillDebounce->setInterval(100);
    m_refillDebounce->setSingleShot(true);
    connect(m_refillDebounce, &QTimer::timeout,
            this,             &Playlist::refillRandomList);

    // Any of the random-related actions being triggered will cause the parent
    // group to emit the triggered signal.
    QActionGroup *randomGroup = action("disableRandomPlay")->actionGroup();
    connect(randomGroup,      &QActionGroup::triggered,
            m_refillDebounce, qOverload<>(&QTimer::start));

147
148
149
150
151
152
153
154
155
156
157
    // 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)
{
158
159
}

160
Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items,
161
162
                   const QString &name, const QString &iconName)
    : Playlist(false, name, collection, iconName, 0)
163
164
165
166
167
{
    createItems(items);
}

Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile,
168
169
                   const QString &iconName)
    : Playlist(true, QString(), collection, iconName, 0)
170
{
171
    m_fileName = playlistFile.canonicalFilePath();
172
173
174

    // Load the file after construction completes so that virtual methods in
    // subclasses can take effect.
175
    QTimer::singleShot(0, this, [=]() {
176
177
178
        loadFile(m_fileName, playlistFile);
        collection->setupPlaylist(this, iconName);
    });
179
180
}

181
182
Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns)
    : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns)
183
{
184
185
186
187
}

Playlist::~Playlist()
{
188
189
    // clearItem() will take care of removing the items from the history,
    // so call clearItems() to make sure it happens.
190
191
192
193
    //
    // Some subclasses override clearItems and items so we manually dispatch to
    // make clear that it's intentional that those subclassed versions don't
    // get called (because we can't call them)
194

195
    m_randomSequence.clear();
196
    Playlist::clearItems(Playlist::items());
197

198
    if(!m_shuttingDown)
199
        m_collection->removePlaylist(this);
200
201
}

202
203
QString Playlist::name() const
{
204
    if(m_playlistName.isEmpty())
205
        return m_fileName.section(QDir::separator(), -1).section('.', 0, -2);
206
    else
207
        return m_playlistName;
208
209
210
211
}

FileHandle Playlist::currentFile() const
{
212
    return playingItem() ? playingItem()->file() : FileHandle();
213
214
}

215
216
void Playlist::playFirst()
{
217
218
    QTreeWidgetItemIterator listIt(const_cast<Playlist *>(this), QTreeWidgetItemIterator::NotHidden);
    beginPlayingItem(static_cast<PlaylistItem *>(*listIt));
219
    refillRandomList();
220
221
}

222
223
void Playlist::playNextAlbum()
{
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
    const auto &item = playingItem();
    if(!item || !action("albumRandomPlay")->isChecked()) {
        playNext();
        return;
    }

    const auto currentAlbum = item->file().tag()->album();
    const auto nextAlbumTrack = std::find_if(m_randomSequence.begin(), m_randomSequence.end(),
            [currentAlbum](const PlaylistItem *item) {
                return item->file().tag()->album() != currentAlbum;
            });

    if(nextAlbumTrack == m_randomSequence.end()) {
        // We were on the last album, playNext will handle looping if we should loop
        m_randomSequence.clear();
        playNext();
    }
    else {
        m_randomSequence.erase(m_randomSequence.begin(), nextAlbumTrack);
        beginPlayingItem(*nextAlbumTrack);
    }
245
246
}

247
248
void Playlist::playNext()
{
249
    PlaylistItem *next = nullptr;
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
    auto nowPlaying = playingItem();
    bool doLoop = action("loopPlaylist")->isChecked();

    // Treat an item from a different playlist as if we were being asked to
    // play from a stop
    if(nowPlaying && nowPlaying->playlist() != this) {
        nowPlaying = nullptr;
    }

    if(action("disableRandomPlay")->isChecked()) {
        QTreeWidgetItemIterator listIt = nowPlaying
            ? QTreeWidgetItemIterator(nowPlaying, QTreeWidgetItemIterator::NotHidden)
            : QTreeWidgetItemIterator(this,       QTreeWidgetItemIterator::NotHidden);

        if(*listIt && nowPlaying) {
            ++listIt;
        }

268
        next = static_cast<PlaylistItem *>(*listIt);
269
270
271
272
273

        if(!next && doLoop) {
            playFirst();
            return;
        }
274
    }
275
276
277
278
279
280
281
282
283
284
285
286
287
    else {
        // The two random play modes are identical here, the difference is in how the
        // randomized sequence is generated by refillRandomList

        if(m_randomSequence.isEmpty() && (doLoop || !nowPlaying)) {
            refillRandomList();

            // Don't play the same track twice in a row even if it can
            // "randomly" happen
            if(m_randomSequence.front() == nowPlaying) {
                std::swap(m_randomSequence.front(), m_randomSequence.back());
            }
        }
288

289
290
291
292
293
294
        if(!m_randomSequence.isEmpty()) {
            next = m_randomSequence.takeFirst();
        }
    }

    // Will stop playback if next is still null
295
    beginPlayingItem(next);
296
297
298
299
}

void Playlist::stop()
{
300
    m_history.clear();
301
    setPlaying(nullptr);
302
303
304
305
}

void Playlist::playPrevious()
{
306
    if(!playingItem())
307
        return;
308
309
310

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

311
    PlaylistItem *previous = nullptr;
312
313

    if(random && !m_history.isEmpty()) {
314
        PlaylistItemList::Iterator last = m_history.end() - 1;
315
        previous = *last;
316
        m_history.erase(last);
317
318
    }
    else {
319
        m_history.clear();
320
321
        QTreeWidgetItemIterator listIt(playingItem(), QTreeWidgetItemIterator::NotHidden);
        previous = static_cast<PlaylistItem *>(*--listIt);
322
323
    }

324
    beginPlayingItem(previous);
325
326
327
328
}

void Playlist::setName(const QString &n)
{
329
330
    m_collection->addNameToDict(n);
    m_collection->removeNameFromDict(m_playlistName);
331

332
333
334
335
    m_playlistName = n;
    emit signalNameChanged(m_playlistName);
}

336
void Playlist::save()
337
{
338
    if(m_fileName.isEmpty())
339
        return saveAs();
340

341
    QFile file(m_fileName);
342

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

346
347
    QTextStream stream(&file);

348
    const QStringList fileList = files();
349

350
    for(const auto &file : fileList) {
351
        stream << file << '\n';
352
    }
353

354
    file.close();
355
356
357
358
}

void Playlist::saveAs()
{
359
360
    m_collection->removeFileFromDict(m_fileName);

361
    m_fileName = MediaFiles::savePlaylistDialog(name(), this);
362

363
    if(!m_fileName.isEmpty()) {
364
        m_collection->addFileToDict(m_fileName);
365

366
367
368
369
        // If there's no playlist name set, use the file name.
        if(m_playlistName.isEmpty())
            emit signalNameChanged(name());
        save();
370
    }
371
372
}

373
void Playlist::updateDeletedItem(PlaylistItem *item)
374
{
375
    m_members.remove(item->file().absFilePath());
376
    m_randomSequence.removeAll(item);
377
    m_history.removeAll(item);
378
}
379

380
void Playlist::clearItem(PlaylistItem *item)
381
382
{
    // Automatically updates internal structs via updateDeletedItem
383
    delete item;
384

385
    playlistItemsChanged();
386
387
}

388
void Playlist::clearItems(const PlaylistItemList &items)
389
{
390
    for(auto &item : items) {
391
        delete item;
392
    }
393
    playlistItemsChanged();
394
395
}

396
397
PlaylistItem *Playlist::playingItem() // static
{
398
399
400
    return PlaylistItem::playingItems().isEmpty()
        ? nullptr
        : PlaylistItem::playingItems().front();
401
402
}

403
QStringList Playlist::files() const
404
{
405
    QStringList list;
406

407
    for(QTreeWidgetItemIterator it(const_cast<Playlist *>(this)); *it; ++it)
408
        list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath());
409

410
    return list;
411
412
}

413
414
PlaylistItemList Playlist::items()
{
415
    return items(QTreeWidgetItemIterator::IteratorFlag(0));
416
417
}

418
PlaylistItemList Playlist::visibleItems()
419
{
420
    return items(QTreeWidgetItemIterator::NotHidden);
421
422
}

423
PlaylistItemList Playlist::selectedItems()
424
{
Kacper Kasper's avatar
Kacper Kasper committed
425
    return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden);
426
427
}

428
429
PlaylistItem *Playlist::firstChild() const
{
430
    return static_cast<PlaylistItem *>(topLevelItem(0));
431
432
}

Scott Wheeler's avatar
Scott Wheeler committed
433
434
435
436
437
void Playlist::updateLeftColumn()
{
    int newLeftColumn = leftMostVisibleColumn();

    if(m_leftColumn != newLeftColumn) {
438
439
        updatePlaying();
        m_leftColumn = newLeftColumn;
Scott Wheeler's avatar
Scott Wheeler committed
440
441
442
    }
}

443
void Playlist::setItemsVisible(const QModelIndexList &indexes, bool visible) // static
444
{
445
    m_visibleChanged = true;
446

447
448
    for(QModelIndex index : indexes)
        itemFromIndex(index)->setHidden(!visible);
449
450
}

451
void Playlist::setSearch(PlaylistSearch* s)
452
453
454
455
{
    m_search = s;

    if(!m_searchEnabled)
456
        return;
457

458
459
460
    for(int row = 0; row < topLevelItemCount(); ++row)
        topLevelItem(row)->setHidden(true);
    setItemsVisible(s->matchedItems(), true);
461
462
463
464
465
}

void Playlist::setSearchEnabled(bool enabled)
{
    if(m_searchEnabled == enabled)
466
        return;
467
468
469
470

    m_searchEnabled = enabled;

    if(enabled) {
471
472
473
        for(int row = 0; row < topLevelItemCount(); ++row)
            topLevelItem(row)->setHidden(true);
        setItemsVisible(m_search->matchedItems(), true);
474
    }
475
476
477
    else {
        const auto &playlistItems = items();
        for(PlaylistItem* item : playlistItems)
478
            item->setHidden(false);
479
    }
480
481
}

482
483
484
// 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
485
void Playlist::synchronizePlayingItems(Playlist *playlist, bool setMaster)
486
{
487
488
489
490
491
492
493
494
    if(!playlist || !playlist->playing())
        return;

    CollectionListItem *base = playingItem()->collectionItem();
    for(QTreeWidgetItemIterator itemIt(playlist); *itemIt; ++itemIt) {
        PlaylistItem *item = static_cast<PlaylistItem *>(*itemIt);
        if(base == item->collectionItem()) {
            item->setPlaying(true, setMaster);
495
496
497
498
499
            return;
        }
    }
}

500
501
502
503
504
505
506
void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster)
{
    for(auto p : sources) {
        synchronizePlayingItems(p, setMaster);
    }
}

507
508
509
510
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

511
512
void Playlist::copy()
{
513
    const PlaylistItemList items = selectedItems();
514
    QList<QUrl> urls;
515

516
    for(const auto &item : items) {
517
        urls << QUrl::fromLocalFile(item->file().absFilePath());
518
519
520
    }

    QMimeData *mimeData = new QMimeData;
521
    mimeData->setUrls(urls);
522
523

    QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
524
525
526
527
}

void Playlist::paste()
{
528
529
530
    addFilesFromMimeData(
        QApplication::clipboard()->mimeData(),
        static_cast<PlaylistItem *>(currentItem()));
531
532
533
534
}

void Playlist::clear()
{
535
536
    PlaylistItemList l = selectedItems();
    if(l.isEmpty())
537
        l = items();
538
539

    clearItems(l);
540
541
}

542
543
void Playlist::slotRefresh()
{
544
545
546
    PlaylistItemList itemList = selectedItems();
    if(itemList.isEmpty())
        itemList = visibleItems();
547

Luigi Toscano's avatar
Luigi Toscano committed
548
    QApplication::setOverrideCursor(Qt::WaitCursor);
549
    for(auto &item : itemList) {
550
        item->refreshFromDisk();
551

552
        if(!item->file().tag() || !item->file().fileInfo().exists()) {
Michael Pyne's avatar
Michael Pyne committed
553
            qCDebug(JUK_LOG) << "Error while trying to refresh the tag.  "
554
                           << "This file has probably been removed.";
555
            delete item->collectionItem();
556
        }
557

558
        processEvents();
559
    }
Luigi Toscano's avatar
Luigi Toscano committed
560
    QApplication::restoreOverrideCursor();
561
562
}

563
564
565
566
567
568
569
570
571
572
573
574
575
576
void Playlist::slotOpenItemDir()
{
    PlaylistItemList itemList = selectedItems();
    QList<QUrl> pathList;

    for(auto &item : itemList) {
        QUrl path = QUrl::fromLocalFile(item->file().fileInfo().absoluteDir().absolutePath());
        if(!pathList.contains(path))
            pathList.append(path);
    }

    if (pathList.length() > 4) {
        if(KMessageBox::warningContinueCancel(
            this,
577
578
579
            i18np("You are about to open directory. Are you sure you want to continue?",
                  "You are about to open %1 directories. Are you sure you want to continue?",
                  pathList.length()),
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
            i18n("Open Containing Folder")
        ) == KMessageBox::Cancel)
        {
            return;
        }
    }

    QApplication::setOverrideCursor(Qt::WaitCursor);
    for(auto &path : pathList) {
        QDesktopServices::openUrl(path);

        processEvents();
    }
    QApplication::restoreOverrideCursor();
}

596
597
void Playlist::slotRenameFile()
{
598
    FileRenamer renamer;
599
    PlaylistItemList items = selectedItems();
600
601

    if(items.isEmpty())
602
603
        return;

604
605
    emit signalEnableDirWatch(false);

606
    m_blockDataChanged = true;
607
    renamer.rename(items);
608
    m_blockDataChanged = false;
609
    playlistItemsChanged();
610

611
    emit signalEnableDirWatch(true);
612
613
}

614
615
616
617
618
619
void Playlist::slotBeginPlayback()
{
    QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden);
    PlaylistItem *item = static_cast<PlaylistItem *>(*visible);

    if(item) {
620
621
        refillRandomList();
        playNext();
622
623
624
625
626
627
    }
    else {
        action("stop")->trigger();
    }
}

628
629
void Playlist::slotViewCover()
{
630
    const PlaylistItemList items = selectedItems();
631
632
633
634
635
636
637
638
    for(const auto &item : items) {
        const auto cover = item->file().coverInfo();

        if(cover->hasCover()) {
            cover->popup();
            return; // If we select multiple items, only show one
        }
    }
639
640
641
642
643
}

void Playlist::slotRemoveCover()
{
    PlaylistItemList items = selectedItems();
644
    if(items.isEmpty())
645
        return;
646
    int button = KMessageBox::warningContinueCancel(this,
647
                                                    i18n("Are you sure you want to delete these covers?"),
648
                                                    QString(),
Laurent Montel's avatar
Laurent Montel committed
649
                                                    KGuiItem(i18n("&Delete Covers")));
650
    if(button == KMessageBox::Continue)
651
        refreshAlbums(items);
652
653
}

654
655
656
657
658
void Playlist::slotShowCoverManager()
{
    static CoverDialog *managerDialog = 0;

    if(!managerDialog)
659
        managerDialog = new CoverDialog(this);
660
661
662
663

    managerDialog->show();
}

664
665
666
667
668
669
670
void Playlist::slotAddCover(bool retrieveLocal)
{
    PlaylistItemList items = selectedItems();

    if(items.isEmpty())
        return;

671
    if(!retrieveLocal) {
672
        m_fetcher->setFile((*items.begin())->file());
673
        m_fetcher->searchCover();
674
        return;
675
676
    }

677
678
679
680
    QUrl file = QFileDialog::getOpenFileUrl(
        this, i18n("Select Cover Image File"),
        QUrl::fromLocalFile(QDir::home().path()),
        i18n("Images (*.png *.jpg)"), nullptr,
681
        {}, QStringList() << QStringLiteral("file")
682
        );
683
684
685
    if(file.isEmpty())
        return;

686
687
688
    QString artist = items.front()->file().tag()->artist();
    QString album = items.front()->file().tag()->album();

689
690
691
692
    coverKey newId = CoverManager::addCover(file, artist, album);

    if(newId != CoverManager::NoMatch)
        refreshAlbums(items, newId);
693
694
}

695
696
697
// Called when image fetcher has added a new cover.
void Playlist::slotCoverChanged(int coverId)
{
698
    qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.";
699
700
701
    refreshAlbums(selectedItems(), coverId);
}

702
void Playlist::slotGuessTagInfo(TagGuesser::Type type)
703
{
Luigi Toscano's avatar
Luigi Toscano committed
704
    QApplication::setOverrideCursor(Qt::WaitCursor);
705
    const PlaylistItemList items = selectedItems();
706
    setDynamicListsFrozen(true);
707
708
709

    m_blockDataChanged = true;

710
    for(auto &item : items) {
711
        item->guessTagInfo(type);
712
        processEvents();
713
    }
714
715
716
717
718

    // 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)
719
        TagTransactionManager::instance()->commit();
720

721
722
    m_blockDataChanged = false;

723
    playlistItemsChanged();
724
    setDynamicListsFrozen(false);
Luigi Toscano's avatar
Luigi Toscano committed
725
    QApplication::restoreOverrideCursor();
726
727
}

728
729
730
731
void Playlist::slotReload()
{
    QFileInfo fileInfo(m_fileName);
    if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable())
732
        return;
733

734
    clearItems(items());
735
736
737
    loadFile(m_fileName, fileInfo);
}

738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
void Playlist::refillRandomList()
{
    qCDebug(JUK_LOG) << "Refilling random items.";

    if(action("disableRandomPlay")->isChecked()) {
        m_randomSequence.clear();
        return;
    }

    PlaylistItemList randomItems = visibleItems();

    // See https://www.pcg-random.org/posts/cpp-seeding-surprises.html
    std::random_device rdev;
    uint64_t rseed = (uint64_t(rdev()) << 32) | rdev();
    std::linear_congruential_engine<
        uint64_t, 6364136223846793005U, 1442695040888963407U, 0U
        > knuth_lcg(rseed);

    std::shuffle(randomItems.begin(), randomItems.end(), knuth_lcg);

    if(action("albumRandomPlay")->isChecked()) {
        std::sort(randomItems.begin(), randomItems.end(),
            [](PlaylistItem *a, PlaylistItem *b) {
                return a->file().tag()->album() < b->file().tag()->album();
            });
763
764
765
766
767
768
769
770
771
772
773
774

        // If there is an item playing from our playlist already, move its
        // album to the front

        const auto wasPlaying = playingItem();
        if(wasPlaying && wasPlaying->playlist() == this) {
            const auto playingAlbum = wasPlaying->file().tag()->album();
            std::stable_partition(randomItems.begin(), randomItems.end(),
                [playingAlbum](const PlaylistItem *item) {
                    return item->file().tag()->album() == playingAlbum;
                });
        }
775
776
777
778
779
    }

    std::swap(m_randomSequence, randomItems);
}

780
void Playlist::slotWeightDirty(int column)
781
782
{
    if(column < 0) {
783
        m_weightDirty.clear();
784
        for(int i = 0; i < columnCount(); i++) {
Kacper Kasper's avatar
Kacper Kasper committed
785
            if(!isColumnHidden(i))
786
787
788
                m_weightDirty.append(i);
        }
        return;
789
790
    }

791
    if(!m_weightDirty.contains(column))
792
        m_weightDirty.append(column);
793
794
}

795
796
void Playlist::slotShowPlaying()
{
797
    if(!playingItem())
798
        return;
799

800
    Playlist *l = playingItem()->playlist();
801
802

    l->clearSelection();
803
804
805
806
807
808
809
810

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

811
    l->setCurrentItem(playingItem());
812
    l->scrollToItem(playingItem(), QAbstractItemView::PositionAtCenter);
813
814
}

815
816
void Playlist::slotColumnResizeModeChanged()
{
817
818
819
820
821
822
823
    if(manualResize()) {
        header()->setSectionResizeMode(QHeaderView::Interactive);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    } else {
        header()->setSectionResizeMode(QHeaderView::Fixed);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    }
824

825
    if(!manualResize())
826
        slotUpdateColumnWidths();
827
828
829
830

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

831
void Playlist::playlistItemsChanged()
832
833
{
    if(m_blockDataChanged)
834
        return;
835
    PlaylistInterface::playlistItemsChanged();
836
837
}

838
////////////////////////////////////////////////////////////////////////////////
839
// protected members
840
841
////////////////////////////////////////////////////////////////////////////////

842
void Playlist::removeFromDisk(const PlaylistItemList &items)
843
{
844
845
846
    if(!isVisible() || items.isEmpty()) {
        return;
    }
847

848
849
850
851
    QStringList files;
    for(const auto &item : items) {
        files.append(item->file().absFilePath());
    }
852

853
    DeleteDialog dialog(this);
854

855
    m_blockDataChanged = true;
856

857
858
859
    if(dialog.confirmDeleteList(files)) {
        bool shouldDelete = dialog.shouldDelete();
        QStringList errorFiles;
860

861
862
863
        for(const auto &item : items) {
            if(playingItem() == item)
                action("forward")->trigger();
864

865
866
867
868
869
870
            QString removePath = item->file().absFilePath();
            QUrl removeUrl = QUrl::fromLocalFile(removePath);
            if((!shouldDelete && KIO::trash(removeUrl)->exec()) ||
               (shouldDelete && QFile::remove(removePath)))
            {
                delete item->collectionItem();
871
            }
872
873
874
            else
                errorFiles.append(item->file().absFilePath());
        }
875

876
877
878
879
880
        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);
881
        }
882
    }
883

884
    m_blockDataChanged = false;
885

886
    playlistItemsChanged();
887
888
}

889
890
void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList)
{
891
    // direct call to ::items to avoid infinite loop, bug 402355
892
    m_randomSequence.clear();
893
    clearItems(Playlist::items());
894
    createItems(itemList);
895
896
}

897
898
899
900
901
902
903
904
905
906
907
908
void Playlist::beginPlayingItem(PlaylistItem *itemToPlay)
{
    if(itemToPlay) {
        setPlaying(itemToPlay, true);
        m_collection->requestPlaybackFor(itemToPlay->file());
    }
    else {
        setPlaying(nullptr);
        action("stop")->trigger();
    }
}

Kacper Kasper's avatar
Kacper Kasper committed
909
void Playlist::dragEnterEvent(QDragEnterEvent *e)
910
{
Michael Pyne's avatar
Michael Pyne committed
911
    if(CoverDrag::isCover(e->mimeData())) {
Kacper Kasper's avatar
Kacper Kasper committed
912
        setDropIndicatorShown(false);
913
914
        e->accept();
        return;
Michael Pyne's avatar
Michael Pyne committed
915
916
    }

917
918
    if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) {
        setDropIndicatorShown(true);
Kacper Kasper's avatar
Kacper Kasper committed
919
        e->acceptProposedAction();
920
    }
921
922
    else
        e->ignore();
Kacper Kasper's avatar
Kacper Kasper committed
923
}
Michael Pyne's avatar
Michael Pyne committed
924

925
void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after)
Michael Pyne's avatar
Michael Pyne committed
926
{
927
928
929
    if(!urls->hasUrls()) {
        return;
    }
Michael Pyne's avatar
Michael Pyne committed
930

931
    addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after);
932
933
}

934
bool Playlist::eventFilter(QObject *watched, QEvent *e)
935
{
936
    if(watched == header()) {
937
938
939
        switch(e->type()) {
        case QEvent::MouseMove:
        {
940
            if((static_cast<QMouseEvent *>(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton &&
941
942
943
944
945
946
947
948
949
950
951
952
                !action<KToggleAction>("resizeColumnsManually")->isChecked())
            {
                m_columnWidthModeChanged = true;

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

            break;
        }
        case QEvent::MouseButtonPress:
        {
953
            if(static_cast<QMouseEvent *>(e)->button() == Qt::RightButton)
954
955
956
957
958
959
960
961
962
963
964
965
                m_headerMenu->popup(QCursor::pos());

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

            if(!manualResize() && m_widthsDirty)
966
                QTimer::singleShot(0, this, &Playlist::slotUpdateColumnWidths);
967
968
969
970
971
            break;
        }
        default:
            break;
        }
972
973
    }

974
    return QTreeWidget::eventFilter(watched, e);
975
976
}

977
978
void Playlist::keyPressEvent(QKeyEvent *event)
{
979
    if(event->key() == Qt::Key_Up) {
980
        if(const auto activeItem = currentItem()) {
981
            QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden);
982
            if(activeItem == *visible) {
983
984
985
                emit signalMoveFocusAway();
                event->accept();
            }
986
        }