/* PlaylistItemModel.cpp */
/* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
*
* This file is part of sayonara player
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
/*
* PlaylistItemModel.cpp
*
* Created on: Apr 8, 2011
* Author: Michael Lugmair (Lucio Carreras)
*/
#include "PlaylistModel.h"
#include "Components/Playlist/Playlist.h"
#include "Components/Playlist/ExternTracksPlaylistGenerator.h"
#include "Components/Tagging/UserTaggingOperations.h"
#include "Components/Covers/CoverLocation.h"
#include "Interfaces/PlaylistInterface.h"
#include "Database/Connector.h"
#include "Database/CoverConnector.h"
#include "Gui/Utils/MimeData/CustomMimeData.h"
#include "Gui/Utils/Icons.h"
#include "Utils/Utils.h"
#include "Utils/Algorithm.h"
#include "Utils/FileUtils.h"
#include "Utils/Set.h"
#include "Utils/globals.h"
#include "Utils/Language/Language.h"
#include "Utils/Library/SearchMode.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/Settings/Settings.h"
#include
#include
#include
#include
namespace Algorithm = Util::Algorithm;
using Playlist::Model;
namespace
{
constexpr const auto AlbumSearchPrefix = '%';
constexpr const auto ArtistSearchPrefix = '$';
constexpr const auto JumpPrefix = ':';
static QString convertEntryLook(const QString& entryLook, const MetaData& md)
{
auto ret = entryLook;
ret.replace(QStringLiteral("*"), QChar(Model::Bold));
ret.replace(QStringLiteral("'"), QChar(Model::Italic));
ret.replace(QStringLiteral("%title%"), md.title());
ret.replace(QStringLiteral("%nr%"), QString::number(md.trackNumber()));
ret.replace(QStringLiteral("%artist%"), md.artist());
ret.replace(QStringLiteral("%album%"), md.album());
return ret;
}
}
enum class PlaylistSearchMode
{
Artist,
Album,
Title,
Jump
};
struct Model::Private
{
QHash coverLookupMap;
int oldRowCount;
int dragIndex;
int rowHeight;
PlaylistPtr playlist;
Tagging::UserOperations* uto = nullptr;
PlaylistCreator* playlistCreator;
Private(PlaylistCreator* playlistCreator, PlaylistPtr playlistArg) :
oldRowCount(0),
dragIndex(-1),
rowHeight(20),
playlist(playlistArg),
playlistCreator {playlistCreator} {}
};
Model::Model(PlaylistCreator* playlistCreator, PlaylistPtr playlist, QObject* parent) :
SearchableTableModel(parent)
{
m = Pimpl::make(playlistCreator, playlist);
connect(m->playlist.get(), &Playlist::Playlist::sigItemsChanged, this, &Model::playlistChanged);
connect(m->playlist.get(), &Playlist::Playlist::sigTrackChanged, this, &Model::currentTrackChanged);
ListenSettingNoCall(Set::PL_EntryLook, Model::lookChanged);
playlistChanged(0);
}
Model::~Model() = default;
int Model::rowCount([[maybe_unused]] const QModelIndex& parent) const
{
return m->playlist->count();
}
int Model::columnCount([[maybe_unused]] const QModelIndex& parent) const
{
return int(ColumnName::NumColumns);
}
QVariant Model::data(const QModelIndex& index, int role) const
{
const auto row = index.row();
const auto col = index.column();
const auto isCurrentTrack = (row == m->playlist->currentTrackIndex());
if(!Util::between(row, m->playlist->count()))
{
return QVariant();
}
if(role == Qt::DisplayRole)
{
if(col == ColumnName::TrackNumber)
{
return (isCurrentTrack)
? QString()
: QString("%1.").arg(row + 1);
}
else if(col == ColumnName::Time)
{
auto durationMs = m->playlist->track(row).durationMs();
return (durationMs / 1000 <= 0)
? QVariant()
: Util::msToString(durationMs, QStringLiteral("$M:$S"));
}
return QVariant();
}
else if(role == Qt::TextAlignmentRole)
{
if(col != ColumnName::Description)
{
return QVariant(Qt::AlignRight | Qt::AlignVCenter);
}
}
else if(role == Qt::DecorationRole)
{
if(col == ColumnName::Cover)
{
const auto& track = m->playlist->track(row);
if(!m->coverLookupMap.contains(track.albumId()))
{
const auto height = m->rowHeight - 6;
const auto coverLocation = Cover::Location::coverLocation(track);
auto* coverDatabase = DB::Connector::instance()->coverConnector();
QPixmap cover;
const auto hash = coverLocation.hash();
coverDatabase->getCover(hash, cover);
if(cover.isNull())
{
cover = QPixmap(coverLocation.preferredPath());
}
if(!cover.isNull())
{
m->coverLookupMap[track.albumId()] =
cover.scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
}
return m->coverLookupMap[track.albumId()];
}
}
else if(role == Qt::SizeHintRole)
{
if(col == ColumnName::Cover)
{
const auto h = m->rowHeight - 4;
return QSize(h, h);
}
}
else if(role == Model::EntryLookRole)
{
if(col == ColumnName::Description)
{
return convertEntryLook(GetSetting(Set::PL_EntryLook), m->playlist->track(row));
}
}
else if(role == Model::DragIndexRole)
{
return (row == m->dragIndex);
}
else if(role == Model::RatingRole || role == Qt::EditRole)
{
if(col == ColumnName::Description)
{
const auto& track = m->playlist->track(row);
return (track.radioMode() == RadioMode::Off)
? QVariant::fromValue(metadata(row).rating())
: QVariant::fromValue(Rating::Last);
}
}
else if(role == Model::CurrentPlayingRole)
{
return isCurrentTrack;
}
return QVariant();
}
bool Model::setData(const QModelIndex& index, const QVariant& value, int role)
{
const auto validEdit = (role == Qt::EditRole) && index.isValid();
if(validEdit)
{
const auto row = index.row();
changeRating({row}, value.value());
}
return validEdit;
}
Qt::ItemFlags Model::flags(const QModelIndex& index) const
{
if(!index.isValid())
{
return (Qt::ItemIsEnabled | Qt::ItemIsDropEnabled);
}
const auto row = index.row();
if(!Util::between(row, m->playlist->count()) || metadata(row).isDisabled())
{
return Qt::NoItemFlags;
}
const auto itemFlags = QAbstractTableModel::flags(index);
return (index.column() == static_cast(ColumnName::Description))
? (itemFlags | Qt::ItemIsEditable)
: (itemFlags | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
}
void Model::clear()
{
m->playlist->clear();
}
void Model::removeTracks(const IndexSet& indexes)
{
m->playlist->removeTracks(indexes);
}
IndexSet Model::moveTracks(const IndexSet& indexes, int target_index)
{
return m->playlist->moveTracks(indexes, target_index);
}
IndexSet Model::moveTracksUp(const IndexSet& indexes)
{
const auto minRow = *(std::min_element(indexes.begin(), indexes.end()));
return (minRow <= 0)
? IndexSet()
: moveTracks(indexes, minRow - 1);
}
IndexSet Model::moveTracksDown(const IndexSet& indexes)
{
auto minMax = std::minmax_element(indexes.begin(), indexes.end());
int minRow = *(minMax.first);
int maxRow = *(minMax.second);
return (maxRow < rowCount() - 1)
? moveTracks(indexes, minRow + static_cast(indexes.size()) + 1)
: IndexSet();
}
IndexSet Model::copyTracks(const IndexSet& indexes, int target_index)
{
return m->playlist->copyTracks(indexes, target_index);
}
void Model::changeRating(const IndexSet& indexes, Rating rating)
{
MetaDataList tracks;
tracks.reserve(indexes.size());
for(auto idx : indexes)
{
auto track = m->playlist->track(idx);
if(rating != track.rating())
{
tracks << track;
track.setRating(rating);
m->playlist->replaceTrack(idx, track);
}
emit dataChanged(index(idx, 0), index(idx, int(ColumnName::Description)));
}
if(tracks.isEmpty())
{
return;
}
if(!m->uto)
{
m->uto = new Tagging::UserOperations(-1, this);
}
m->uto->setTrackRating(tracks, rating);
}
void Model::insertTracks(const MetaDataList& tracks, int row)
{
m->playlist->insertTracks(tracks, row);
}
void Model::insertTracks(const QStringList& files, int row)
{
auto* playlistGenerator = new ExternTracksPlaylistGenerator(m->playlistCreator, m->playlist);
connect(playlistGenerator, &ExternTracksPlaylistGenerator::sigFinished, playlistGenerator, &QObject::deleteLater);
playlistGenerator->insertPaths(files, row);
}
void Model::reverseTracks()
{
m->playlist->reverse();
}
int Model::currentTrack() const
{
return m->playlist->currentTrackIndex();
}
const MetaData& Model::metadata(int row) const
{
return m->playlist->track(row);
}
MetaDataList Model::metadata(const IndexSet& rows) const
{
MetaDataList tracks;
tracks.reserve(rows.size());
Util::Algorithm::transform(rows, tracks, [this](int row) {
return m->playlist->track(row);
});
return tracks;
}
QModelIndexList Model::searchResults(const QString& substr)
{
QModelIndexList ret;
auto pureSearchString = substr;
auto playlistSearchMode = PlaylistSearchMode::Title;
if(pureSearchString.startsWith(ArtistSearchPrefix))
{
playlistSearchMode = PlaylistSearchMode::Artist;
pureSearchString.remove(ArtistSearchPrefix);
}
else if(pureSearchString.startsWith(AlbumSearchPrefix))
{
playlistSearchMode = PlaylistSearchMode::Album;
pureSearchString.remove(AlbumSearchPrefix);
}
else if(pureSearchString.startsWith(JumpPrefix))
{
playlistSearchMode = PlaylistSearchMode::Jump;
pureSearchString.remove(JumpPrefix);
}
pureSearchString = pureSearchString.trimmed();
if(playlistSearchMode == PlaylistSearchMode::Jump)
{
bool ok;
const auto line = pureSearchString.toInt(&ok);
return (ok && line < rowCount())
? QModelIndexList {this->index(line, 0)}
: QModelIndexList {QModelIndex {}};
}
const auto rows = rowCount();
for(int i = 0; i < rows; i++)
{
const auto& track = m->playlist->track(i);
QString str;
switch(playlistSearchMode)
{
case PlaylistSearchMode::Artist:
str = track.artist();
break;
case PlaylistSearchMode::Album:
str = track.album();
break;
default:
str = track.title();
break;
}
str = Library::Utils::convertSearchstring(str, searchMode());
if(str.contains(pureSearchString))
{
return QModelIndexList {this->index(i, 0)};
}
}
return QModelIndexList {};
}
using ExtraTriggerMap = SearchableModelInterface::ExtraTriggerMap;
ExtraTriggerMap Model::getExtraTriggers()
{
ExtraTriggerMap map;
map.insert(ArtistSearchPrefix, Lang::get(Lang::Artist));
map.insert(AlbumSearchPrefix, Lang::get(Lang::Album));
map.insert(JumpPrefix, tr("Goto row"));
return map;
}
QMimeData* Model::mimeData(const QModelIndexList& indexes) const
{
if(indexes.isEmpty())
{
return nullptr;
}
Util::Set rowSet;
for(const auto& index : indexes)
{
rowSet << index.row();
}
auto rows = rowSet.toList();
Algorithm::sort(rows, [](int row1, int row2) {
return (row1 < row2);
});
MetaDataList tracks;
tracks.reserve(static_cast(rows.size()));
for(const auto row : Algorithm::AsConst(rows))
{
if(row < m->playlist->count())
{
tracks << m->playlist->track(row);
}
}
if(tracks.empty())
{
return nullptr;
}
auto* mimedata = new Gui::CustomMimeData(this);
mimedata->setMetadata(tracks);
mimedata->setPlaylistSourceIndex(m->playlist->index());
return mimedata;
}
bool Model::hasLocalMedia(const IndexSet& rows) const
{
const auto& tracks = m->playlist->tracks();
return Algorithm::contains(rows, [tracks](const auto row) {
return (!Util::File::isWWW(tracks[row].filepath()));
});
}
void Model::setDragIndex(int dragIndex)
{
if(Util::between(m->dragIndex, rowCount()))
{
emit dataChanged(index(m->dragIndex, 0), index(m->dragIndex, columnCount() - 1));
}
m->dragIndex = dragIndex;
if(Util::between(dragIndex, rowCount()))
{
emit dataChanged(index(dragIndex, 0), index(dragIndex, columnCount() - 1));
}
}
void Model::setRowHeight(int rowHeight)
{
if(m->rowHeight != rowHeight)
{
m->coverLookupMap.clear();
m->rowHeight = rowHeight;
}
}
void Model::refreshData()
{
m->playlist->enableAll();
}
void Model::lookChanged()
{
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
}
void Model::playlistChanged([[maybe_unused]] int playlistIndex)
{
if(m->oldRowCount > m->playlist->count())
{
beginRemoveRows(QModelIndex(), m->playlist->count(), m->oldRowCount - 1);
endRemoveRows();
}
else if(m->playlist->count() > m->oldRowCount)
{
beginInsertRows(QModelIndex(), m->oldRowCount, m->playlist->count() - 1);
endInsertRows();
}
if(m->playlist->count() == 0)
{
beginResetModel();
endResetModel();
}
m->oldRowCount = m->playlist->count();
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
emit sigDataReady();
}
void Model::currentTrackChanged(int oldIndex, int newIndex)
{
if(Util::between(oldIndex, m->playlist->count()))
{
emit dataChanged(index(oldIndex, 0), index(oldIndex, columnCount() - 1));
}
if(Util::between(oldIndex, m->playlist->count()))
{
emit dataChanged(index(newIndex, 0), index(newIndex, columnCount() - 1));
emit sigCurrentTrackChanged(newIndex);
}
}
void Playlist::Model::deleteTracks(const IndexSet& rows)
{
m->playlist->deleteTracks(rows);
}
void Playlist::Model::findTrack(int index)
{
m->playlist->findTrack(index);
}
int Playlist::Model::playlistIndex() const
{
return m->playlist->index();
}
void Playlist::Model::setBusy(bool b)
{
m->playlist->setBusy(b);
}