File indexing completed on 2022-08-04 15:34:48

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