Compare commits

...
Author SHA1 Message Date
Jonah Brüchert cca7eb4c0c Add automatic model template
This is based on KDABs updatable model, to automatically handle changes
in the data efficiently.
Additionally, it uses my desert library, to automatically create role
names and to implement tha data method of the model.
2023-02-03 12:42:43 +01:00
10 changed files with 676 additions and 176 deletions

No files matched your search

+1
View File
@@ -43,6 +43,7 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddon
find_package(pybind11 REQUIRED)
find_package(Ytdlp REQUIRED RUNTIME)
find_package(YTMusicAPI 0.24.0 REQUIRED RUNTIME)
find_package(Desert REQUIRED)
ki18n_install(po)
+2
View File
@@ -33,6 +33,7 @@ add_executable(audiotube
errorhandler.cpp
playerutils.cpp
abstractytmusicmodel.cpp
autolistmodel.h
multiiterableview.h
library.cpp
resources.qrc
@@ -51,6 +52,7 @@ target_link_libraries(audiotube
KF5::CoreAddons
KF5::Crash
KF5::WindowSystem
Desert::desert
ytm
asyncdatabase
)
+91
View File
@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
//
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QDebug>
#include <Desert/Reflection>
#include "updateablemodel.h"
template <typename T>
using AutoListModelBase = UpdateableModel<QAbstractListModel, T>;
template <typename T>
concept IsComparable = requires(T v, T o)
{
{ v == o } -> std::same_as<bool>;
{ v < o } -> std::same_as<bool>;
};
template <typename T>
requires desert::IsDesertObject<T> && IsComparable<T>
class AutoListModel : public AutoListModelBase<T>
{
public:
AutoListModel(QObject *parent = nullptr) : AutoListModelBase<T>(parent) {
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
int i = Qt::UserRole + 1;
T mock_obj;
desert::iterate_struct(mock_obj, [&](auto attribute) {
roles.insert(i, attribute.attributeName().toUtf8());
i++;
});
return roles;
}
int rowCount(const QModelIndex &parent) const override {
return parent.isValid() ? 0 : m_data.size();
}
QVariant data(const QModelIndex &index, int role) const override {
int structIndex = role - Qt::UserRole - 1;
if (structIndex >= pfr::tuple_size_v<T>) {
qDebug() << "Tried to fetch a role that is not defined by this model. Role id:" << role;
return {};
}
return structAttributeAt<0, T>(m_data.at(index.row()), structIndex);
}
void updateData(std::vector<T> &&updatedData) {
// allow comparing items. Could also have used an operator< on Data
auto lessThan = [](const T &lhs, const T &rhs) { return lhs < rhs; };
// check if the item has changed data, but is otherwise the same object. Needs to return the actual changes made.
auto itemHasChanged = [](const T &lhs, const T &rhs) {
typename AutoListModelBase<T>::DataChanges changes;
if (lhs != rhs)
changes.changedColumns.append(0);
changes.changedRoles = QVector<int>{}; // TODO more specific updates
return changes;
};
// call the updateData method to trigger merging in the changes to the model
auto changes = AutoListModelBase<T>::updateData(updatedData.cbegin(), updatedData.cend(), m_data, lessThan, itemHasChanged);
// the method above returns some stats, so lets print them.
qDebug() << "changes in model: inserts:" << changes.inserts << " deletes:" << changes.removals
<< " updates:" << changes.updates;
}
private:
std::vector<T> m_data;
};
template <size_t i, typename T>
requires desert::IsDesertObject<T>
QVariant structAttributeAt(const T &object, size_t index) {
if constexpr (i >= pfr::tuple_size_v<T>) {
Q_UNREACHABLE();
} else if (i == index) {
return QVariant::fromValue(pfr::get<i>(object).value);
} else {
return structAttributeAt<i + 1>(object, index);
}
}
+8 -10
View File
@@ -59,8 +59,7 @@ Kirigami.ScrollablePage {
delegate: ColumnLayout {
id: delegateItem
required property string title
required property var artists
required property string artistsDisplayString
required property string artists
required property string videoId
required property int index
@@ -183,7 +182,7 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: 200
leftPadding: 5
color: Kirigami.Theme.disabledTextColor
text: delegateItem.artistsDisplayString
text: delegateItem.artists
elide: Text.ElideRight
}
}
@@ -192,8 +191,8 @@ Kirigami.ScrollablePage {
icon.name: "view-more-horizontal-symbolic"
onPressed: menu.openForSong(delegateItem.videoId,
delegateItem.title,
delegateItem.artists,
delegateItem.artistsDisplayString)
delegateItem.artists, // TODO
delegateItem.artists)
}
}
@@ -244,8 +243,7 @@ Kirigami.ScrollablePage {
delegate: ColumnLayout {
id: mpdelegateItem
required property string title
required property var artists
required property string artistsDisplayString
required property string artists
required property string videoId
Layout.fillWidth: false
@@ -365,7 +363,7 @@ Kirigami.ScrollablePage {
Layout.maximumWidth: 200
leftPadding: 5
color: Kirigami.Theme.disabledTextColor
text: mpdelegateItem.artistsDisplayString
text: mpdelegateItem.artists
elide: Text.ElideRight
}
}
@@ -374,8 +372,8 @@ Kirigami.ScrollablePage {
icon.name: "view-more-horizontal-symbolic"
onPressed: menu.openForSong(mpdelegateItem.videoId,
mpdelegateItem.title,
mpdelegateItem.artists,
mpdelegateItem.artistsDisplayString)
mpdelegateItem.artists, // TODO
mpdelegateItem.artists)
}
}
+1 -2
View File
@@ -36,8 +36,7 @@ Kirigami.ScrollablePage {
required property string title
required property string videoId
required property var artists
required property string artistsDisplayString
required property string artists
required property int index
onClicked: {
+6 -4
View File
@@ -49,7 +49,7 @@ Item {
pageStack.push("qrc:/SearchPage.qml", {
"searchQuery": text,
objectName: "searchPage"
})
})
} else {
wideScreen
? pageStack.push("qrc:/LibraryPage.qml")
@@ -299,6 +299,8 @@ Item {
filterRegularExpression: searchField.filterExpression
}
delegate: Kirigami.AbstractListItem {
required property string query
id: completionDelegate
highlighted: focus
Kirigami.Theme.colorSet: Kirigami.Theme.Window
@@ -314,7 +316,7 @@ Item {
color: Kirigami.Theme.disabledTextColor
}
Controls.Label{
text: model.display
text: query
Layout.fillWidth: true
}
Controls.ToolButton {
@@ -322,13 +324,13 @@ Item {
text: i18n("remove from search history")
display: Controls.AbstractButton.IconOnly
onClicked: {
Library.removeSearch(model.display)
Library.removeSearch(query)
}
}
}
onClicked: {
searchField.text = model.display
searchField.text = query
searchField.accepted()
}
}
+33 -106
View File
@@ -40,8 +40,7 @@ Library &Library::instance()
FavouritesModel *Library::favourites()
{
auto future = m_database->getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
return new FavouritesModel(std::move(future), this);
return new FavouritesModel(this);
}
void Library::addFavourite(const QString &videoId, const QString &title, const QString &artist, const QString &album)
@@ -66,7 +65,7 @@ FavouriteWatcher *Library::favouriteWatcher(const QString &videoId)
SearchHistoryModel *Library::searches()
{
return new SearchHistoryModel(m_database->getResults<SingleValue<QString>>("select distinct (search_query) from searches order by search_id desc"), this);
return new SearchHistoryModel(this);
}
void Library::addSearch(const QString &text)
@@ -81,8 +80,7 @@ void Library::removeSearch(const QString &text)
PlaybackHistoryModel *Library::playbackHistory()
{
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs");
return new PlaybackHistoryModel(std::move(future), this);
return new PlaybackHistoryModel(this, PlaybackHistoryModel::History);
}
void Library::addPlaybackHistoryItem(const QString &videoId, const QString &title, const QString &artist, const QString &album)
@@ -110,8 +108,7 @@ WasPlayedWatcher *Library::wasPlayedWatcher(const QString& videoId)
PlaybackHistoryModel *Library::mostPlayed()
{
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
return new PlaybackHistoryModel(std::move(future), this);
return new PlaybackHistoryModel(this, PlaybackHistoryModel::MostPlayed);
}
QNetworkAccessManager &Library::nam()
@@ -171,93 +168,35 @@ void ThumbnailSource::setVideoId(const QString &id) {
});
}
PlaybackHistoryModel::PlaybackHistoryModel(QFuture<std::vector<PlayedSong> > &&songs, QObject *parent)
: QAbstractListModel(parent)
PlaybackHistoryModel::PlaybackHistoryModel(Library *library, Type type)
: AutoListModel<PlayedSong>(library)
{
connectFuture(songs, this, [this](const auto songs) {
beginResetModel();
m_playedSongs = songs;
endResetModel();
});
}
QHash<int, QByteArray> PlaybackHistoryModel::roleNames() const {
return {
{Roles::VideoId, "videoId"},
{Roles::Title, "title"},
{Roles::Artists, "artists"},
{Roles::ArtistsDisplayString, "artistsDisplayString"},
{Roles::Plays, "plays"}
};
}
int PlaybackHistoryModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_playedSongs.size();
}
QVariant PlaybackHistoryModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Roles::VideoId:
return m_playedSongs.at(index.row()).videoId;
case Roles::Title:
return m_playedSongs.at(index.row()).title;
case Roles::Artists:
return QVariant::fromValue(std::vector<meta::Artist> {
{
m_playedSongs.at(index.row()).artist.toStdString(),
{}
auto update = [=, this]() {
auto future = [=, this]() {
if (type == MostPlayed) {
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
} else {
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs");
}
});
case Roles::ArtistsDisplayString:
return m_playedSongs.at(index.row()).artist;
case Roles::Plays:
return m_playedSongs.at(index.row()).plays;
}
}();
Q_UNREACHABLE();
connectFuture(future, this, &PlaybackHistoryModel::updateData);
};
update();
connect(library, &Library::playbackHistoryChanged, this, update);
}
FavouritesModel::FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent)
: QAbstractListModel(parent)
FavouritesModel::FavouritesModel(Library *library)
: AutoListModel<Song>(library)
{
connectFuture(songs, this, [this](const auto songs) {
beginResetModel();
m_favouriteSongs = songs;
endResetModel();
});
}
QHash<int, QByteArray> FavouritesModel::roleNames() const {
return {
{Roles::VideoId, "videoId"},
{Roles::Title, "title"},
{Roles::Artists, "artists"},
{Roles::ArtistsDisplayString, "artistsDisplayString"}
auto update = [=, this]() {
auto future = library->database().getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
connectFuture(future, this, &FavouritesModel::updateData);
};
}
int FavouritesModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_favouriteSongs.size();
}
QVariant FavouritesModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Roles::VideoId:
return m_favouriteSongs.at(index.row()).videoId;
case Roles::Title:
return m_favouriteSongs.at(index.row()).title;
case Roles::ArtistsDisplayString:
return m_favouriteSongs.at(index.row()).artist;
case Roles::Artists:
return QVariant::fromValue(std::vector<meta::Artist> {
{
m_favouriteSongs.at(index.row()).artist.toStdString(),
{}
}
});
}
Q_UNREACHABLE();
update();
connect(library, &Library::favouritesChanged, this, update);
}
FavouriteWatcher::FavouriteWatcher(Library *library, const QString &videoId)
@@ -279,27 +218,15 @@ bool FavouriteWatcher::isFavourite() const {
return m_isFavourite;
}
SearchHistoryModel::SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent)
: QAbstractListModel(parent)
SearchHistoryModel::SearchHistoryModel(Library *library)
: AutoListModel<SearchQuery>(library)
{
connectFuture(historyFuture, this, [this](const auto history) {
beginResetModel();
m_history = history;
endResetModel();
});
}
int SearchHistoryModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_history.size();
}
QVariant SearchHistoryModel::data(const QModelIndex &index, int role) const {
switch (role) {
case Qt::DisplayRole:
return m_history[index.row()].value;
}
Q_UNREACHABLE();
auto update = [=, this]() {
auto future = library->database().getResults<SearchQuery>("select distinct (search_query) from searches order by search_id desc");
connectFuture(future, this, &SearchHistoryModel::updateData);
};
update();
connect(library, &Library::searchesChanged, update);
}
WasPlayedWatcher::WasPlayedWatcher(Library* library, const QString& videoId)
+56 -51
View File
@@ -15,10 +15,22 @@
#include "asyncytmusic.h"
#include "asyncdatabase.h"
#include "autolistmodel.h"
using namespace desert;
class FavouriteWatcher;
class WasPlayedWatcher;
inline std::vector<meta::Artist> metaArtist(const QString &artist) {
return { meta::Artist { artist.toStdString(), {} } };
}
struct Song {
DESERT_OBJECT
int operator<=>(const Song &other) const = default;
using ColumnTypes = std::tuple<QString, QString, QString, QString>;
static Song fromSql(ColumnTypes tuple) {
@@ -26,34 +38,26 @@ struct Song {
return Song {videoId, title, artist, album};
}
QString videoId;
QString title;
QString artist;
QString album;
Attribute<u"videoId", QString> videoId;
Attribute<u"title", QString> title;
Attribute<u"artists", QString> artists;
Attribute<u"album", QString> album;
};
class FavouritesModel : public QAbstractListModel {
class Library;
class FavouritesModel : public AutoListModel<Song> {
Q_OBJECT
enum Roles {
VideoId = Qt::UserRole + 1,
Title,
Artists,
ArtistsDisplayString
};
public:
FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
std::vector<Song> m_favouriteSongs;
FavouritesModel(Library *library);
};
struct PlayedSong {
DESERT_OBJECT
int operator<=>(const PlayedSong &other) const = default;
using ColumnTypes = std::tuple<QString, int, QString, QString, QString>;
static PlayedSong fromSql(ColumnTypes tuple) {
@@ -61,55 +65,56 @@ struct PlayedSong {
return PlayedSong {videoId, title, artist, album, plays};
}
QString videoId;
QString title;
QString artist;
QString album;
int plays;
Attribute<u"videoId", QString> videoId;
Attribute<u"title", QString> title;
Attribute<u"artists", QString> artists;
Attribute<u"album", QString> album;
Attribute<u"album", int> plays;
};
class PlaybackHistoryModel : public QAbstractListModel {
class PlaybackHistoryModel : public AutoListModel<PlayedSong> {
Q_OBJECT
public:
enum Roles {
VideoId = Qt::UserRole + 1,
Title,
Artists,
ArtistsDisplayString,
Plays
enum Type {
MostPlayed,
History
};
Q_ENUM(Roles);
PlaybackHistoryModel(QFuture<std::vector<PlayedSong>> &&songs, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
std::vector<PlayedSong> m_playedSongs;
PlaybackHistoryModel(Library *library, Type type);
};
class SearchHistoryModel : public QAbstractListModel {
class SearchQuery {
DESERT_OBJECT
using ColumnTypes = std::tuple<QString>;
static SearchQuery fromSql(ColumnTypes tuple) {
auto [query] = tuple;
return { query };
}
int operator<=>(const SearchQuery &other) const = default;
Attribute<u"query", QString> query;
};
class SearchHistoryModel : public AutoListModel<SearchQuery> {
Q_OBJECT
public:
SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
SearchHistoryModel(Library *library);
private:
std::vector<SingleValue<QString>> m_history;
std::vector<SearchQuery> m_history;
};
class Library : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractListModel *favourites READ favourites NOTIFY favouritesChanged)
Q_PROPERTY(QAbstractListModel *searches READ searches NOTIFY searchesChanged)
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory NOTIFY playbackHistoryChanged)
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed NOTIFY playbackHistoryChanged)
Q_PROPERTY(QAbstractListModel *favourites READ favourites CONSTANT)
Q_PROPERTY(QAbstractListModel *searches READ searches CONSTANT)
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory CONSTANT)
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed CONSTANT)
public:
explicit Library(QObject *parent = nullptr);
+3 -3
View File
@@ -10,20 +10,20 @@
#include <QtGlobal>
template <typename Tuple, typename Func, std::size_t i>
inline constexpr void iterate_impl(Tuple &tup, Func fun)
inline constexpr void iterate_tuple_impl(Tuple &tup, Func fun)
{
if constexpr(i >= std::tuple_size_v<std::decay_t<decltype(tup)>>) {
return;
} else {
fun(std::get<i>(tup));
return iterate_impl<Tuple, Func, i + 1>(tup, fun);
return iterate_tuple_impl<Tuple, Func, i + 1>(tup, fun);
}
}
template <typename Tuple, typename Func>
inline constexpr void iterate_tuple(Tuple &tup, Func fun)
{
iterate_impl<Tuple, Func, 0>(tup, fun);
iterate_tuple_impl<Tuple, Func, 0>(tup, fun);
}
/// MultiIterableView is a view over multiple contigous memory arrays, like std::vector or std::array.
+475
View File
@@ -0,0 +1,475 @@
/****************************************************************************
** MIT License
**
** Copyright (C) 2018-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
** Author: André Somers <[email protected]>
**
** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox).
**
** Permission is hereby granted, free of charge, to any person obtaining a copy
** of this software and associated documentation files (the "Software"), to deal
** in the Software without restriction, including without limitation the rights
** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
** copies of the Software, ** and to permit persons to whom the Software is
** furnished to do so, subject to the following conditions:
**
** The above copyright notice and this permission notice (including the next paragraph)
** shall be included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
****************************************************************************/
#ifndef UPDATEABLEMODEL_H
#define UPDATEABLEMODEL_H
#include <QModelIndex>
#include <QSet>
#include <QVector>
#include <functional>
// concepts
#define ForwardIt typename
#define RandomIt typename
#define Container typename
#define BinaryPredicate typename
#define EventHandler typename
#define QAIM typename // QAbstractItemModel-derived class
// basic algorithm
// precondition: src and target are both ordered with respect to lessThan
template<ForwardIt FwdIt, Container TargetCollection, BinaryPredicate LessThan, BinaryPredicate HasChanged,
EventHandler OnChanged, EventHandler OnInsert, EventHandler OnRemove, EventHandler OnEqual>
void updateCollection(const FwdIt srcBegin, const FwdIt srcEnd, TargetCollection &target, LessThan lessThan,
HasChanged itemHasChanged, OnChanged onChanged, OnInsert onInsert, OnRemove onRemove,
OnEqual onEqual)
{
auto srcIt = srcBegin;
target.reserve(std::distance(srcBegin, srcEnd));
auto targetIt = std::begin(target);
while (srcIt != srcEnd)
{
if (targetIt == std::end(target))
{
// insert: src has still items left while target has no more items
// so, insert all remaining items from src into target
targetIt = onInsert(srcIt, srcEnd, targetIt);
// targetIt now points to the item after the item(s) just inserted
srcIt = srcEnd;
}
else if (lessThan(*srcIt, *targetIt))
{
// insert: src has one or more items that need to be inserted into target
auto srcInsertEnd = std::next(srcIt);
while (srcInsertEnd != srcEnd && lessThan(*srcInsertEnd, *targetIt))
{
srcInsertEnd++;
}
targetIt = onInsert(srcIt, srcInsertEnd, targetIt);
// targetIt now points to the item after the item(s) just inserted
srcIt = srcInsertEnd;
}
else if (lessThan(*targetIt, *srcIt))
{
// removal: target has items that are not in src (any more), so remove them
auto targetRemoveEnd = std::next(targetIt);
auto targetEnd = std::end(target);
while (targetRemoveEnd != targetEnd && lessThan(*targetRemoveEnd, *srcIt))
{
targetRemoveEnd++;
}
targetIt = onRemove(targetIt, targetRemoveEnd);
// targetIt now points to the item after the items just removed,
// the same item targetEnd was pointing to before the call
}
else
{
// same item, check for changes
auto changes = itemHasChanged(*srcIt, *targetIt);
if (changes)
{
onChanged(srcIt, targetIt, changes);
}
else
{
onEqual(srcIt, targetIt);
}
srcIt++;
targetIt++;
}
}
if (targetIt != std::end(target))
{
// target has some items left, but we're at the end of src, so remove the rest from target
onRemove(targetIt, std::end(target));
}
}
// helper functions
namespace
{
// helper function to create a QSet<T> out of any container<T>.
template<typename T>
auto setFromContainer(T container) -> QSet<typename T::value_type>
{
QSet<typename T::value_type> set;
set.reserve(container.size());
for (const auto value : container)
{
set.insert(value);
}
return set;
}
// helper for manual overload ranking
// highest rank is selected first
template<size_t Rank>
struct OverloadRanker;
template<>
struct OverloadRanker<0>
{
};
template<size_t Rank>
struct OverloadRanker : OverloadRanker<Rank - 1>
{
};
// insert range into container, 2 overloads (using SFINAE) with manual ranking in case of multiple matches
constexpr size_t BestInsertRangeOverloadRank = 1;
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
OverloadRanker<1>) // Best match
-> decltype(container.insert(targetInsertBefore, sourceBegin, sourceEnd), InsertIt())
{
// primary option: use insert with a whole range directly
InsertIt it =
std::next(container.insert(targetInsertBefore, sourceBegin, sourceEnd), std::distance(sourceBegin, sourceEnd));
return it;
}
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
OverloadRanker<0>) // Fallback
-> InsertIt
{
#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
// Check if, when using QVector in Qt , the value type is default constructable
static_assert(!std::is_same<QVector<typename C::value_type>, C>::value ||
std::is_default_constructible<typename C::value_type>::value,
"When using QVector as your container type, the value type must be default-contructible. Consider "
"using std::vector instead or use a more recent Qt version.");
#endif
// store the location and the length of the inserted items
const auto location = std::distance(std::begin(container), targetInsertBefore);
const auto originalCount = container.size();
// append the new items to the container
std::copy(sourceBegin, sourceEnd, std::back_inserter(container));
// move the items into the right position in the container
const auto b = std::begin(container);
targetInsertBefore = std::next(b, location);
const InsertIt originalEnd = std::next(b, originalCount);
return std::rotate(targetInsertBefore, originalEnd, std::end(container));
}
/**
* Inserts a range into the target container
* @returns an iterator pointing one item past the last item inserted
* @param container is the target container
* @param targetInsertBefore iterator to the item the new items should be inserted before
* @param sourceBegin iterator to the first item to insert into @arg container
* @param sourceEnd iterator to the item past the last item to insert into @arg container
*
* Implementation is done using SFINEA to select an efficient method for the type of
* container used.
*/
// "public API"
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
InsertIt insertRange(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd)
{
return insertRange_imp(container, targetInsertBefore, sourceBegin, sourceEnd,
OverloadRanker<BestInsertRangeOverloadRank>{});
}
// less than on data type
constexpr size_t BestDataLessThanOverloadRank = 1;
template<typename T>
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<1>) -> decltype(operator<(lhs, rhs), bool())
{
return operator<(lhs, rhs);
}
template<typename T>
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<0>) -> decltype(bool())
{
// fake implementation to stop the compiler from whining. User is probably providing his own via function pointer
Q_UNUSED(lhs);
Q_UNUSED(rhs);
return false;
}
template<typename T>
bool dataLessThan(const T &lhs, const T &rhs)
{
return dataLessThan_imp(lhs, rhs, OverloadRanker<BestDataLessThanOverloadRank>{});
}
}
// actual class to inherit from
template<QAIM BaseModel, typename DataType>
class UpdateableModel : public BaseModel
{
// can't use Q_OBJECT on templates
public:
explicit UpdateableModel(QObject *parent = nullptr)
: BaseModel(parent)
{
}
protected: // types
struct Operations
{
uint inserts;
uint removals;
uint updates;
};
struct DataChanges
{
operator bool() const { return !changedColumns.isEmpty(); }
QVector<int> changedColumns; // important: keep ordered!
QVector<int> changedRoles;
};
enum ChangeMergePolicy
{
AlwaysMergeNeighbouringRows,
MergeWhenColumnsMatch,
MergeWhenRolesMatch,
MergeOnPerfectMatch
};
using HasChangesFunction = std::function<DataChanges(const DataType & /*lhs*/, const DataType & /*rhs*/)>;
protected: // methods
void setChangeMergePolicy(ChangeMergePolicy policy) { m_changeMergePolicy = policy; }
ChangeMergePolicy changeMergePolicy() const { return m_changeMergePolicy; }
virtual bool lessThan(const DataType &lhs, const DataType &rhs) const { return dataLessThan(lhs, rhs); }
virtual DataChanges itemHasChanged(const DataType &lhs, const DataType &rhs) const
{
Q_UNUSED(lhs);
Q_UNUSED(rhs);
return {};
}
template<ForwardIt Iterator, Container DataContainer, BinaryPredicate LessThan>
Operations updateData(Iterator srcBegin, Iterator srcEnd, DataContainer &targetContainer, LessThan lessThan,
HasChangesFunction itemHasChanged)
{
Operations ops{0, 0, 0};
// events
auto onChanged = [this, &targetContainer, &ops](Iterator lhs, typename DataContainer::iterator rhs,
const DataChanges &changes) {
*rhs = *lhs;
int row = std::distance(targetContainer.begin(), rhs);
addChange(row, changes.changedColumns, changes.changedRoles);
ops.updates++;
};
auto onInsert = [this, &targetContainer, &ops](Iterator lhsBegin, Iterator lhsEnd,
typename DataContainer::iterator rhsInsertAt) {
flushCachedChanges();
int firstNewRow = std::distance(targetContainer.begin(), rhsInsertAt);
int lastNewRow = firstNewRow + std::distance(lhsBegin, lhsEnd) - 1;
beginInsertRows(firstNewRow, lastNewRow);
auto rangeEnd = insertRange(targetContainer, rhsInsertAt, lhsBegin, lhsEnd);
endInsertRows();
ops.inserts += lhsEnd - lhsBegin;
return rangeEnd;
};
auto onRemove = [this, &targetContainer, &ops](typename DataContainer::iterator rhsBegin,
typename DataContainer::iterator rhsEnd) {
flushCachedChanges();
const auto targetContainerBegin = std::begin(targetContainer);
int firstRemovedRow = std::distance(targetContainerBegin, rhsBegin);
int lastRemovedRow = std::distance(targetContainerBegin, rhsEnd) - 1;
beginRemoveRows(firstRemovedRow, lastRemovedRow);
auto newRhsEnd = targetContainer.erase(rhsBegin, rhsEnd);
endRemoveRows();
ops.removals += std::distance(rhsBegin, rhsEnd);
return newRhsEnd;
};
auto onEqual = [this](Iterator /*lhs*/, typename DataContainer::iterator /*rhs*/) { flushCachedChanges(); };
updateCollection(srcBegin, srcEnd, targetContainer, lessThan, itemHasChanged, onChanged, onInsert, onRemove,
onEqual);
flushCachedChanges();
return ops;
}
template<ForwardIt Iterator, Container Data>
Operations updateData(Iterator srcBegin, Iterator srcEnd, Data &targetContainer, HasChangesFunction hasChanged = {})
{
auto lessThanFunction = [this](const DataType &lhs, const DataType &rhs) { return this->lessThan(lhs, rhs); };
if (!hasChanged)
{
hasChanged = [this](const DataType &lhs, const DataType &rhs) -> DataChanges {
return this->itemHasChanged(lhs, rhs);
};
}
return updateData(srcBegin, srcEnd, targetContainer, lessThanFunction, hasChanged);
}
protected:
// Shadowing the methods in the base model, because MSVC thinks the onRemove and onInsert
// lambdas in updateData are not allowed to access the protected member functions in BaseClass.
// GCC is ok with it...
void beginInsertRows(int firstRow, int lastRow) { BaseModel::beginInsertRows(QModelIndex(), firstRow, lastRow); }
void endInsertRows() { BaseModel::endInsertRows(); }
void beginRemoveRows(int firstRow, int lastRow) { BaseModel::beginRemoveRows(QModelIndex(), firstRow, lastRow); }
void endRemoveRows() { BaseModel::endRemoveRows(); }
private:
void flushCachedChanges()
{
if (m_firstChangedRow == -1)
return;
// reconstruct vector of changed roles
QVector<int> roles;
roles.reserve(m_changedRoles.count());
for (int role : qAsConst(m_changedRoles))
{
roles.append(role);
}
// convert the vector of ints indicating changed columns into a vector of pairs of ints indicating ranges.
// for example, the vector {1, 2, 3, 5, 6, 9} would be converted to {{1, 3}, {5, 6}, {9, 9}}
typedef QVector<QPair<int, int>> ColumnRanges;
auto accumulator = [](ColumnRanges ranges, int column) {
if (ranges.isEmpty() || (ranges.last().second < column - 1))
{
ranges.append(qMakePair(column, column));
}
else
{
ranges[ranges.count() - 1].second = column;
}
return ranges;
};
const auto ranges =
std::accumulate(m_changedColumns.cbegin(), m_changedColumns.cend(), ColumnRanges(), accumulator);
for (const auto &columnRange : ranges)
{
QModelIndex topLeft = BaseModel::index(m_firstChangedRow, columnRange.first);
QModelIndex bottomRight = BaseModel::index(m_lastChangedRow, columnRange.second);
Q_EMIT BaseModel::dataChanged(topLeft, bottomRight, roles);
}
m_firstChangedRow = -1;
}
void addChange(int row, const QVector<int> &columns, const QVector<int> &roles)
{
auto mergeColumns = [this](const QVector<int> &columns) {
QVector<int> columnUnion;
// we have to cast this to QAbstractItemModel, to work around QAbstractListModel making columnCount private
columnUnion.reserve(static_cast<QAbstractItemModel *>(this)->columnCount());
auto inserter = std::back_inserter(columnUnion);
std::set_union(m_changedColumns.cbegin(), m_changedColumns.cend(), columns.cbegin(), columns.cend(),
inserter);
m_changedColumns = std::move(columnUnion);
};
if (m_firstChangedRow > -1)
{
// from here rows are guaranteed to be sequential, so no need to check for that
m_lastChangedRow = row; // optimistic, but lets see...
switch (m_changeMergePolicy)
{
case AlwaysMergeNeighbouringRows:
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
mergeColumns(columns);
return;
case MergeWhenColumnsMatch:
if (m_changedColumns == columns)
{
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
return;
}
break;
case MergeWhenRolesMatch:
if (m_changedRoles == setFromContainer(roles))
{
mergeColumns(columns);
return;
}
break;
case MergeOnPerfectMatch:
if (m_changedColumns == columns && m_changedRoles == setFromContainer(roles))
{
return;
}
break;
}
m_lastChangedRow = row - 1; // we were too optimistic
flushCachedChanges();
}
// just copy the details of the first change
m_firstChangedRow = row;
m_lastChangedRow = row;
m_changedColumns = columns;
m_changedRoles = setFromContainer(roles);
}
ChangeMergePolicy m_changeMergePolicy = MergeOnPerfectMatch;
int m_firstChangedRow = -1;
int m_lastChangedRow = -1;
QVector<int> m_changedColumns;
QSet<int> m_changedRoles;
};
#undef ForwardIt
#undef RandomIt
#undef Container
#undef BinaryPredicate
#undef EventHandler
#undef QAIM
#endif // UPDATEABLEMODEL_H