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: