Files
qt5-legatus-legacy/Lts/Common/LtsTimeline/timeline_model.cpp
T
2026-08-13 18:33:10 +02:00

773 lines
20 KiB
C++
Executable File

/**
* @RDE_HEADER@
* MVC model
* $Id: TimelineModel.cpp 52154 2020-09-17 09:29:34Z wille_m $
* $HeadURL: svn://192.168.133.21/TDS/branches/product/features/hotline/Lts/Common/LtsTimelineClassic/TimelineModel.cpp $
* $Author: wille_m $
* Created by Mike Wille, SLSE, 2014-10-01
*/
#include "timeline_model.h"
#include "timeline_view_drag_handler.h"
#include "track.h"
#include <NGLLogger/NGLLoggerBootstrap.h>
#include <QDebug>
namespace LtsTimeline
{
TimelineModel::TimelineModel(TimelineView *view, QObject *parent)
: QAbstractItemModel(parent)
, m_view(view)
, m_logger(NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Timeline.TimelineModel"))
{
m_rootTrack.m_id = nextFreeTrackId();
m_trackCache.insert(std::pair<int, Track *>(m_rootTrack.m_id, &m_rootTrack));
}
void TimelineModel::clear()
{
m_rootTrack.m_childrenTracks.clear();
m_rootTrack.m_items.clear();
//#warning // TODO: also clear copy item ids on re-summing!!
m_rootTrack.m_copyItemIds.clear();
m_trackCache.clear();
//m_parentTrackCache.clear();
m_itemCache.clear();
m_currentTrackId = 1; // skip root track (0)
m_currentItemId = 0;
m_trackCache.insert(std::pair<int, Track *>(m_rootTrack.m_id, &m_rootTrack)); // re-add root track
}
void TimelineModel::clearItems()
{
for (std::pair<int , Track *> p : m_trackCache)
p.second->clearItems();
m_itemCache.clear();
m_currentItemId = 0;
}
Track &TimelineModel::track(int id) noexcept(false)
{
if (!m_trackCaching)
{
if (id == m_rootTrack.id())
return m_rootTrack;
return m_rootTrack.track(id);
}
auto it = m_trackCache.find(id);
if (it == m_trackCache.end())
throw QStringLiteral("Model: track with id %1 not found in cache").arg(id);
return *it->second;
}
const Track &TimelineModel::track(int id) const noexcept(false)
{
if (!m_trackCaching)
{
if (id == m_rootTrack.id())
return m_rootTrack;
return m_rootTrack.track(id);
}
std::map<int /* trackId */, Track *>::const_iterator it = m_trackCache.find(id);
if (it == m_trackCache.cend())
throw QStringLiteral("Model: const track with id %1 not found in cache").arg(id);
return *it->second;
}
Item &TimelineModel::item(int id) noexcept(false)
{
if (!m_itemCaching)
return m_rootTrack.item(id);
auto it = m_itemCache.find(id);
if (it == m_itemCache.end())
throw QStringLiteral("Model: item with id %1 not found in cache").arg(id);
return *it->second;
}
const Item &TimelineModel::item(int id) const noexcept(false)
{
if (!m_itemCaching)
return m_rootTrack.item(id);
decltype(m_itemCache)::const_iterator it = m_itemCache.find(id);
if (it == m_itemCache.cend())
throw QStringLiteral("Model: const item with id %1 not found in cache").arg(id);
return *it->second;
}
const QVector<int> TimelineModel::itemIds() const
{
if (!m_itemCaching)
return m_rootTrack.itemIds(true);
QVector<int> ids;
std::transform(m_itemCache.cbegin(), m_itemCache.cend(), std::back_inserter(ids),
[](const std::pair<int, Item *> &pair)
{
return pair.first;
});
return ids;
}
//#warning // TODO: recursive doesn't make sense on some of the following item getters, as the root track will NEVER have items, so it MUST be always recursive!
const QVector<int> TimelineModel::itemIdsBetween(qint64 start, qint64 end, bool strict, bool complete, bool recursive, bool sorted) const
{
return m_rootTrack.itemIdsBetween(start, end, strict, complete, recursive, sorted);
}
bool TimelineModel::isItemsBetween(qint64 start, qint64 end, const QVector<int> &ignoreIds, bool strict, bool complete, bool recursive) const
{
return m_rootTrack.isItemsBetween(start, end, ignoreIds, strict, complete, recursive);
}
const QVector<int> TimelineModel::copyItemIdsBetween(qint64 start, qint64 end, bool strict, bool complete, bool sorted) const
{
return m_rootTrack.copyItemIdsBetween(start, end, strict, complete, true, sorted);
}
bool TimelineModel::isItemsBetween(qint64 start, qint64 end, int ignoreId, bool strict, bool complete, bool recursive) const
{
return m_rootTrack.isItemsBetween(start, end, ignoreId, strict, complete, recursive);
}
const QVector<int> TimelineModel::_copyItemIdsBetween(Track &t, qint64 start, qint64 end, bool strict, bool complete, bool recursive, bool sorted, qint64 minimumDuration) const
{
QVector<int> itemIds;
itemIds.reserve(t.itemCount());
// copy items
for (const int itemId : qAsConst(t.copyItemIds()))
{
const Item &i = item(itemId);
if (Track::isItemBetween(i, start, end, strict, complete, minimumDuration))
itemIds += i.id();
}
if (recursive)
for (const TrackPtr &trackPtr : qAsConst(t.tracks()))
itemIds += _copyItemIdsBetween(*trackPtr.get(), start, end, strict, complete, true, false, minimumDuration);
// remove duplicates because of copy items (TODO: is this even possible??)
itemIds.erase(std::unique(itemIds.begin(), itemIds.end()), itemIds.end());
itemIds.squeeze(); // some items most likely have been filtered out, save memory
if (!sorted)
return itemIds;
//#warning // TODO: insertion sort??
std::sort(itemIds.begin(), itemIds.end(), [this](const int itemIdA, const int itemIdB)
{
try
{
return item(itemIdA).startTime() < item(itemIdB).startTime();
}
catch (...) { return false; } // hmm....
});
return itemIds;
}
const QVector<int> TimelineModel::_itemIdsBetween(Track &t, qint64 start, qint64 end, bool strict, bool complete, bool recursive, bool sorted, qint64 minimumDuration) const
{
QVector<int> itemIds;
itemIds.reserve(t.itemCount());
for (const ItemPtr &itemPtr : qAsConst(t.items()))
{
const Item &i = *itemPtr;
if (Track::isItemBetween(i, start, end, strict, complete, minimumDuration))
itemIds += i.id();
}
if (recursive)
for (const TrackPtr &trackPtr : qAsConst(t.tracks()))
itemIds += _itemIdsBetween(*trackPtr.get(), start, end, strict, complete, true, false, minimumDuration);
// remove duplicates because of copy items (TODO: is this even possible??)
//itemIds.erase(std::unique(itemIds.begin(), itemIds.end()), itemIds.end());
itemIds.squeeze(); // some items most likely have been filtered out, save memory
if (!sorted)
return itemIds;
//#warning // TODO: insertion sort??
std::sort(itemIds.begin(), itemIds.end(), [this](const int itemIdA, const int itemIdB)
{
try
{
return item(itemIdA).startTime() < item(itemIdB).startTime();
}
catch (...) { return false; } // hmm....
});
return itemIds;
}
bool TimelineModel::_isItemsBetween(const Track &t, qint64 start, qint64 end, const QVector<int> &ignoreIds, bool strict, bool complete, bool recursive, qint64 minimumDuration) const
{
for (const ItemPtr &itemPtr : qAsConst(t.items()))
{
const Item &i = *itemPtr;
if (!ignoreIds.contains(i.id()))
if (Track::isItemBetween(i, start, end, strict, complete, minimumDuration))
return true;
}
if (!recursive)
return false;
for (const TrackPtr &trackPtr : qAsConst(t.tracks()))
if (_isItemsBetween(*trackPtr.get(), start, end, ignoreIds, strict, complete, true, minimumDuration))
return true;
return false;
}
bool TimelineModel::_isItemsBetween(const Track &t, qint64 start, qint64 end, int ignoreId, bool strict, bool complete, bool recursive, qint64 minimumDuration) const
{
return _isItemsBetween(t, start, end, QVector<int>({ ignoreId }), strict, complete, recursive, minimumDuration);
}
QVariant TimelineModel::data(const QModelIndex &index, int role) const
{
if ((role != Qt::DisplayRole && role != Qt::UserRole) || !index.isValid())
return {};
int trackId = index.internalId();
int column = index.column();
if (column == 1 && role == Qt::DisplayRole) // item column (1)
return trackId;
if (role == Qt::UserRole) // doesn't matter what column, as a track is a row ;)
{
if (column == 0)
return trackId; // ffs
try
{
const Track &t = track(trackId);
return t.userData();
}
catch(...)
{
Q_ASSERT(false);
return {};
}
}
if (column != 0) // actuator column (0) AND display role! (can only be display role and user role - but user role would have returned already)
return {};
QString text;
try
{
const Track &t = track(trackId);
#if 0 // #ifdef LTS_DEBUG
text = QStringLiteral("[%1] %3").arg(t.id()).arg(t.text());
#else
text = t.text();
#endif
}
catch(...)
{
Q_ASSERT(false);
return QStringLiteral("[?????]");
}
if (text.isEmpty())
text = tr("[unknown]");
return text;
}
Qt::ItemFlags TimelineModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return {};
return QAbstractItemModel::flags(index);
}
QModelIndex TimelineModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return {};
int childTrackId;
try
{
const Track &parentTrack = parent.isValid() ? track(parent.internalId()) : m_rootTrack;
const Track &childTrack = parentTrack.trackAt(row); // throws - qt!
childTrackId = /*static_cast<quintptr>(*/childTrack.id()/*)*/;
}
catch (const QString &msg)
{
DEBUG_LOG(m_logger) << Q_FUNC_INFO << "cannot get parent track " << msg;
return {};
}
catch (const std::out_of_range &oor)
{
DEBUG_LOG(m_logger) << Q_FUNC_INFO << "cannot get child track " << QString::fromUtf8(oor.what());
return {};
}
return createIndex(row, column, childTrackId);
}
int TimelineModel::rowOfTrackId(int id) const noexcept(false)
{
const Track &parent = track/*2*/(track/*2*/(id).parentId()); // throws!
if (parent.items().empty())
throw("parent doesn't have items");
int row = 0;
for (const TrackPtr &trackPtr : qAsConst(parent.tracks()))
{
if (trackPtr->id() == id)
return row;
row++;
}
throw("not found");
}
const Track &TimelineModel::trackOfIndex(const QModelIndex &index) const
{
if (!index.isValid())
throw QStringLiteral("trackOfIndex: index is invalid!");
// get child track
try
{
return track(index.internalId());
}
catch (const QString &msg)
{
throw QStringLiteral("trackOfIndex: cannot find track by internal id %1: %2").arg(index.internalId()).arg(msg);
}
// TODO: assert? or is this okay-ish behaviour?
return m_rootTrack;
}
//void TimelineModel::move(int fromTrackId, int itemId, int toTrackId, qint64 newStartTime) noexcept(false)
//{
// // NOTE: no need to modify cache, as the item class / ptr doesn't change
// Track &sourceTrack = track(fromTrackId); // throws!
// ItemPtr itemPtr = sourceTrack.takeItem(itemId); // throws!
// itemPtr->_setStartTime(newStartTime);
// Track &destTrack = track(toTrackId); // throws!
// destTrack.addItemPtr(std::move(itemPtr)); // sorts!
//#warning // TODO: !
// emit dataChanged({},{});
//}
const Track &TimelineModel::parentOfTrackId(int id) const noexcept(false)
{
if (!m_trackCaching)
{
if (m_rootTrack.hasTrack(id))
return m_rootTrack;
// return m_rootTrack.parentOf(id);
//not found look the old way instead
}
const Track &theTrack = track(id);
if (theTrack.parentId() == m_rootTrack.id())
return m_rootTrack;
return track(theTrack.parentId());
}
QModelIndex TimelineModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return {};
// => gib mir den parent track vom dem gegebenen track
/*
[ root ] <= auf dem parent des parent muss also geschaut werden, in welcher row der parent liegt!
[ sub ] <= der parent!
[ sub ] <= gib mir den parent von dem da
*/
// createIndex mit ...:
// die row, in dem der parent liegt
// die column ... 0
// die track id from parent
// man will also die ROW vom parent, indem man auf dem parent des parent guckt, in welcher row er liegt
int trackId = index.internalId();
int parentId;
int superParentId;
try
{
const Track &parent = parentOfTrackId(trackId);
parentId = parent.id();
superParentId = parent.parentId();
}
catch (const QString &msg)
{
DEBUG_LOG(m_logger) << Q_FUNC_INFO << "cannot get parent of track with id" << trackId << msg;
return {};
}
if (parentId == m_rootTrack.id()) // there is no parent for the parent
return {};
int parentsRow;
try
{
const Track &superParent = track(superParentId);
try
{
parentsRow = superParent.rowOfTrack(parentId);
}
catch (const QString &msg)
{
DEBUG_LOG(m_logger) << Q_FUNC_INFO << "row of track for parent not found for parent id" << parentId << "-" << msg;
return {};
}
}
catch (const QString &msg)
{
DEBUG_LOG(m_logger) << Q_FUNC_INFO << "super parent not found for super parent id" << superParentId << "-" << msg;
return {};
}
return createIndex(parentsRow, 0, parentId);
}
void TimelineModel::setItemStartTime(Track &t, Item &i, qint64 newStart)
{
i._setStartTime(newStart);
t.sort(); // TODO: or just invalidate?
}
void TimelineModel::setItemStartTime(Item &i, qint64 newStart) noexcept(false)
{
setItemStartTime(track(i.parentTrackId()), i, newStart);
}
void TimelineModel::setItemStartTime(int itemId, qint64 newStart) noexcept(false)
{
setItemStartTime(item(itemId), newStart);
}
void TimelineModel::setItemDuration(Track &t, Item &i, int duration)
{
i._setDuration(std::max(duration, m_minItemDuration));
t.sort(); // TODO: or just invalidate?
}
void TimelineModel::setItemDuration(Item &i, int duration) noexcept(false)
{
setItemDuration(track(i.parentTrackId()), i, duration);
}
void TimelineModel::setItemDuration(int itemId, int duration) noexcept(false)
{
setItemDuration(item(itemId), duration);
}
void TimelineModel::copyToClipboard(const QVector<int> &ids) noexcept(false)
{
m_clipboard.clear();
m_clipboard.reserve(ids.size());
qint64 tStartMin = LLONG_MAX;
for (int itemId : qAsConst(ids))
{
const Item &i = item(itemId);
ItemPtr iPtr = std::make_unique<Item>(Item(i.startTime(), i.duration(), i.color()));
iPtr->m_parentTrackId = i.m_parentTrackId;
m_clipboard.push_back(std::move(iPtr));
tStartMin = std::min(tStartMin, i.startTime());
}
for (ItemPtr &iPtr : m_clipboard)
iPtr->_setStartTime(iPtr->startTime() - tStartMin);
}
void TimelineModel::cutToClipboard(const QVector<int> &ids) noexcept(false)
{
copyToClipboard(ids);
for (int itemId : qAsConst(ids))
removeItem(itemId);
}
void TimelineModel::pasteFromClipboard(qint64 time) noexcept(false)
{
if (m_isSnapEnabled)
time -= (time % m_snapStep);
for (ItemPtr &iPtr : m_clipboard)
{
iPtr->_setStartTime(time + iPtr->startTime());
addItem(iPtr->parentTrackId(), std::move(iPtr));
}
m_clipboard.clear();
}
int TimelineModel::rowCount(const QModelIndex &parent) const
{
if (parent.column() > 0)
return 0;
if (!parent.isValid())
return m_rootTrack.tracks().size();
try
{
return track(parent.internalId()).tracks().size();
}
catch(...) {}
return 0;
}
QModelIndex TimelineModel::indexOfTrack(int trackId) const
{
for (int row = 0; row < rowCount(); ++row)
{
QModelIndex idx = indexOfTrackFromIndex(index(row, 0, {}), trackId);
if (idx.isValid())
return idx;
}
return {};
}
QModelIndex TimelineModel::indexOfTrackFromIndex(const QModelIndex &idx, int trackId) const
{
if (static_cast<int>(idx.internalId()) == trackId)
return index(idx.row(), 1, idx.parent());
for (int row = 0; row < rowCount(idx); ++row)
{
const QModelIndex i = indexOfTrackFromIndex(index(row, 0, idx), trackId);
if (i.isValid())
return i;
}
return {};
}
QModelIndex TimelineModel::indexOfTrackSafe(int trackId) const noexcept(false)
{
for (int row = 0; row < rowCount(); ++row)
{
const QModelIndex idx(indexOfTrackFromIndex(index(row, 0 /* actuator */, {}), trackId));
if (idx.isValid())
return idx;
}
QString trackText;
try
{
trackText = track(trackId).text();
} catch(...){}
throw QStringLiteral("indexOfTrackSafe: index for trackId %1 ('%2') not found ").arg(trackId).arg(trackText.isEmpty() ? QStringLiteral("(unknown)") : trackText);
}
QModelIndex TimelineModel::indexOfTrackFromIndexSafe(const QModelIndex &idx, int trackId) const noexcept(false)
{
if (static_cast<int>(idx.internalId()) == trackId)
return idx;
for (int row = 0; row < rowCount(idx); ++row)
{
const QModelIndex i = indexOfTrackFromIndex(index(row, 1, idx), trackId);
if (i.isValid())
return i;
}
throw QStringLiteral("indexOfTrackFromIndexSafe: index for trackId %1 not found").arg(trackId);
}
void TimelineModel::moveItemToTrack(int itemId, int trackId, qint64 time) noexcept(false)
{
/* NOTE: CAUTION! There is no rollback functionality - so be sure to only call
* safe, non-throwing functions that are guaranteed to succeed after the item
* was stolen, the new start time was set or the new parent id was set.
* Otherwise the result will be undefined and have inconsistent data. */
const Item &theItem = item(itemId);
Track &oldTrack = track(theItem.parentTrackId());
Track &newTrack = track(trackId);
ItemPtr itemPtr = oldTrack.takeItem(itemId); // stolen!
// point of no return (see comment!)
Item *theItemPtr = newTrack.addItem(std::move(itemPtr)); // doesn't throw!
setItemStartTime(newTrack, *theItemPtr, time); // doesn't throw!
theItemPtr->m_parentTrackId = trackId; // set item's parent id to new track's id
}
void TimelineModel::removeItem(int itemId) noexcept(false)
{
const Item &i = item(itemId);
Track &t = track(i.parentTrackId());
t.removeItem(itemId);
auto it = m_itemCache.find(itemId);
if (it == m_itemCache.end())
{
Q_ASSERT(false);
throw QStringLiteral("removeItem: item with id %1 not found").arg(itemId);
}
m_itemCache.erase(it);
// QModelIndex idx = indexOfTrack(t.id());
// emit dataChanged(idx, idx);
}
void TimelineModel::removeTrack(int trackId) noexcept(false)
{
if (trackId == m_rootTrack.id())
{
Q_ASSERT(false);
throw QStringLiteral("removeTrack: the root track shouldn't and *can't* be deleted");
}
// first *all* items of the track *and its sub-tracks* have to be erased from this model's item cache
const Track &t = track(trackId);
Track &parentTrack = track(t.parentId());
for (int itemId : t.itemIds(true))
{
auto it = m_itemCache.find(itemId);
if (it == m_itemCache.end())
{
Q_ASSERT(false);
continue;
}
m_itemCache.erase(it);
}
// get the parent of the Track, to tell it to remove its child (the given track id) NOTE: lowest level of parent track is m_rootTrack
parentTrack.removeTrack(trackId);
auto it = m_trackCache.find(trackId);
if (it == m_trackCache.end())
{
Q_ASSERT(false);
return;
}
m_trackCache.erase(it);
}
void TimelineModel::setMinimumDuration(int duration)
{
if (duration < 0)
{
WARNING_LOG(m_logger) << "negative duration is not possible -> setting minimum duration to 0";
duration = 0;
}
if (duration % 1000 && m_isSnapEnabled)
WARNING_LOG(m_logger) << "Snapping to 1000ms enabled, yet minimum duration is not a multiple of 1000ms: " << duration;
m_minItemDuration = duration;
}
bool TimelineModel::canDropItem(const Item &item, int trackId, qint64 start) const
{
Q_UNUSED(item);
Q_UNUSED(trackId);
Q_UNUSED(start);
return true;
}
bool TimelineModel::canMoveItem(const Item &item, qint64 start) const
{
Q_UNUSED(item);
Q_UNUSED(start);
return true;
}
bool TimelineModel::canResizeItem(const Item &item, qint64 start, int duration) const
{
Q_UNUSED(item);
Q_UNUSED(start);
Q_UNUSED(duration);
return true;
}
bool TimelineModel::isItemDragged(int _itemId) const
{
return m_view->dragHandler() ? m_view->dragHandler()->dragItemIds().contains(_itemId) : false;
}
} // namespace LtsTimeline