File indexing completed on 2023-05-30 11:30:49

0001 /**
0002  * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
0003  * Copyright (C) 2008-2018 Michael Pyne <mpyne@kde.org>
0004  *
0005  * This program is free software; you can redistribute it and/or modify it under
0006  * the terms of the GNU General Public License as published by the Free Software
0007  * Foundation; either version 2 of the License, or (at your option) any later
0008  * version.
0009  *
0010  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0011  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0012  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU General Public License along with
0015  * this program.  If not, see <http://www.gnu.org/licenses/>.
0016  */
0017 
0018 #include "playlist.h"
0019 #include "juk-exception.h"
0020 
0021 #include <KLocalizedString>
0022 #include <KSharedConfig>
0023 #include <kconfig.h>
0024 #include <kmessagebox.h>
0025 #include <kiconloader.h>
0026 #include <klineedit.h>
0027 #include <kio/copyjob.h>
0028 #include <kactioncollection.h>
0029 #include <kconfiggroup.h>
0030 #include <ktoolbarpopupaction.h>
0031 #include <kactionmenu.h>
0032 #include <ktoggleaction.h>
0033 
0034 #include <QActionGroup>
0035 #include <QClipboard>
0036 #include <QCursor>
0037 #include <QDesktopServices>
0038 #include <QDir>
0039 #include <QDragEnterEvent>
0040 #include <QDropEvent>
0041 #include <QFile>
0042 #include <QFileDialog>
0043 #include <QHeaderView>
0044 #include <QKeyEvent>
0045 #include <QList>
0046 #include <QMenu>
0047 #include <QMimeData>
0048 #include <QMouseEvent>
0049 #include <QPainter>
0050 #include <QResizeEvent>
0051 #include <QScrollBar>
0052 #include <QSet>
0053 #include <QStackedWidget>
0054 #include <QTextStream>
0055 #include <QTimer>
0056 
0057 #include <QtConcurrent>
0058 #include <QFutureWatcher>
0059 
0060 #include <id3v1genres.h>
0061 
0062 #include <time.h>
0063 #include <cmath>
0064 #include <algorithm>
0065 #include <random>
0066 #include <utility>
0067 
0068 #include "actioncollection.h"
0069 #include "cache.h"
0070 #include "collectionlist.h"
0071 #include "coverdialog.h"
0072 #include "coverinfo.h"
0073 #include "deletedialog.h"
0074 #include "directoryloader.h"
0075 #include "filerenamer.h"
0076 #include "iconsupport.h"
0077 #include "juk_debug.h"
0078 #include "juktag.h"
0079 #include "mediafiles.h"
0080 #include "playlistcollection.h"
0081 #include "playlistitem.h"
0082 #include "playlistsearch.h"
0083 #include "playlistsharedsettings.h"
0084 #include "tagtransactionmanager.h"
0085 #include "upcomingplaylist.h"
0086 #include "webimagefetcher.h"
0087 
0088 using namespace ActionCollection; // ""_act and others
0089 
0090 /**
0091  * Used to give every track added in the program a unique identifier. See
0092  * PlaylistItem
0093  */
0094 quint32 g_trackID = 0;
0095 
0096 /**
0097  * Just a shortcut of sorts.
0098  */
0099 
0100 static bool manualResize()
0101 {
0102     return "resizeColumnsManually"_act->isChecked();
0103 }
0104 
0105 ////////////////////////////////////////////////////////////////////////////////
0106 // static members
0107 ////////////////////////////////////////////////////////////////////////////////
0108 
0109 bool                    Playlist::m_visibleChanged = false;
0110 bool                    Playlist::m_shuttingDown   = false;
0111 PlaylistItemList        Playlist::m_history;
0112 QVector<PlaylistItem *> Playlist::m_backMenuItems;
0113 int                     Playlist::m_leftColumn     = 0;
0114 
0115 ////////////////////////////////////////////////////////////////////////////////
0116 // public members
0117 ////////////////////////////////////////////////////////////////////////////////
0118 
0119 Playlist::Playlist(
0120         bool delaySetup, const QString &name,
0121         PlaylistCollection *collection, const QString &iconName,
0122         int extraCols)
0123   : QTreeWidget(collection->playlistStack())
0124   , m_collection(collection)
0125   , m_playlistName(name)
0126   , m_refillDebounce(new QTimer(this))
0127   , m_fetcher(new WebImageFetcher(this))
0128 {
0129     setup(extraCols);
0130 
0131     // The timer soaks up repeated events that may cause the random play list
0132     // to be regenerated repeatedly.
0133     m_refillDebounce->setInterval(100);
0134     m_refillDebounce->setSingleShot(true);
0135     connect(m_refillDebounce, &QTimer::timeout,
0136             this,             &Playlist::refillRandomList);
0137 
0138     // Any of the random-related actions being triggered will cause the parent
0139     // group to emit the triggered signal.
0140     QActionGroup *randomGroup = action("disableRandomPlay")->actionGroup();
0141     connect(randomGroup,      &QActionGroup::triggered,
0142             m_refillDebounce, qOverload<>(&QTimer::start));
0143 
0144     // Some subclasses need to do even more handling but will remember to
0145     // call setupPlaylist
0146     if(!delaySetup) {
0147         collection->setupPlaylist(this, iconName);
0148     }
0149 }
0150 
0151 Playlist::Playlist(PlaylistCollection *collection, const QString &name,
0152                    const QString &iconName)
0153     : Playlist(false, name, collection, iconName, 0)
0154 {
0155 }
0156 
0157 Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items,
0158                    const QString &name, const QString &iconName)
0159     : Playlist(false, name, collection, iconName, 0)
0160 {
0161     createItems(items);
0162 }
0163 
0164 Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile,
0165                    const QString &iconName)
0166     : Playlist(true, QString(), collection, iconName, 0)
0167 {
0168     m_fileName = playlistFile.canonicalFilePath();
0169 
0170     // Load the file after construction completes so that virtual methods in
0171     // subclasses can take effect.
0172     QTimer::singleShot(0, this, [=]() {
0173         loadFile(m_fileName, playlistFile);
0174         collection->setupPlaylist(this, iconName);
0175     });
0176 }
0177 
0178 Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns)
0179     : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns)
0180 {
0181 }
0182 
0183 Playlist::~Playlist()
0184 {
0185     // clearItem() will take care of removing the items from the history,
0186     // so call clearItems() to make sure it happens.
0187     //
0188     // Some subclasses override clearItems and items so we manually dispatch to
0189     // make clear that it's intentional that those subclassed versions don't
0190     // get called (because we can't call them)
0191 
0192     m_randomSequence.clear();
0193     Playlist::clearItems(Playlist::items());
0194 
0195     if(!m_shuttingDown)
0196         m_collection->removePlaylist(this);
0197 }
0198 
0199 QString Playlist::name() const
0200 {
0201     if(m_playlistName.isEmpty())
0202         return m_fileName.section(QDir::separator(), -1).section('.', 0, -2);
0203     else
0204         return m_playlistName;
0205 }
0206 
0207 FileHandle Playlist::currentFile() const
0208 {
0209     return playingItem() ? playingItem()->file() : FileHandle();
0210 }
0211 
0212 void Playlist::playFirst()
0213 {
0214     QTreeWidgetItemIterator listIt(const_cast<Playlist *>(this), QTreeWidgetItemIterator::NotHidden);
0215     beginPlayingItem(static_cast<PlaylistItem *>(*listIt));
0216     refillRandomList();
0217 }
0218 
0219 void Playlist::playNextAlbum()
0220 {
0221     const auto &item = playingItem();
0222     if(!item || !action("albumRandomPlay")->isChecked()) {
0223         playNext();
0224         return;
0225     }
0226 
0227     const auto currentAlbum = item->file().tag()->album();
0228     const auto nextAlbumTrack = std::find_if(m_randomSequence.begin(), m_randomSequence.end(),
0229             [currentAlbum](const PlaylistItem *item) {
0230                 return item->file().tag()->album() != currentAlbum;
0231             });
0232 
0233     if(nextAlbumTrack == m_randomSequence.end()) {
0234         // We were on the last album, playNext will handle looping if we should loop
0235         m_randomSequence.clear();
0236         playNext();
0237     }
0238     else {
0239         m_randomSequence.erase(m_randomSequence.begin(), nextAlbumTrack);
0240         beginPlayingItem(*nextAlbumTrack);
0241     }
0242 }
0243 
0244 void Playlist::playNext()
0245 {
0246     PlaylistItem *next = nullptr;
0247     auto nowPlaying = playingItem();
0248     bool doLoop = action("loopPlaylist")->isChecked();
0249 
0250     // Treat an item from a different playlist as if we were being asked to
0251     // play from a stop
0252     if(nowPlaying && nowPlaying->playlist() != this) {
0253         nowPlaying = nullptr;
0254     }
0255 
0256     if(action("disableRandomPlay")->isChecked()) {
0257         QTreeWidgetItemIterator listIt = nowPlaying
0258             ? QTreeWidgetItemIterator(nowPlaying, QTreeWidgetItemIterator::NotHidden)
0259             : QTreeWidgetItemIterator(this,       QTreeWidgetItemIterator::NotHidden);
0260 
0261         if(*listIt && nowPlaying) {
0262             ++listIt;
0263         }
0264 
0265         next = static_cast<PlaylistItem *>(*listIt);
0266 
0267         if(!next && doLoop) {
0268             playFirst();
0269             return;
0270         }
0271     }
0272     else {
0273         // The two random play modes are identical here, the difference is in how the
0274         // randomized sequence is generated by refillRandomList
0275 
0276         if(m_randomSequence.isEmpty() && (doLoop || !nowPlaying)) {
0277             refillRandomList();
0278 
0279             // Don't play the same track twice in a row even if it can
0280             // "randomly" happen
0281             if(m_randomSequence.front() == nowPlaying) {
0282                 std::swap(m_randomSequence.front(), m_randomSequence.back());
0283             }
0284         }
0285 
0286         if(!m_randomSequence.isEmpty()) {
0287             next = m_randomSequence.takeFirst();
0288         }
0289     }
0290 
0291     // Will stop playback if next is still null
0292     beginPlayingItem(next);
0293 }
0294 
0295 void Playlist::stop()
0296 {
0297     m_history.clear();
0298     setPlaying(nullptr);
0299 }
0300 
0301 void Playlist::playPrevious()
0302 {
0303     if(!playingItem())
0304         return;
0305 
0306     bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked();
0307 
0308     PlaylistItem *previous = nullptr;
0309 
0310     if(random && !m_history.isEmpty()) {
0311         PlaylistItemList::Iterator last = m_history.end() - 1;
0312         previous = *last;
0313         m_history.erase(last);
0314     }
0315     else {
0316         m_history.clear();
0317         QTreeWidgetItemIterator listIt(playingItem(), QTreeWidgetItemIterator::NotHidden);
0318         previous = static_cast<PlaylistItem *>(*--listIt);
0319     }
0320 
0321     beginPlayingItem(previous);
0322 }
0323 
0324 void Playlist::setName(const QString &n)
0325 {
0326     m_collection->addNameToDict(n);
0327     m_collection->removeNameFromDict(m_playlistName);
0328 
0329     m_playlistName = n;
0330     emit signalNameChanged(m_playlistName);
0331 }
0332 
0333 void Playlist::save()
0334 {
0335     if(m_fileName.isEmpty())
0336         return saveAs();
0337 
0338     QFile file(m_fileName);
0339 
0340     if(!file.open(QIODevice::WriteOnly))
0341         return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName));
0342 
0343     QTextStream stream(&file);
0344 
0345     const QStringList fileList = files();
0346 
0347     for(const auto &file : fileList) {
0348         stream << file << '\n';
0349     }
0350 
0351     file.close();
0352 }
0353 
0354 void Playlist::saveAs()
0355 {
0356     m_collection->removeFileFromDict(m_fileName);
0357 
0358     m_fileName = MediaFiles::savePlaylistDialog(name(), this);
0359 
0360     if(!m_fileName.isEmpty()) {
0361         m_collection->addFileToDict(m_fileName);
0362 
0363         // If there's no playlist name set, use the file name.
0364         if(m_playlistName.isEmpty())
0365             emit signalNameChanged(name());
0366         save();
0367     }
0368 }
0369 
0370 void Playlist::updateDeletedItem(PlaylistItem *item)
0371 {
0372     m_members.remove(item->file().absFilePath());
0373     m_randomSequence.removeAll(item);
0374     m_history.removeAll(item);
0375 }
0376 
0377 void Playlist::clearItem(PlaylistItem *item)
0378 {
0379     // Automatically updates internal structs via updateDeletedItem
0380     delete item;
0381 
0382     playlistItemsChanged();
0383 }
0384 
0385 void Playlist::clearItems(const PlaylistItemList &items)
0386 {
0387     for(auto &item : items) {
0388         delete item;
0389     }
0390     playlistItemsChanged();
0391 }
0392 
0393 PlaylistItem *Playlist::playingItem() // static
0394 {
0395     return PlaylistItem::playingItems().isEmpty()
0396         ? nullptr
0397         : PlaylistItem::playingItems().front();
0398 }
0399 
0400 QStringList Playlist::files() const
0401 {
0402     QStringList list;
0403 
0404     for(QTreeWidgetItemIterator it(const_cast<Playlist *>(this)); *it; ++it)
0405         list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath());
0406 
0407     return list;
0408 }
0409 
0410 PlaylistItemList Playlist::items()
0411 {
0412     return items(QTreeWidgetItemIterator::IteratorFlag(0));
0413 }
0414 
0415 PlaylistItemList Playlist::visibleItems()
0416 {
0417     return items(QTreeWidgetItemIterator::NotHidden);
0418 }
0419 
0420 PlaylistItemList Playlist::selectedItems()
0421 {
0422     return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden);
0423 }
0424 
0425 PlaylistItem *Playlist::firstChild() const
0426 {
0427     return static_cast<PlaylistItem *>(topLevelItem(0));
0428 }
0429 
0430 void Playlist::updateLeftColumn()
0431 {
0432     int newLeftColumn = leftMostVisibleColumn();
0433 
0434     if(m_leftColumn != newLeftColumn) {
0435         updatePlaying();
0436         m_leftColumn = newLeftColumn;
0437     }
0438 }
0439 
0440 void Playlist::setItemsVisible(const QModelIndexList &indexes, bool visible) // static
0441 {
0442     m_visibleChanged = true;
0443 
0444     for(QModelIndex index : indexes)
0445         itemFromIndex(index)->setHidden(!visible);
0446 }
0447 
0448 void Playlist::setSearch(PlaylistSearch* s)
0449 {
0450     m_search = s;
0451 
0452     if(!m_searchEnabled)
0453         return;
0454 
0455     for(int row = 0; row < topLevelItemCount(); ++row)
0456         topLevelItem(row)->setHidden(true);
0457     setItemsVisible(s->matchedItems(), true);
0458 }
0459 
0460 void Playlist::setSearchEnabled(bool enabled)
0461 {
0462     if(m_searchEnabled == enabled)
0463         return;
0464 
0465     m_searchEnabled = enabled;
0466 
0467     if(enabled) {
0468         for(int row = 0; row < topLevelItemCount(); ++row)
0469             topLevelItem(row)->setHidden(true);
0470         setItemsVisible(m_search->matchedItems(), true);
0471     }
0472     else {
0473         const auto &playlistItems = items();
0474         for(PlaylistItem* item : playlistItems)
0475             item->setHidden(false);
0476     }
0477 }
0478 
0479 // Mostly seems to be for DynamicPlaylist
0480 // TODO: See if this can't all be eliminated by making 'is-playing' a predicate
0481 // of the playlist item itself
0482 void Playlist::synchronizePlayingItems(Playlist *playlist, bool setMaster)
0483 {
0484     if(!playlist || !playlist->playing())
0485         return;
0486 
0487     CollectionListItem *base = playingItem()->collectionItem();
0488     for(QTreeWidgetItemIterator itemIt(playlist); *itemIt; ++itemIt) {
0489         PlaylistItem *item = static_cast<PlaylistItem *>(*itemIt);
0490         if(base == item->collectionItem()) {
0491             item->setPlaying(true, setMaster);
0492             return;
0493         }
0494     }
0495 }
0496 
0497 void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster)
0498 {
0499     for(auto p : sources) {
0500         synchronizePlayingItems(p, setMaster);
0501     }
0502 }
0503 
0504 ////////////////////////////////////////////////////////////////////////////////
0505 // public slots
0506 ////////////////////////////////////////////////////////////////////////////////
0507 
0508 void Playlist::copy()
0509 {
0510     const PlaylistItemList items = selectedItems();
0511     QList<QUrl> urls;
0512 
0513     for(const auto &item : items) {
0514         urls << QUrl::fromLocalFile(item->file().absFilePath());
0515     }
0516 
0517     QMimeData *mimeData = new QMimeData;
0518     mimeData->setUrls(urls);
0519 
0520     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
0521 }
0522 
0523 void Playlist::paste()
0524 {
0525     addFilesFromMimeData(
0526         QApplication::clipboard()->mimeData(),
0527         static_cast<PlaylistItem *>(currentItem()));
0528 }
0529 
0530 void Playlist::clear()
0531 {
0532     PlaylistItemList l = selectedItems();
0533     if(l.isEmpty())
0534         l = items();
0535 
0536     clearItems(l);
0537 }
0538 
0539 void Playlist::slotRefresh()
0540 {
0541     PlaylistItemList itemList = selectedItems();
0542     if(itemList.isEmpty())
0543         itemList = visibleItems();
0544 
0545     QApplication::setOverrideCursor(Qt::WaitCursor);
0546     for(auto &item : itemList) {
0547         item->refreshFromDisk();
0548 
0549         if(!item->file().tag() || !item->file().fileInfo().exists()) {
0550             qCDebug(JUK_LOG) << "Error while trying to refresh the tag.  "
0551                            << "This file has probably been removed.";
0552             delete item->collectionItem();
0553         }
0554 
0555         processEvents();
0556     }
0557     QApplication::restoreOverrideCursor();
0558 }
0559 
0560 void Playlist::slotOpenItemDir()
0561 {
0562     PlaylistItemList itemList = selectedItems();
0563     QList<QUrl> pathList;
0564 
0565     for(auto &item : itemList) {
0566         QUrl path = QUrl::fromLocalFile(item->file().fileInfo().absoluteDir().absolutePath());
0567         if(!pathList.contains(path))
0568             pathList.append(path);
0569     }
0570 
0571     if (pathList.length() > 4) {
0572         if(KMessageBox::warningContinueCancel(
0573             this,
0574             i18np("You are about to open directory. Are you sure you want to continue?",
0575                   "You are about to open %1 directories. Are you sure you want to continue?",
0576                   pathList.length()),
0577             i18n("Open Containing Folder")
0578         ) == KMessageBox::Cancel)
0579         {
0580             return;
0581         }
0582     }
0583 
0584     QApplication::setOverrideCursor(Qt::WaitCursor);
0585     for(auto &path : pathList) {
0586         QDesktopServices::openUrl(path);
0587 
0588         processEvents();
0589     }
0590     QApplication::restoreOverrideCursor();
0591 }
0592 
0593 void Playlist::slotRenameFile()
0594 {
0595     FileRenamer renamer;
0596     PlaylistItemList items = selectedItems();
0597 
0598     if(items.isEmpty())
0599         return;
0600 
0601     emit signalEnableDirWatch(false);
0602 
0603     m_blockDataChanged = true;
0604     renamer.rename(items);
0605     m_blockDataChanged = false;
0606     playlistItemsChanged();
0607 
0608     emit signalEnableDirWatch(true);
0609 }
0610 
0611 void Playlist::slotBeginPlayback()
0612 {
0613     QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden);
0614     PlaylistItem *item = static_cast<PlaylistItem *>(*visible);
0615 
0616     if(item) {
0617         refillRandomList();
0618         playNext();
0619     }
0620     else {
0621         action("stop")->trigger();
0622     }
0623 }
0624 
0625 void Playlist::slotViewCover()
0626 {
0627     const PlaylistItemList items = selectedItems();
0628     for(const auto &item : items) {
0629         const auto cover = item->file().coverInfo();
0630 
0631         if(cover->hasCover()) {
0632             cover->popup();
0633             return; // If we select multiple items, only show one
0634         }
0635     }
0636 }
0637 
0638 void Playlist::slotRemoveCover()
0639 {
0640     PlaylistItemList items = selectedItems();
0641     if(items.isEmpty())
0642         return;
0643     int button = KMessageBox::warningContinueCancel(this,
0644                                                     i18n("Are you sure you want to delete these covers?"),
0645                                                     QString(),
0646                                                     KGuiItem(i18n("&Delete Covers")));
0647     if(button == KMessageBox::Continue)
0648         refreshAlbums(items);
0649 }
0650 
0651 void Playlist::slotShowCoverManager()
0652 {
0653     static CoverDialog *managerDialog = 0;
0654 
0655     if(!managerDialog)
0656         managerDialog = new CoverDialog(this);
0657 
0658     managerDialog->show();
0659 }
0660 
0661 void Playlist::slotAddCover(bool retrieveLocal)
0662 {
0663     PlaylistItemList items = selectedItems();
0664 
0665     if(items.isEmpty())
0666         return;
0667 
0668     if(!retrieveLocal) {
0669         m_fetcher->setFile((*items.begin())->file());
0670         m_fetcher->searchCover();
0671         return;
0672     }
0673 
0674     QUrl file = QFileDialog::getOpenFileUrl(
0675         this, i18n("Select Cover Image File"),
0676         QUrl::fromLocalFile(QDir::home().path()),
0677         i18n("Images (*.png *.jpg)"), nullptr,
0678         {}, QStringList() << QStringLiteral("file")
0679         );
0680     if(file.isEmpty())
0681         return;
0682 
0683     QString artist = items.front()->file().tag()->artist();
0684     QString album = items.front()->file().tag()->album();
0685 
0686     coverKey newId = CoverManager::addCover(file, artist, album);
0687 
0688     if(newId != CoverManager::NoMatch)
0689         refreshAlbums(items, newId);
0690 }
0691 
0692 // Called when image fetcher has added a new cover.
0693 void Playlist::slotCoverChanged(int coverId)
0694 {
0695     qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.";
0696     refreshAlbums(selectedItems(), coverId);
0697 }
0698 
0699 void Playlist::slotGuessTagInfo(TagGuesser::Type type)
0700 {
0701     QApplication::setOverrideCursor(Qt::WaitCursor);
0702     const PlaylistItemList items = selectedItems();
0703     setDynamicListsFrozen(true);
0704 
0705     m_blockDataChanged = true;
0706 
0707     for(auto &item : items) {
0708         item->guessTagInfo(type);
0709         processEvents();
0710     }
0711 
0712     // MusicBrainz queries automatically commit at this point.  What would
0713     // be nice is having a signal emitted when the last query is completed.
0714 
0715     if(type == TagGuesser::FileName)
0716         TagTransactionManager::instance()->commit();
0717 
0718     m_blockDataChanged = false;
0719 
0720     playlistItemsChanged();
0721     setDynamicListsFrozen(false);
0722     QApplication::restoreOverrideCursor();
0723 }
0724 
0725 void Playlist::slotReload()
0726 {
0727     QFileInfo fileInfo(m_fileName);
0728     if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable())
0729         return;
0730 
0731     clearItems(items());
0732     loadFile(m_fileName, fileInfo);
0733 }
0734 
0735 void Playlist::refillRandomList()
0736 {
0737     qCDebug(JUK_LOG) << "Refilling random items.";
0738 
0739     if(action("disableRandomPlay")->isChecked()) {
0740         m_randomSequence.clear();
0741         return;
0742     }
0743 
0744     PlaylistItemList randomItems = visibleItems();
0745 
0746     // See https://www.pcg-random.org/posts/cpp-seeding-surprises.html
0747     std::random_device rdev;
0748     uint64_t rseed = (uint64_t(rdev()) << 32) | rdev();
0749     std::linear_congruential_engine<
0750         uint64_t, 6364136223846793005U, 1442695040888963407U, 0U
0751         > knuth_lcg(rseed);
0752 
0753     std::shuffle(randomItems.begin(), randomItems.end(), knuth_lcg);
0754 
0755     if(action("albumRandomPlay")->isChecked()) {
0756         std::sort(randomItems.begin(), randomItems.end(),
0757             [](PlaylistItem *a, PlaylistItem *b) {
0758                 return a->file().tag()->album() < b->file().tag()->album();
0759             });
0760 
0761         // If there is an item playing from our playlist already, move its
0762         // album to the front
0763 
0764         const auto wasPlaying = playingItem();
0765         if(wasPlaying && wasPlaying->playlist() == this) {
0766             const auto playingAlbum = wasPlaying->file().tag()->album();
0767             std::stable_partition(randomItems.begin(), randomItems.end(),
0768                 [playingAlbum](const PlaylistItem *item) {
0769                     return item->file().tag()->album() == playingAlbum;
0770                 });
0771         }
0772     }
0773 
0774     std::swap(m_randomSequence, randomItems);
0775 }
0776 
0777 void Playlist::slotWeightDirty(int column)
0778 {
0779     if(column < 0) {
0780         m_weightDirty.clear();
0781         for(int i = 0; i < columnCount(); i++) {
0782             if(!isColumnHidden(i))
0783                 m_weightDirty.append(i);
0784         }
0785         return;
0786     }
0787 
0788     if(!m_weightDirty.contains(column))
0789         m_weightDirty.append(column);
0790 }
0791 
0792 void Playlist::slotShowPlaying()
0793 {
0794     if(!playingItem())
0795         return;
0796 
0797     Playlist *l = playingItem()->playlist();
0798 
0799     l->clearSelection();
0800 
0801     // Raise the playlist before selecting the items otherwise the tag editor
0802     // will not update when it gets the selectionChanged() notification
0803     // because it will think the user is choosing a different playlist but not
0804     // selecting a different item.
0805 
0806     m_collection->raise(l);
0807 
0808     l->setCurrentItem(playingItem());
0809     l->scrollToItem(playingItem(), QAbstractItemView::PositionAtCenter);
0810 }
0811 
0812 void Playlist::slotColumnResizeModeChanged()
0813 {
0814     if(manualResize()) {
0815         header()->setSectionResizeMode(QHeaderView::Interactive);
0816         setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0817     } else {
0818         header()->setSectionResizeMode(QHeaderView::Fixed);
0819         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0820     }
0821 
0822     if(!manualResize())
0823         slotUpdateColumnWidths();
0824 
0825     SharedSettings::instance()->sync();
0826 }
0827 
0828 void Playlist::playlistItemsChanged()
0829 {
0830     if(m_blockDataChanged)
0831         return;
0832     PlaylistInterface::playlistItemsChanged();
0833 }
0834 
0835 ////////////////////////////////////////////////////////////////////////////////
0836 // protected members
0837 ////////////////////////////////////////////////////////////////////////////////
0838 
0839 void Playlist::removeFromDisk(const PlaylistItemList &items)
0840 {
0841     if(!isVisible() || items.isEmpty()) {
0842         return;
0843     }
0844 
0845     QStringList files;
0846     for(const auto &item : items) {
0847         files.append(item->file().absFilePath());
0848     }
0849 
0850     DeleteDialog dialog(this);
0851 
0852     m_blockDataChanged = true;
0853 
0854     if(dialog.confirmDeleteList(files)) {
0855         bool shouldDelete = dialog.shouldDelete();
0856         QStringList errorFiles;
0857 
0858         for(const auto &item : items) {
0859             if(playingItem() == item)
0860                 action("forward")->trigger();
0861 
0862             QString removePath = item->file().absFilePath();
0863             QUrl removeUrl = QUrl::fromLocalFile(removePath);
0864             if((!shouldDelete && KIO::trash(removeUrl)->exec()) ||
0865                (shouldDelete && QFile::remove(removePath)))
0866             {
0867                 delete item->collectionItem();
0868             }
0869             else
0870                 errorFiles.append(item->file().absFilePath());
0871         }
0872 
0873         if(!errorFiles.isEmpty()) {
0874             QString errorMsg = shouldDelete ?
0875                     i18n("Could not delete these files") :
0876                     i18n("Could not move these files to the Trash");
0877             KMessageBox::errorList(this, errorMsg, errorFiles);
0878         }
0879     }
0880 
0881     m_blockDataChanged = false;
0882 
0883     playlistItemsChanged();
0884 }
0885 
0886 void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList)
0887 {
0888     // direct call to ::items to avoid infinite loop, bug 402355
0889     m_randomSequence.clear();
0890     clearItems(Playlist::items());
0891     createItems(itemList);
0892 }
0893 
0894 void Playlist::beginPlayingItem(PlaylistItem *itemToPlay)
0895 {
0896     if(itemToPlay) {
0897         setPlaying(itemToPlay, true);
0898         m_collection->requestPlaybackFor(itemToPlay->file());
0899     }
0900     else {
0901         setPlaying(nullptr);
0902         action("stop")->trigger();
0903     }
0904 }
0905 
0906 void Playlist::dragEnterEvent(QDragEnterEvent *e)
0907 {
0908     if(CoverDrag::isCover(e->mimeData())) {
0909         setDropIndicatorShown(false);
0910         e->accept();
0911         return;
0912     }
0913 
0914     if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) {
0915         setDropIndicatorShown(true);
0916         e->acceptProposedAction();
0917     }
0918     else
0919         e->ignore();
0920 }
0921 
0922 void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after)
0923 {
0924     if(!urls->hasUrls()) {
0925         return;
0926     }
0927 
0928     addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after);
0929 }
0930 
0931 bool Playlist::eventFilter(QObject *watched, QEvent *e)
0932 {
0933     if(watched == header()) {
0934         switch(e->type()) {
0935         case QEvent::MouseMove:
0936         {
0937             if((static_cast<QMouseEvent *>(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton &&
0938                 !action<KToggleAction>("resizeColumnsManually")->isChecked())
0939             {
0940                 m_columnWidthModeChanged = true;
0941 
0942                 action<KToggleAction>("resizeColumnsManually")->setChecked(true);
0943                 slotColumnResizeModeChanged();
0944             }
0945 
0946             break;
0947         }
0948         case QEvent::MouseButtonPress:
0949         {
0950             if(static_cast<QMouseEvent *>(e)->button() == Qt::RightButton)
0951                 m_headerMenu->popup(QCursor::pos());
0952 
0953             break;
0954         }
0955         case QEvent::MouseButtonRelease:
0956         {
0957             if(m_columnWidthModeChanged) {
0958                 m_columnWidthModeChanged = false;
0959                 notifyUserColumnWidthModeChanged();
0960             }
0961 
0962             if(!manualResize() && m_widthsDirty)
0963                 QTimer::singleShot(0, this, &Playlist::slotUpdateColumnWidths);
0964             break;
0965         }
0966         default:
0967             break;
0968         }
0969     }
0970 
0971     return QTreeWidget::eventFilter(watched, e);
0972 }
0973 
0974 void Playlist::keyPressEvent(QKeyEvent *event)
0975 {
0976     if(event->key() == Qt::Key_Up) {
0977         if(const auto activeItem = currentItem()) {
0978             QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden);
0979             if(activeItem == *visible) {
0980                 emit signalMoveFocusAway();
0981                 event->accept();
0982             }
0983         }
0984     }
0985     else if(event->key() == Qt::Key_Return && !event->isAutoRepeat()) {
0986         event->accept();
0987         slotPlayCurrent();
0988         return; // event completely handled already
0989     }
0990 
0991     QTreeWidget::keyPressEvent(event);
0992 }
0993 
0994 QStringList Playlist::mimeTypes() const
0995 {
0996     return QStringList("text/uri-list");
0997 }
0998 
0999 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1000 QMimeData* Playlist::mimeData(const QList<QTreeWidgetItem *> items) const
1001 #else
1002 QMimeData* Playlist::mimeData(const QList<QTreeWidgetItem *> &items) const
1003 #endif
1004 {
1005     QList<QUrl> urls;
1006     for(const auto &item : items) {
1007         urls << QUrl::fromLocalFile(static_cast<const PlaylistItem*>(item)->file().absFilePath());
1008     }
1009 
1010     QMimeData *urlDrag = new QMimeData();
1011     urlDrag->setUrls(urls);
1012 
1013     return urlDrag;
1014 }
1015 
1016 bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action)
1017 {
1018     // TODO: Re-add DND
1019     Q_UNUSED(parent);
1020     Q_UNUSED(index);
1021     Q_UNUSED(data);
1022     Q_UNUSED(action);
1023 
1024     return false;
1025 }
1026 
1027 void Playlist::dropEvent(QDropEvent *e)
1028 {
1029     QPoint vp = e->pos();
1030     PlaylistItem *item = static_cast<PlaylistItem *>(itemAt(vp));
1031 
1032     // First see if we're dropping a cover, if so we can get it out of the
1033     // way early.
1034     if(item && CoverDrag::isCover(e->mimeData())) {
1035         coverKey id = CoverDrag::idFromData(e->mimeData());
1036 
1037         // If the item we dropped on is selected, apply cover to all selected
1038         // items, otherwise just apply to the dropped item.
1039 
1040         if(item->isSelected()) {
1041             const PlaylistItemList selItems = selectedItems();
1042             for(auto &playlistItem : selItems) {
1043                 playlistItem->file().coverInfo()->setCoverId(id);
1044                 playlistItem->refresh();
1045             }
1046         }
1047         else {
1048             item->file().coverInfo()->setCoverId(id);
1049             item->refresh();
1050         }
1051 
1052         return;
1053     }
1054 
1055     // When dropping on the toUpper half of an item, insert before this item.
1056     // This is what the user expects, and also allows the insertion at
1057     // top of the list
1058 
1059     QRect rect = visualItemRect(item);
1060     if(!item)
1061         item = static_cast<PlaylistItem *>(topLevelItem(topLevelItemCount() - 1));
1062     else if(vp.y() < rect.y() + rect.height() / 2)
1063         item = static_cast<PlaylistItem *>(item->itemAbove());
1064 
1065     m_blockDataChanged = true;
1066 
1067     if(e->source() == this) {
1068 
1069         // Since we're trying to arrange things manually, turn off sorting.
1070 
1071         sortItems(columnCount() + 1, Qt::AscendingOrder);
1072 
1073         const QList<QTreeWidgetItem *> items = QTreeWidget::selectedItems();
1074         int insertIndex = item ? indexOfTopLevelItem(item) : 0;
1075 
1076         // Move items from elsewhere in the playlist
1077 
1078         for(auto &listViewItem : items) {
1079             auto oldItem = takeTopLevelItem(indexOfTopLevelItem(listViewItem));
1080             insertTopLevelItem(++insertIndex, oldItem);
1081         }
1082     }
1083     else
1084         addFilesFromMimeData(e->mimeData(), item);
1085 
1086     m_blockDataChanged = false;
1087 
1088     playlistItemsChanged();
1089     emit signalPlaylistItemsDropped(this);
1090     QTreeWidget::dropEvent(e);
1091 }
1092 
1093 void Playlist::showEvent(QShowEvent *e)
1094 {
1095     if(m_applySharedSettings) {
1096         SharedSettings::instance()->apply(this);
1097         m_applySharedSettings = false;
1098     }
1099 
1100     QTreeWidget::showEvent(e);
1101 }
1102 
1103 void Playlist::applySharedSettings()
1104 {
1105     m_applySharedSettings = true;
1106 }
1107 
1108 void Playlist::read(QDataStream &s)
1109 {
1110     s >> m_playlistName
1111       >> m_fileName;
1112 
1113     // m_fileName is probably empty.
1114     if(m_playlistName.isEmpty())
1115         throw BICStreamException();
1116 
1117     // Do not sort. Add the files in the order they were saved.
1118     setSortingEnabled(false);
1119 
1120     QStringList files;
1121     s >> files;
1122 
1123     QTreeWidgetItem *after = 0;
1124     const CollectionList *clInst = CollectionList::instance();
1125 
1126     m_blockDataChanged = true;
1127 
1128     for(const auto &file : qAsConst(files)) {
1129         if(file.isEmpty())
1130             throw BICStreamException();
1131 
1132         const auto &cItem = clInst->lookup(file);
1133         if(cItem) {
1134             // Reuse FileHandle so the playlist doesn't force TagLib to read it
1135             // from disk
1136             after = createItem(cItem->file(), after);
1137         }
1138         else {
1139             after = createItem(FileHandle(file), after);
1140         }
1141     }
1142 
1143     m_blockDataChanged = false;
1144 
1145     playlistItemsChanged();
1146     m_collection->setupPlaylist(this, "audio-midi");
1147 }
1148 
1149 void Playlist::paintEvent(QPaintEvent *pe)
1150 {
1151     // If there are columns that need to be updated, well, update them.
1152 
1153     if(!m_weightDirty.isEmpty() && !manualResize())
1154     {
1155         calculateColumnWeights();
1156         slotUpdateColumnWidths();
1157     }
1158 
1159     QTreeWidget::paintEvent(pe);
1160 }
1161 
1162 void Playlist::resizeEvent(QResizeEvent *re)
1163 {
1164     // If the width of the view has changed, manually update the column
1165     // widths.
1166 
1167     if(re->size().width() != re->oldSize().width() && !manualResize())
1168         slotUpdateColumnWidths();
1169 
1170     QTreeWidget::resizeEvent(re);
1171 }
1172 
1173 // Reimplemented to show a visual indication of which of the view's playlist
1174 // items is actually playing.
1175 void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const
1176 {
1177     PlaylistItem *item = static_cast<PlaylistItem *>(itemFromIndex(index));
1178     if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) {
1179         return QTreeWidget::drawRow(p, option, index);
1180     }
1181 
1182     // Seems that the view draws the background now so we have to do this
1183     // manually
1184     p->fillRect(option.rect, QPalette{}.midlight());
1185 
1186     QStyleOptionViewItem newOption {option};
1187     newOption.font.setBold(true);
1188 
1189     QTreeWidget::drawRow(p, newOption, index);
1190 }
1191 
1192 void Playlist::insertItem(QTreeWidgetItem *item)
1193 {
1194     QTreeWidget::insertTopLevelItem(0, item);
1195 }
1196 
1197 void Playlist::takeItem(QTreeWidgetItem *item)
1198 {
1199     int index = indexOfTopLevelItem(item);
1200     QTreeWidget::takeTopLevelItem(index);
1201 }
1202 
1203 PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after)
1204 {
1205     return createItem<PlaylistItem>(file, after);
1206 }
1207 
1208 void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after)
1209 {
1210     createItems<QVector, PlaylistItem, PlaylistItem>(siblings, after);
1211 }
1212 
1213 void Playlist::addFiles(const QStringList &files, PlaylistItem *after)
1214 {
1215     if(Q_UNLIKELY(files.isEmpty())) {
1216         return;
1217     }
1218 
1219     m_blockDataChanged = true;
1220     setEnabled(false);
1221 
1222     QVector<QFuture<void>> pendingFutures;
1223     for(const auto &file : files) {
1224         // some files added here will launch threads that we must wait until
1225         // they're done to cleanup
1226         auto pendingResult = addUntypedFile(file, after);
1227         if(!pendingResult.isFinished()) {
1228             pendingFutures.push_back(pendingResult);
1229             ++m_itemsLoading;
1230         }
1231     }
1232 
1233     // It's possible for no async threads to be launched, and also possible
1234     // for this function to be called while there were other threads in flight
1235     if(pendingFutures.isEmpty() && m_itemsLoading == 0) {
1236         cleanupAfterAllFileLoadsCompleted();
1237         return;
1238     }
1239 
1240     // Build handlers for all the still-active loaders on the heap and then
1241     // return to the event loop.
1242     for(const auto &future : qAsConst(pendingFutures)) {
1243         auto loadWatcher = new QFutureWatcher<void>(this);
1244         loadWatcher->setFuture(future);
1245 
1246         connect(loadWatcher, &QFutureWatcher<void>::finished, this, [=]() {
1247                 if(--m_itemsLoading == 0) {
1248                     cleanupAfterAllFileLoadsCompleted();
1249                 }
1250 
1251                 loadWatcher->deleteLater();
1252             });
1253     }
1254 }
1255 
1256 void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id)
1257 {
1258     QSet<QPair<QString, QString>> albums;
1259     bool setAlbumCovers = items.count() == 1;
1260 
1261     for(const auto &item : items) {
1262         const QString artist = item->file().tag()->artist();
1263         const QString album = item->file().tag()->album();
1264 
1265         albums.insert(qMakePair(artist, album));
1266 
1267         item->file().coverInfo()->setCoverId(id);
1268         if(setAlbumCovers)
1269             item->file().coverInfo()->applyCoverToWholeAlbum(true);
1270     }
1271 
1272     for(const auto &albumPair : qAsConst(albums)) {
1273         refreshAlbum(albumPair.first, albumPair.second);
1274     }
1275 }
1276 
1277 void Playlist::updatePlaying() const
1278 {
1279     const auto playingItems = PlaylistItem::playingItems();
1280     for(const auto &item : playingItems) {
1281         item->treeWidget()->viewport()->update();
1282     }
1283 }
1284 
1285 void Playlist::refreshAlbum(const QString &artist, const QString &album)
1286 {
1287     ColumnList columns;
1288     columns.append(PlaylistItem::ArtistColumn);
1289     PlaylistSearch::Component artistComponent(artist, false, columns,
1290                                               PlaylistSearch::Component::Exact);
1291 
1292     columns.clear();
1293     columns.append(PlaylistItem::AlbumColumn);
1294     PlaylistSearch::Component albumComponent(album, false, columns,
1295                                              PlaylistSearch::Component::Exact);
1296 
1297     PlaylistSearch::ComponentList components;
1298     components.append(artist);
1299     components.append(album);
1300 
1301     PlaylistList playlists;
1302     playlists.append(CollectionList::instance());
1303 
1304     PlaylistSearch search(playlists, components);
1305     const QModelIndexList matches = search.matchedItems();
1306 
1307     for(QModelIndex index: matches)
1308         static_cast<PlaylistItem*>(itemFromIndex(index))->refresh();
1309 }
1310 
1311 void Playlist::hideColumn(int c, bool updateSearch)
1312 {
1313     const auto headerActions = m_headerMenu->actions();
1314     for(auto &action : headerActions) {
1315         if(!action)
1316             continue;
1317 
1318         if(action->data().toInt() == c) {
1319             action->setChecked(false);
1320             break;
1321         }
1322     }
1323 
1324     if(isColumnHidden(c)) {
1325         return;
1326     }
1327 
1328     QTreeWidget::hideColumn(c);
1329 
1330     if(c == m_leftColumn) {
1331         updatePlaying();
1332         m_leftColumn = leftMostVisibleColumn();
1333     }
1334 
1335     if(!manualResize()) {
1336         slotUpdateColumnWidths();
1337         viewport()->update();
1338     }
1339 
1340     if(this != CollectionList::instance()) {
1341         CollectionList::instance()->hideColumn(c, false);
1342     }
1343 
1344     if(updateSearch) {
1345         redisplaySearch();
1346     }
1347 }
1348 
1349 void Playlist::showColumn(int c, bool updateSearch)
1350 {
1351     const auto headerActions = m_headerMenu->actions();
1352     for(auto &action : headerActions) {
1353         if(!action)
1354             continue;
1355 
1356         if(action->data().toInt() == c) {
1357             action->setChecked(true);
1358             break;
1359         }
1360     }
1361 
1362     if(!isColumnHidden(c)) {
1363         return;
1364     }
1365 
1366     QTreeWidget::showColumn(c);
1367 
1368     if(c == leftMostVisibleColumn()) {
1369         updatePlaying();
1370         m_leftColumn = leftMostVisibleColumn();
1371     }
1372 
1373     if(!manualResize()) {
1374         slotUpdateColumnWidths();
1375         viewport()->update();
1376     }
1377 
1378     if(this != CollectionList::instance()) {
1379         CollectionList::instance()->showColumn(c, false);
1380     }
1381 
1382     if(updateSearch) {
1383         redisplaySearch();
1384     }
1385 }
1386 
1387 void Playlist::sortByColumn(int column, Qt::SortOrder order)
1388 {
1389     setSortingEnabled(true);
1390     QTreeWidget::sortByColumn(column, order);
1391 }
1392 
1393 // This function is called during startup so it cannot rely on any virtual
1394 // functions that might be changed by a subclass (virtual functions relying on
1395 // superclasses are fine since the C++ runtime can statically dispatch those).
1396 void Playlist::slotInitialize(int numColumnsToReserve)
1397 {
1398     // Setup the columns in the list view. We set aside room for
1399     // subclass-specific extra columns (always added at the beginning, see
1400     // columnOffset()) and then supplement with columns that apply to every
1401     // playlist.
1402     const QStringList standardColHeaders = {
1403         i18n("Track Name"),
1404         i18n("Artist"),
1405         i18n("Album"),
1406         i18n("Cover"),
1407         i18nc("cd track number", "Track"),
1408         i18n("Genre"),
1409         i18n("Year"),
1410         i18n("Length"),
1411         i18n("Bitrate"),
1412         i18n("Comment"),
1413         i18n("File Name"),
1414         i18n("File Name (full path)"),
1415     };
1416 
1417     QStringList allColHeaders;
1418     allColHeaders.reserve(numColumnsToReserve + standardColHeaders.size());
1419     std::fill_n(std::back_inserter(allColHeaders), numColumnsToReserve, i18n("JuK"));
1420     std::copy  (standardColHeaders.cbegin(), standardColHeaders.cend(),
1421             std::back_inserter(allColHeaders));
1422 
1423     setHeaderLabels(allColHeaders);
1424     setAllColumnsShowFocus(true);
1425     setSelectionMode(QTreeWidget::ExtendedSelection);
1426     header()->setSortIndicatorShown(true);
1427 
1428     int numColumns = columnCount();
1429 
1430     m_columnFixedWidths.resize(numColumns);
1431     m_weightDirty.resize(numColumns);
1432     m_columnWeights.resize(numColumns);
1433 
1434     //////////////////////////////////////////////////
1435     // setup header RMB menu
1436     //////////////////////////////////////////////////
1437 
1438     QAction *showAction;
1439     const auto sharedSettings = SharedSettings::instance();
1440 
1441     for(int i = 0; i < numColumns; ++i) {
1442         showAction = new QAction(allColHeaders[i], m_headerMenu);
1443         showAction->setData(i);
1444         showAction->setCheckable(true);
1445         showAction->setChecked(sharedSettings->isColumnVisible(i));
1446         m_headerMenu->addAction(showAction);
1447 
1448         resizeColumnToContents(i);
1449     }
1450 
1451     connect(m_headerMenu, &QMenu::triggered,
1452             this, &Playlist::slotToggleColumnVisible);
1453 
1454     connect(this, &QWidget::customContextMenuRequested,
1455             this, &Playlist::slotShowRMBMenu);
1456 
1457     // Disabled for now because adding new items (File->Open) causes Qt to send
1458     // an itemChanged signal for unrelated playlist items which can cause the
1459     // inline editor done slot to mistakenly overwrite tags associated to
1460     // *other* playlist items. I haven't found a way to determine whether the
1461     // itemChanged signal is really coming from the inline editor so instead
1462     // users will need to use the tag editor. :(
1463     //  -- mpyne 2018-12-20
1464     //connect(this, &QTreeWidget::itemChanged,
1465     //        this, &Playlist::slotInlineEditDone);
1466 
1467     connect(action("resizeColumnsManually"), &KToggleAction::triggered,
1468             this, &Playlist::slotColumnResizeModeChanged);
1469 
1470     if(action<KToggleAction>("resizeColumnsManually")->isChecked()) {
1471         header()->setSectionResizeMode(QHeaderView::Interactive);
1472         setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1473     } else {
1474         header()->setSectionResizeMode(QHeaderView::Fixed);
1475         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1476     }
1477 
1478     viewport()->setAcceptDrops(true);
1479     setDropIndicatorShown(true);
1480     setDragEnabled(true);
1481 
1482     // TODO: Retain last-run's sort
1483     sortByColumn(1, Qt::AscendingOrder);
1484 
1485     m_disableColumnWidthUpdates = false;
1486 }
1487 
1488 void Playlist::setupItem(PlaylistItem *item)
1489 {
1490     item->setTrackId(g_trackID);
1491     g_trackID++;
1492 
1493     QModelIndex index = indexFromItem(item);
1494     if(!m_search->isEmpty())
1495         item->setHidden(!m_search->checkItem(&index));
1496 
1497     if(topLevelItemCount() <= 2 && !manualResize()) {
1498         slotWeightDirty();
1499         slotUpdateColumnWidths();
1500         viewport()->update();
1501     }
1502 }
1503 
1504 void Playlist::setDynamicListsFrozen(bool frozen)
1505 {
1506     m_collection->setDynamicListsFrozen(frozen);
1507 }
1508 
1509 CollectionListItem *Playlist::collectionListItem(const FileHandle &file)
1510 {
1511     CollectionListItem *item = CollectionList::instance()->lookup(file.absFilePath());
1512 
1513     if(!item) {
1514         if(!QFile::exists(file.absFilePath())) {
1515             qCCritical(JUK_LOG) << "File" << file.absFilePath() << "does not exist.";
1516             return nullptr;
1517         }
1518 
1519         item = CollectionList::instance()->createItem(file);
1520     }
1521 
1522     return item;
1523 }
1524 
1525 ////////////////////////////////////////////////////////////////////////////////
1526 // protected slots
1527 ////////////////////////////////////////////////////////////////////////////////
1528 
1529 void Playlist::slotPopulateBackMenu() const
1530 {
1531     if(!playingItem())
1532         return;
1533 
1534     QMenu *menu = action<KToolBarPopupAction>("back")->menu();
1535     menu->clear();
1536     m_backMenuItems.clear();
1537     m_backMenuItems.reserve(10);
1538 
1539     int count = 0;
1540     PlaylistItemList::ConstIterator it = m_history.constEnd();
1541 
1542     QAction *action;
1543 
1544     while(it != m_history.constBegin() && count < 10) {
1545         ++count;
1546         --it;
1547         action = new QAction((*it)->file().tag()->title(), menu);
1548         action->setData(count - 1);
1549         menu->addAction(action);
1550         m_backMenuItems << *it;
1551     }
1552 }
1553 
1554 void Playlist::slotPlayFromBackMenu(QAction *backAction)
1555 {
1556     int number = backAction->data().toInt();
1557 
1558     if(number >= m_backMenuItems.size())
1559         return;
1560 
1561     auto &nextItem = m_backMenuItems[number];
1562     beginPlayingItem(nextItem);
1563 }
1564 
1565 ////////////////////////////////////////////////////////////////////////////////
1566 // private members
1567 ////////////////////////////////////////////////////////////////////////////////
1568 
1569 void Playlist::setup(int numColumnsToReserve)
1570 {
1571     m_search = new PlaylistSearch(this);
1572 
1573     setAlternatingRowColors(true);
1574     setRootIsDecorated(false);
1575     setContextMenuPolicy(Qt::CustomContextMenu);
1576     setUniformRowHeights(true);
1577     setEditTriggers(QAbstractItemView::EditKeyPressed); // Don't edit on double-click
1578 
1579     connect(header(), &QHeaderView::sectionMoved,
1580             this,     &Playlist::slotColumnOrderChanged);
1581 
1582     connect(m_fetcher, &WebImageFetcher::signalCoverChanged,
1583             this,      &Playlist::slotCoverChanged);
1584 
1585     // Prevent list of selected items from changing while internet search is in
1586     // progress.
1587     connect(this,      &Playlist::itemSelectionChanged,
1588             m_fetcher, &WebImageFetcher::abortSearch);
1589 
1590     connect(this, &QTreeWidget::itemDoubleClicked,
1591             this, &Playlist::slotPlayCurrent);
1592 
1593     // Use a timer to soak up the multiple dataChanged signals we're going to get
1594     auto updateRequestor = new QTimer(this);
1595     updateRequestor->setSingleShot(true);
1596     updateRequestor->setInterval(10);
1597 
1598     connect(model(), &QAbstractItemModel::dataChanged,
1599             updateRequestor, qOverload<>(&QTimer::start));
1600     connect(updateRequestor, &QTimer::timeout,
1601             this, &Playlist::slotUpdateTime);
1602 
1603     // This apparently must be created very early in initialization for other
1604     // Playlist code requiring m_headerMenu.
1605     m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this);
1606     ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction);
1607 
1608     m_headerMenu = m_columnVisibleAction->menu();
1609 
1610     header()->installEventFilter(this);
1611 
1612     // TODO: Determine if other stuff in setup must happen before slotInitialize().
1613 
1614     // Explicitly call slotInitialize() so that the columns are added before
1615     // SharedSettings::apply() sets the visible and hidden ones.
1616     slotInitialize(numColumnsToReserve);
1617 }
1618 
1619 void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo)
1620 {
1621     QFile file(fileName);
1622     if(!file.open(QIODevice::ReadOnly))
1623         return;
1624 
1625     QTextStream stream(&file);
1626 
1627     // Turn off non-explicit sorting.
1628 
1629     setSortingEnabled(false);
1630 
1631     m_disableColumnWidthUpdates = true;
1632     m_blockDataChanged = true;
1633 
1634     PlaylistItem *after = nullptr;
1635 
1636     while(!stream.atEnd()) {
1637         QString itemName = stream.readLine().trimmed();
1638 
1639         QFileInfo item(itemName);
1640 
1641         if(item.isRelative())
1642             item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName));
1643 
1644         if(item.exists() && item.isFile() && item.isReadable() &&
1645            MediaFiles::isMediaFile(item.fileName()))
1646         {
1647             after = createItem(FileHandle(item), after);
1648         }
1649     }
1650 
1651     m_blockDataChanged = false;
1652     m_disableColumnWidthUpdates = false;
1653 
1654     file.close();
1655 
1656     playlistItemsChanged();
1657 }
1658 
1659 void Playlist::setPlaying(PlaylistItem *item, bool addToHistory)
1660 {
1661     auto wasPlayingItem = playingItem();
1662     if(wasPlayingItem == item)
1663         return;
1664 
1665     if(wasPlayingItem && addToHistory) {
1666         m_history.append(wasPlayingItem->collectionItem());
1667 
1668         const bool enableBack = !m_history.isEmpty();
1669         action<KToolBarPopupAction>("back")->menu()->setEnabled(enableBack);
1670     }
1671 
1672     if(wasPlayingItem) {
1673         // NB: will clear the whole list of playing items recursively
1674         wasPlayingItem->setPlaying(false);
1675     }
1676 
1677     if(item) {
1678         item->setPlaying(true);
1679 
1680         if(wasPlayingItem
1681             && m_search->isEmpty()
1682             && state() == QAbstractItemView::NoState
1683             && (!m_rmbMenu || m_rmbMenu->isHidden())
1684             )
1685         {
1686             scrollToItem(item, QAbstractItemView::PositionAtCenter);
1687         }
1688     }
1689 }
1690 
1691 bool Playlist::playing() const
1692 {
1693     return playingItem() && this == playingItem()->playlist();
1694 }
1695 
1696 int Playlist::leftMostVisibleColumn() const
1697 {
1698     int i = 0;
1699     while(i < PlaylistItem::lastColumn() && isColumnHidden(i))
1700         i++;
1701 
1702     return i < PlaylistItem::lastColumn() ? i : 0;
1703 }
1704 
1705 // used with slotShowRMBMenu
1706 void Playlist::createPlaylistRMBMenu()
1707 {
1708     using namespace IconSupport; // ""_icon
1709 
1710     m_rmbMenu = new QMenu(this);
1711 
1712     m_rmbMenu->addAction("go-jump-today"_icon, i18n("Add to Play Queue"),
1713             this, &Playlist::slotAddToUpcoming);
1714     m_rmbMenu->addSeparator();
1715 
1716     if(!this->readOnly()) {
1717         m_rmbMenu->addAction(action("edit_cut"));
1718         m_rmbMenu->addAction(action("edit_copy"));
1719         m_rmbMenu->addAction(action("edit_paste"));
1720         m_rmbMenu->addSeparator();
1721         m_rmbMenu->addAction(action("removeFromPlaylist"));
1722     }
1723     else
1724         m_rmbMenu->addAction(action("edit_copy"));
1725 
1726     m_rmbEdit = m_rmbMenu->addAction(i18n("Edit"));
1727 
1728     m_rmbMenu->addAction(action("refresh"));
1729     m_rmbMenu->addAction(action("openItemDir"));
1730     m_rmbMenu->addAction(action("removeItem"));
1731 
1732     m_rmbMenu->addSeparator();
1733 
1734     m_rmbMenu->addAction(action("guessTag"));
1735     m_rmbMenu->addAction(action("renameFile"));
1736 
1737     m_rmbMenu->addAction(action("coverManager"));
1738 
1739     m_rmbMenu->addSeparator();
1740 
1741     m_rmbMenu->addAction("folder-new"_icon,
1742         i18n("Create Playlist From Selected Items..."),
1743         this, &Playlist::slotCreateGroup);
1744 }
1745 
1746 PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags)
1747 {
1748     PlaylistItemList list;
1749 
1750     for(QTreeWidgetItemIterator it(this, flags); *it; ++it)
1751         list.append(static_cast<PlaylistItem *>(*it));
1752 
1753     return list;
1754 }
1755 
1756 void Playlist::calculateColumnWeights()
1757 {
1758     if(m_disableColumnWidthUpdates)
1759         return;
1760 
1761     const PlaylistItemList itemList = items();
1762 
1763     QVector<double> averageWidth(columnCount());
1764     double itemCount = itemList.size();
1765 
1766     // Here we're not using a real average, but averaging the squares of the
1767     // column widths and then using the square root of that value.  This gives
1768     // a nice weighting to the longer columns without doing something arbitrary
1769     // like adding a fixed amount of padding.
1770 
1771     for(const auto &item : itemList) {
1772         const auto cachedWidth = item->cachedWidths();
1773 
1774         // Extra columns start at 0, but those weights aren't shared with all
1775         // items.
1776         for(int i = 0; i < columnOffset(); ++i) {
1777             const auto width = columnWidth(i);
1778             averageWidth[i] += width * width / itemCount;
1779         }
1780 
1781         const auto offset = columnOffset();
1782         for(int column = offset; column < columnCount(); ++column) {
1783             const auto width = cachedWidth[column - offset];
1784             averageWidth[column] += width * width / itemCount;
1785         }
1786     }
1787 
1788     if(m_columnWeights.isEmpty())
1789         m_columnWeights.fill(-1, columnCount());
1790 
1791     for(const auto column : qAsConst(m_weightDirty)) {
1792         m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5);
1793     }
1794 
1795     m_weightDirty.clear();
1796 }
1797 
1798 void Playlist::addPlaylistFile(const QString &m3uFile)
1799 {
1800     if(!m_collection->containsPlaylistFile(m3uFile)) {
1801         new Playlist(m_collection, QFileInfo(m3uFile));
1802     }
1803 }
1804 
1805 QFuture<void> Playlist::addFilesFromDirectory(const QString &dirPath)
1806 {
1807     auto loader = new DirectoryLoader(dirPath);
1808 
1809     connect(loader, &DirectoryLoader::loadedPlaylist, this,
1810         [this](const QString &m3uFile) {
1811             addPlaylistFile(m3uFile);
1812         }
1813     );
1814     connect(loader, &DirectoryLoader::loadedFiles, this,
1815         [this](const FileHandleList &newFiles) {
1816             for(const auto &newFile : newFiles) {
1817                 createItem(newFile);
1818             }
1819         }
1820     );
1821 
1822     auto future = QtConcurrent::run(loader, &DirectoryLoader::startLoading);
1823     auto loadWatcher = new QFutureWatcher<void>(this);
1824     connect(loadWatcher, &QFutureWatcher<void>::finished, this, [=]() {
1825             loader->deleteLater();
1826             loadWatcher->deleteLater();
1827         });
1828 
1829     return future;
1830 }
1831 
1832 // Returns a future since some codepaths will result in an async operation.
1833 QFuture<void> Playlist::addUntypedFile(const QString &file, PlaylistItem *after)
1834 {
1835     if(hasItem(file) && !m_allowDuplicates)
1836         return {};
1837 
1838     const QFileInfo fileInfo(file);
1839     const QString canonicalPath = fileInfo.canonicalFilePath();
1840 
1841     if(fileInfo.isFile() && fileInfo.isReadable() &&
1842         MediaFiles::isMediaFile(file))
1843     {
1844         FileHandle f(fileInfo);
1845         f.tag();
1846         createItem(f, after);
1847         return {};
1848     }
1849 
1850     if(MediaFiles::isPlaylistFile(file)) {
1851         addPlaylistFile(canonicalPath);
1852         return {};
1853     }
1854 
1855     if(fileInfo.isDir()) {
1856         const auto blockedPaths = m_collection->excludedFolders();
1857         if(std::none_of(blockedPaths.begin(), blockedPaths.end(),
1858             [canonicalPath](const auto &dir) {
1859                 return canonicalPath.startsWith(dir);
1860             }))
1861         {
1862             return addFilesFromDirectory(canonicalPath);
1863         }
1864     }
1865 
1866     return {};
1867 }
1868 
1869 // Called directly or after a threaded directory load has completed, managed by
1870 // m_itemsLoading
1871 void Playlist::cleanupAfterAllFileLoadsCompleted()
1872 {
1873     m_blockDataChanged = false;
1874     setEnabled(true);
1875 
1876     // Even if doing a manual column weights we'll generally start off with
1877     // incorrect column sizes so at least figure out a reasonable column size
1878     // and let user adjust from there.
1879     if(manualResize()) {
1880         auto manualResizeAction = action<KToggleAction>("resizeColumnsManually");
1881         auto wasChecked = manualResizeAction->isChecked();
1882 
1883         manualResizeAction->setChecked(false);
1884         calculateColumnWeights();
1885         slotUpdateColumnWidths();
1886         manualResizeAction->setChecked(wasChecked);
1887     }
1888 
1889     playlistItemsChanged();
1890 }
1891 
1892 ////////////////////////////////////////////////////////////////////////////////
1893 // private slots
1894 ////////////////////////////////////////////////////////////////////////////////
1895 
1896 void Playlist::slotUpdateColumnWidths()
1897 {
1898     if(m_disableColumnWidthUpdates || manualResize())
1899         return;
1900 
1901     // Make sure that the column weights have been initialized before trying to
1902     // update the columns.
1903 
1904     QVector<int> visibleColumns;
1905     for(int i = 0; i < columnCount(); i++) {
1906         if(!isColumnHidden(i))
1907             visibleColumns.append(i);
1908     }
1909 
1910     if(visibleColumns.isEmpty()) {
1911         return;
1912     }
1913 
1914     // convenience handler for deprecated text metrics
1915     const auto textWidth = [](const QFontMetrics &fm, const QString &text) {
1916         return fm.horizontalAdvance(text);
1917     };
1918 
1919     // No item content to auto-fit around, use the headers for now
1920     if(count() == 0) {
1921         for(int column : qAsConst(visibleColumns)) {
1922             setColumnWidth(column, textWidth(header()->fontMetrics(),headerItem()->text(column)) + 10);
1923         }
1924 
1925         return;
1926     }
1927 
1928     if(m_columnWeights.isEmpty())
1929         return;
1930 
1931     // First build a list of minimum widths based on the strings in the listview
1932     // header.  We won't let the width of the column go below this width.
1933 
1934     QVector<int> minimumWidth(columnCount(), 0);
1935     int minimumWidthTotal = 0;
1936 
1937     // Also build a list of either the minimum *or* the fixed width -- whichever is
1938     // greater.
1939 
1940     QVector<int> minimumFixedWidth(columnCount(), 0);
1941     int minimumFixedWidthTotal = 0;
1942 
1943     for(int column : qAsConst(visibleColumns)) {
1944         minimumWidth[column] = textWidth(header()->fontMetrics(), headerItem()->text(column)) + 10;
1945         minimumWidthTotal += minimumWidth[column];
1946 
1947         minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]);
1948         minimumFixedWidthTotal += minimumFixedWidth[column];
1949     }
1950 
1951     // Make sure that the width won't get any smaller than this.  We have to
1952     // account for the scrollbar as well.  Since this method is called from the
1953     // resize event this will set a pretty hard toLower bound on the size.
1954 
1955     setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width());
1956 
1957     // If we've got enough room for the fixed widths (larger than the minimum
1958     // widths) then instead use those for our "minimum widths".
1959 
1960     if(minimumFixedWidthTotal < viewport()->width()) {
1961         minimumWidth = minimumFixedWidth;
1962         minimumWidthTotal = minimumFixedWidthTotal;
1963     }
1964 
1965     // We've got a list of columns "weights" based on some statistics gathered
1966     // about the widths of the items in that column.  We need to find the total
1967     // useful weight to use as a divisor for each column's weight.
1968 
1969     double totalWeight = 0;
1970     for(int column : qAsConst(visibleColumns)) {
1971         totalWeight += m_columnWeights[column];
1972     }
1973 
1974     // This can happen during startup, before we have the tracks loaded.
1975     if(qFuzzyIsNull(totalWeight)) {
1976         return;
1977     }
1978 
1979     // Computed a "weighted width" for each visible column.  This would be the
1980     // width if we didn't have to handle the cases of minimum and maximum widths.
1981 
1982     QVector<int> weightedWidth(columnCount(), 0);
1983     for(int column : qAsConst(visibleColumns)) {
1984         weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5);
1985     }
1986 
1987     // The "extra" width for each column.  This is the weighted width less the
1988     // minimum width or zero if the minimum width is greater than the weighted
1989     // width.
1990 
1991     QVector<int> extraWidth(columnCount(), 0);
1992 
1993     // This is used as an indicator if we have any columns where the weighted
1994     // width is less than the minimum width.  If this is false then we can
1995     // just use the weighted width with no problems, otherwise we have to
1996     // "readjust" the widths.
1997 
1998     bool readjust = false;
1999 
2000     // If we have columns where the weighted width is less than the minimum width
2001     // we need to steal that space from somewhere.  The amount that we need to
2002     // steal is the "neededWidth".
2003 
2004     int neededWidth = 0;
2005 
2006     // While we're on the topic of stealing -- we have to have somewhere to steal
2007     // from.  availableWidth is the sum of the amount of space beyond the minimum
2008     // width that each column has been allocated -- the sum of the values of
2009     // extraWidth[].
2010 
2011     int availableWidth = 0;
2012 
2013     // Fill in the values discussed above.
2014 
2015     for(int column : qAsConst(visibleColumns)) {
2016         if(weightedWidth[column] < minimumWidth[column]) {
2017             readjust = true;
2018             extraWidth[column] = 0;
2019             neededWidth += minimumWidth[column] - weightedWidth[column];
2020         }
2021         else {
2022             extraWidth[column] = weightedWidth[column] - minimumWidth[column];
2023             availableWidth += extraWidth[column];
2024         }
2025     }
2026 
2027     // The adjustmentRatio is the amount of the "extraWidth[]" that columns will
2028     // actually be given.
2029 
2030     double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth);
2031 
2032     // This will be the sum of the total space that we actually use.  Because of
2033     // rounding error this won't be the exact available width.
2034 
2035     int usedWidth = 0;
2036 
2037     // Now set the actual column widths.  If the weighted widths are all greater
2038     // than the minimum widths, just use those, otherwise use the "readjusted
2039     // weighted width".
2040 
2041     for(int column : qAsConst(visibleColumns)) {
2042         int width;
2043         if(readjust) {
2044             int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5);
2045             width = minimumWidth[column] + adjustedExtraWidth;
2046         }
2047         else
2048             width = weightedWidth[column];
2049 
2050         setColumnWidth(column, width);
2051         usedWidth += width;
2052     }
2053 
2054     // Fill the remaining gap for a clean fit into the available space.
2055 
2056     int remainingWidth = viewport()->width() - usedWidth;
2057     setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth);
2058 
2059     m_widthsDirty = false;
2060 }
2061 
2062 void Playlist::slotAddToUpcoming()
2063 {
2064     m_collection->setUpcomingPlaylistEnabled(true);
2065     m_collection->upcomingPlaylist()->appendItems(selectedItems());
2066 }
2067 
2068 void Playlist::slotShowRMBMenu(const QPoint &point)
2069 {
2070     QTreeWidgetItem *item = itemAt(point);
2071     int column = columnAt(point.x());
2072     if(!item)
2073         return;
2074 
2075     // Create the RMB menu on demand.
2076 
2077     if(!m_rmbMenu) {
2078         createPlaylistRMBMenu();
2079     }
2080 
2081     // Ignore any columns added by subclasses.
2082 
2083     const int adjColumn = column - columnOffset();
2084 
2085     bool showEdit =
2086         (adjColumn == PlaylistItem::TrackColumn) ||
2087         (adjColumn == PlaylistItem::ArtistColumn) ||
2088         (adjColumn == PlaylistItem::AlbumColumn) ||
2089         (adjColumn == PlaylistItem::TrackNumberColumn) ||
2090         (adjColumn == PlaylistItem::GenreColumn) ||
2091         (adjColumn == PlaylistItem::YearColumn);
2092 
2093     if(showEdit) {
2094         m_rmbEdit->setText(i18n("Edit '%1'", item->text(column)));
2095 
2096         m_rmbEdit->disconnect(this);
2097         connect(m_rmbEdit, &QAction::triggered, this, [this, item, column]() {
2098             this->editItem(item, column);
2099         });
2100     }
2101 
2102     m_rmbEdit->setVisible(showEdit);
2103 
2104     // Disable edit menu if only one file is selected, and it's read-only
2105 
2106     FileHandle file = static_cast<PlaylistItem*>(item)->file();
2107 
2108     m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1);
2109 
2110     // View cover is based on if there is a cover to see.  We should only have
2111     // the remove cover option if the cover is in our database (and not directly
2112     // embedded in the file, for instance).
2113 
2114     action("viewCover")->setEnabled(file.coverInfo()->hasCover());
2115     action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch);
2116 
2117     m_rmbMenu->popup(mapToGlobal(point));
2118 }
2119 
2120 bool Playlist::editTag(PlaylistItem *item, const QString &text, int column)
2121 {
2122     Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag());
2123 
2124     switch(column - columnOffset())
2125     {
2126     case PlaylistItem::TrackColumn:
2127         newTag->setTitle(text);
2128         break;
2129     case PlaylistItem::ArtistColumn:
2130         newTag->setArtist(text);
2131         break;
2132     case PlaylistItem::AlbumColumn:
2133         newTag->setAlbum(text);
2134         break;
2135     case PlaylistItem::TrackNumberColumn:
2136     {
2137         bool ok;
2138         int value = text.toInt(&ok);
2139         if(ok)
2140             newTag->setTrack(value);
2141         break;
2142     }
2143     case PlaylistItem::GenreColumn:
2144         newTag->setGenre(text);
2145         break;
2146     case PlaylistItem::YearColumn:
2147     {
2148         bool ok;
2149         int value = text.toInt(&ok);
2150         if(ok)
2151             newTag->setYear(value);
2152         break;
2153     }
2154     }
2155 
2156     TagTransactionManager::instance()->changeTagOnItem(item, newTag);
2157     return true;
2158 }
2159 
2160 void Playlist::slotInlineEditDone(QTreeWidgetItem *item, int column)
2161 {
2162     // The column we get is as passed from QTreeWidget so it does not need
2163     // adjustment to get the right text from the QTreeWidgetItem
2164 
2165     QString text = item->text(column);
2166     const PlaylistItemList l = selectedItems();
2167 
2168     // See if any of the files have a tag different from the input.
2169 
2170     const int adjColumn = column - columnOffset();
2171     bool changed = std::any_of(l.cbegin(), l.cend(),
2172         [text, adjColumn] (const PlaylistItem *item) { return item->text(adjColumn) != text; }
2173         );
2174 
2175     if(!changed ||
2176        (l.count() > 1 && KMessageBox::warningContinueCancel(
2177            0,
2178            i18n("This will edit multiple files. Are you sure?"),
2179            QString(),
2180            KGuiItem(i18n("Edit")),
2181            KStandardGuiItem::cancel(),
2182            "DontWarnMultipleTags") == KMessageBox::Cancel))
2183     {
2184         return;
2185     }
2186 
2187     for(auto &selItem : l) {
2188         editTag(selItem, text, column);
2189     }
2190 
2191     TagTransactionManager::instance()->commit();
2192 
2193     CollectionList::instance()->playlistItemsChanged();
2194     playlistItemsChanged();
2195 }
2196 
2197 void Playlist::slotColumnOrderChanged(int, int from, int to)
2198 {
2199     if(from == 0 || to == 0) {
2200         updatePlaying();
2201         m_leftColumn = header()->sectionPosition(0);
2202     }
2203 
2204     SharedSettings::instance()->setColumnOrder(this);
2205 }
2206 
2207 void Playlist::slotToggleColumnVisible(QAction *action)
2208 {
2209     int column = action->data().toInt();
2210 
2211     if(isColumnHidden(column)) {
2212         int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset();
2213         int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset();
2214 
2215         if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) {
2216             hideColumn(fullPathColumn, false);
2217             SharedSettings::instance()->toggleColumnVisible(fullPathColumn);
2218         }
2219         if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) {
2220             hideColumn(fileNameColumn, false);
2221             SharedSettings::instance()->toggleColumnVisible(fileNameColumn);
2222         }
2223     }
2224 
2225     if(!isColumnHidden(column))
2226         hideColumn(column);
2227     else
2228         showColumn(column);
2229 
2230     if(column >= columnOffset()) {
2231         SharedSettings::instance()->toggleColumnVisible(column - columnOffset());
2232     }
2233 }
2234 
2235 void Playlist::slotCreateGroup()
2236 {
2237     QString name = m_collection->playlistNameDialog(i18n("Create New Playlist"));
2238 
2239     if(!name.isEmpty())
2240         new Playlist(m_collection, selectedItems(), name);
2241 }
2242 
2243 void Playlist::notifyUserColumnWidthModeChanged()
2244 {
2245     KMessageBox::information(this,
2246                              i18n("Manual column widths have been enabled. You can "
2247                                   "switch back to automatic column sizes in the view "
2248                                   "menu."),
2249                              i18n("Manual Column Widths Enabled"),
2250                              "ShowManualColumnWidthInformation");
2251 }
2252 
2253 void Playlist::columnResized(int column, int, int newSize)
2254 {
2255     m_widthsDirty = true;
2256     m_columnFixedWidths[column] = newSize;
2257 }
2258 
2259 void Playlist::slotPlayCurrent()
2260 {
2261     QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected);
2262     PlaylistItem *next = static_cast<PlaylistItem *>(*it);
2263     beginPlayingItem(next);
2264     refillRandomList();
2265 }
2266 
2267 void Playlist::slotUpdateTime()
2268 {
2269     int newTime = 0;
2270     QTreeWidgetItemIterator it(this);
2271     while(*it) {
2272         const auto item = static_cast<const PlaylistItem*>(*it);
2273         ++it;
2274 
2275         newTime += item->file().tag()->seconds();
2276     }
2277 
2278     m_time = newTime;
2279 }
2280 
2281 ////////////////////////////////////////////////////////////////////////////////
2282 // helper functions
2283 ////////////////////////////////////////////////////////////////////////////////
2284 
2285 QDataStream &operator<<(QDataStream &s, const Playlist &p)
2286 {
2287     s << p.name();
2288     s << p.fileName();
2289     s << p.files();
2290 
2291     return s;
2292 }
2293 
2294 QDataStream &operator>>(QDataStream &s, Playlist &p)
2295 {
2296     p.read(s);
2297     return s;
2298 }
2299 
2300 bool processEvents()
2301 {
2302     static QElapsedTimer time;
2303 
2304     if(time.elapsed() > 100) {
2305         time.restart();
2306         qApp->processEvents();
2307         return true;
2308     }
2309     return false;
2310 }
2311 
2312 // vim: set et sw=4 tw=0 sta: