Commit 61409123 authored by Nicolas Carion's avatar Nicolas Carion
Browse files

start refactoring selection mechanism

parent d9aed044
......@@ -659,34 +659,6 @@ Mlt::Profile *Core::thumbProfile()
return m_thumbProfile.get();
}
void Core::clearSelection()
{
if (m_mainWindow && m_guiConstructed) {
m_mainWindow->getCurrentTimeline()->controller()->clearSelection();
}
}
void Core::selectItem(int itemId)
{
if (m_mainWindow && m_guiConstructed) {
m_mainWindow->getCurrentTimeline()->controller()->addSelection(itemId, true);
}
}
bool Core::isSelected(int itemId) const
{
if (m_mainWindow && m_guiConstructed) {
return m_mainWindow->getCurrentTimeline()->controller()->selection().contains(itemId);
}
return false;
}
void Core::removeFromSelection(int itemId)
{
if (m_mainWindow && m_guiConstructed) {
m_mainWindow->getCurrentTimeline()->controller()->removeSelection(itemId);
}
}
void Core::triggerAction(const QString &name)
{
......
......@@ -174,10 +174,6 @@ public:
/** Show / hide keyframes for a timeline clip */
void showClipKeyframes(ObjectId id, bool enable);
Mlt::Profile *thumbProfile();
void clearSelection();
void selectItem(int itemId);
bool isSelected(int itemId) const;
void removeFromSelection(int itemId);
/** @brief Returns the current project duration */
int projectDuration() const;
/** @brief Returns true if current project has some rendered timeline preview */
......
......@@ -2352,39 +2352,39 @@ void MainWindow::slotRemoveAllSpace()
void MainWindow::slotInsertTrack()
{
pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
getMainTimeline()->controller()->addTrack(-1);
getCurrentTimeline()->controller()->addTrack(-1);
}
void MainWindow::slotDeleteTrack()
{
pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
getMainTimeline()->controller()->deleteTrack(-1);
getCurrentTimeline()->controller()->deleteTrack(-1);
}
void MainWindow::slotSelectTrack()
{
getMainTimeline()->controller()->selectCurrentTrack();
getCurrentTimeline()->controller()->selectCurrentTrack();
}
void MainWindow::slotSelectAllTracks()
{
getMainTimeline()->controller()->selectAll();
getCurrentTimeline()->controller()->selectAll();
}
void MainWindow::slotUnselectAllTracks()
{
getMainTimeline()->controller()->clearSelection();
getCurrentTimeline()->model()->requestClearSelection();
}
void MainWindow::slotEditGuide()
{
getMainTimeline()->controller()->editGuide();
getCurrentTimeline()->controller()->editGuide();
}
void MainWindow::slotDeleteGuide()
{
getMainTimeline()->controller()->switchGuide(-1, true);
getCurrentTimeline()->controller()->switchGuide(-1, true);
}
void MainWindow::slotDeleteAllGuides()
......
......@@ -125,7 +125,6 @@ void ClipModel::deregisterClipToBin()
{
std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
binClip->deregisterTimelineClip(m_id);
pCore->removeFromSelection(m_id);
}
ClipModel::~ClipModel() = default;
......
This diff is collapsed.
......@@ -51,8 +51,13 @@ struct TimelineFunctions
/* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */
static bool processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo);
/* @brief Makes a perfect copy of a given clip, but do not insert it */
static bool copyClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo);
/* @brief Makes a perfect clone of a given clip, but do not insert it */
static bool cloneClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo);
/* @brief Creates a string representation of the given clips, that can then be pasted using pasteClips(). Return an empty string on failure */
static QString copyClips(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> itemIds);
/* @brief Paste the clips as described by the string. Returns true on success*/
static bool pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position);
/* @brief Request the addition of multiple clips to the timeline
* If the addition of any of the clips fails, the entire operation is undone.
......@@ -93,7 +98,7 @@ struct TimelineFunctions
static bool requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget);
static void setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack);
static void enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable);
static void saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> selection, const QDir &targetDir);
static void saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection, const QDir &targetDir);
/** @brief returns the number of same type tracks between 2 tracks
*/
static int getTrackOffset(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int destTrack);
......
......@@ -300,10 +300,8 @@ QVariant TimelineItemModel::data(const QModelIndex &index, int role) const
return clip->getPosition();
case DurationRole:
return clip->getPlaytime();
case GroupedRole: {
int parentId = m_groups->getDirectAncestor(id);
return parentId != -1 && parentId != m_temporarySelectionGroup;
}
case GroupedRole:
return m_groups->isInGroup(id);
case EffectNamesRole:
return clip->effectNames();
case InPointRole:
......@@ -493,23 +491,6 @@ bool TimelineItemModel::loadGroups(const QString &groupsData)
return m_groups->fromJson(groupsData);
}
bool TimelineItemModel::isInMultiSelection(int cid) const
{
if (m_temporarySelectionGroup == -1) {
return false;
}
bool res = (m_groups->getRootId(cid) == m_temporarySelectionGroup) && (m_groups->getDirectChildren(m_temporarySelectionGroup).size() != 1);
return res;
}
bool TimelineItemModel::isSelected(int cid) const
{
if (m_temporarySelectionGroup == -1) {
return false;
}
return m_groups->getRootId(cid) == m_temporarySelectionGroup;
}
void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb)
{
QVector<int> roles;
......
......@@ -99,10 +99,6 @@ public:
const QString groupsData();
bool loadGroups(const QString &groupsData);
/* @brief returns true if clip is in temporary selection group.
*/
bool isInMultiSelection(int cid) const;
bool isSelected(int cid) const;
void _beginRemoveRows(const QModelIndex &, int, int) override;
void _beginInsertRows(const QModelIndex &, int, int) override;
......
......@@ -78,7 +78,11 @@ RTTR_REGISTRATION
.method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
.method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
parameter_names("pos", "id", "trackName", "audioTrack"))
.method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"));
.method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
.method("requestClearSelection", select_overload<void(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
.method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
.method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
.method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("ids"));
}
int TimelineModel::next_id = 0;
......@@ -94,7 +98,6 @@ TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack>
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_temporarySelectionGroup(-1)
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
......@@ -1047,6 +1050,7 @@ bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
if (m_groups->isInGroup(itemId)) {
bool res = requestGroupDeletion(itemId, logUndo);
TRACE_RES(res);
requestClearSelection(true);
return res;
}
Fun undo = []() { return true; };
......@@ -1064,6 +1068,7 @@ bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
PUSH_UNDO(undo, redo, actionLabel);
}
TRACE_RES(res);
requestClearSelection(true);
return res;
}
......@@ -1086,7 +1091,6 @@ bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo)
return true;
};
if (operation()) {
emit removeFromSelection(clipId);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
......@@ -1115,7 +1119,6 @@ bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun
return true;
};
if (operation()) {
emit removeFromSelection(compositionId);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
......@@ -1401,6 +1404,7 @@ bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, logUndo);
requestClearSelection();
if (!m_groups->isInGroup(clipId)) {
TRACE_RES(false);
return false;
......@@ -1424,8 +1428,8 @@ bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo)
std::unordered_set<int> all_compositions;
while (!group_queue.empty()) {
int current_group = group_queue.front();
if (m_temporarySelectionGroup == current_group) {
m_temporarySelectionGroup = -1;
if (m_currentSelection == current_group) {
m_currentSelection = -1;
}
group_queue.pop();
Q_ASSERT(isGroup(current_group));
......@@ -1600,17 +1604,7 @@ int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, bool lo
TRACE(ids, logUndo, type);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (m_temporarySelectionGroup > -1) {
m_groups->destructGroupItem(m_temporarySelectionGroup);
// We don't log in undo the selection changes
// int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin();
// requestClipUngroup(firstChild, undo, redo);
m_temporarySelectionGroup = -1;
}
int result = requestClipsGroup(ids, undo, redo, type);
if (type == GroupType::Selection) {
m_temporarySelectionGroup = result;
}
if (result > -1 && logUndo && type != GroupType::Selection) {
PUSH_UNDO(undo, redo, i18n("Group clips"));
}
......@@ -1649,21 +1643,9 @@ bool TimelineModel::requestClipsUngroup(const std::unordered_set<int> &itemIds,
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
int old_selection = m_temporarySelectionGroup;
if (m_temporarySelectionGroup != -1) {
// Delete selection group without undo
Fun tmp_undo = []() { return true; };
Fun tmp_redo = []() { return true; };
requestClipUngroup(m_temporarySelectionGroup, tmp_undo, tmp_redo);
m_temporarySelectionGroup = -1;
}
requestClearSelection();
std::unordered_set<int> roots;
for (int itemId : itemIds) {
int root = m_groups->getRootId(itemId);
if (root != old_selection) {
roots.insert(root);
}
}
std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
for (int root : roots) {
result = result && requestClipUngroup(root, undo, redo);
}
......@@ -1682,18 +1664,11 @@ bool TimelineModel::requestClipUngroup(int itemId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, logUndo);
requestClearSelection();
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
if (itemId == m_temporarySelectionGroup) {
// Delete selection group without undo
Fun tmp_undo = []() { return true; };
Fun tmp_redo = []() { return true; };
requestClipUngroup(itemId, tmp_undo, tmp_redo);
m_temporarySelectionGroup = -1;
} else {
result = requestClipUngroup(itemId, undo, redo);
}
result = requestClipUngroup(itemId, undo, redo);
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
}
......@@ -2632,6 +2607,12 @@ bool TimelineModel::checkConsistency()
qDebug() << "== ERROR IN GROUP CONSISTENCY";
return false;
}
// Check that the selection is in a valid state:
if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isGroup(m_currentSelection)) {
qDebug() << "Selection is in inconsistent state";
return false;
}
return true;
}
......@@ -2880,3 +2861,109 @@ int TimelineModel::getNextTrackId(int trackId)
}
return it == m_allTracks.end() ? trackId : (*it)->getId();
}
void TimelineModel::requestClearSelection(bool onDeletion)
{
QWriteLocker locker(&m_lock);
TRACE();
if (m_currentSelection == -1) {
return;
}
if (isGroup(m_currentSelection)) {
if (m_groups->getType(m_currentSelection) == GroupType::Selection) {
m_groups->destructGroupItem(m_currentSelection);
}
} else {
Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection));
}
m_currentSelection = -1;
emit selectionChanged();
}
void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo)
{
Fun operation = [this, onDeletion]() {
requestClearSelection(onDeletion);
return true;
};
Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); };
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
}
std::unordered_set<int> TimelineModel::getCurrentSelection() const
{
READ_LOCK();
if (m_currentSelection == -1) {
return {};
}
if (isGroup(m_currentSelection)) {
return m_groups->getLeaves(m_currentSelection);
} else {
Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection));
return {m_currentSelection};
}
}
void TimelineModel::requestAddToSelection(int itemId, bool clear)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, clear);
if (clear) {
requestClearSelection();
}
std::unordered_set<int> selection = getCurrentSelection();
if (selection.count(itemId) == 0) {
selection.insert(itemId);
requestSetSelection(selection);
}
}
void TimelineModel::requestRemoveFromSelection(int itemId)
{
QWriteLocker locker(&m_lock);
TRACE(itemId);
std::unordered_set<int> selection = getCurrentSelection();
if (selection.count(itemId) > 0) {
selection.erase(itemId);
requestSetSelection(selection);
}
}
bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids)
{
QWriteLocker locker(&m_lock);
TRACE(ids);
requestClearSelection();
// if the items are in groups, we must retrieve their topmost containing groups
std::unordered_set<int> roots;
std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
bool result = true;
if (roots.size() == 0) {
m_currentSelection = -1;
} else if (roots.size() == 1) {
m_currentSelection = *(roots.begin());
} else {
Fun undo = []() { return true; };
Fun redo = []() { return true; };
result = m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection);
Q_ASSERT(m_currentSelection >= 0);
}
emit selectionChanged();
return result;
}
bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids, Fun &undo, Fun &redo)
{
Fun reverse = [this]() {
requestClearSelection(false);
return true;
};
Fun operation = [this, ids]() { return requestSetSelection(ids); };
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
......@@ -596,6 +596,29 @@ public:
/** @brief Refresh the tractor profile in case a change was requested. */
void updateProfile(Mlt::Profile *profile);
/** @brief Clear the current selection
@param onDeletion is true when the selection is cleared as a result of a deletion
*/
Q_INVOKABLE void requestClearSelection(bool onDeletion = false);
// same function with undo/redo accumulation
void requestClearSelection(bool onDeletion, Fun &undo, Fun &redo);
/** @brief Add the given item to the selection
If @param clear is true, the selection is first cleared
*/
Q_INVOKABLE void requestAddToSelection(int itemId, bool clear = false);
/** @brief Remove the given item from the selection */
Q_INVOKABLE void requestRemoveFromSelection(int itemId);
/** @brief Set the selection to the set of given ids */
bool requestSetSelection(const std::unordered_set<int> &ids);
// same function with undo/redo
bool requestSetSelection(const std::unordered_set<int> &ids, Fun &undo, Fun &redo);
/** @brief Returns a set containing all the items in the selection */
std::unordered_set<int> getCurrentSelection() const;
protected:
/* @brief Register a new track. This is a call-back meant to be called from TrackModel
@param pos indicates the number of the track we are adding. If this is -1, then we add at the end.
......@@ -688,8 +711,9 @@ signals:
void invalidateZone(int in, int out);
/* @brief signal triggered when a track duration changed (insertion/deletion) */
void durationUpdated();
/* @brief an item was deleted, make sure it is removed from selection */
void removeFromSelection(int id);
/* @brief Signal sent whenever the selection changes */
void selectionChanged();
protected:
std::unique_ptr<Mlt::Tractor> m_tractor;
......@@ -724,8 +748,10 @@ protected:
bool m_id; // id of the timeline itself
// id of the currently selected group in timeline, should be destroyed on each new selection
int m_temporarySelectionGroup;
// id of the selection. If -1, there is no selection, if positive, then it might either be the id of the selection group, or the id of an individual
// item, or, finally, the id of a group which is not of type selection. The last case happens when the selection exactly matches an existing group
// (in that case we cannot further group it because the selection would have only one child, which is prohibited by design)
int m_currentSelection = -1;
// The index of the temporary overlay track in tractor, or -1 if not connected
int m_overlayTrackCount;
......
......@@ -237,7 +237,7 @@ Rectangle {
root.stopScrolling = true
if (mouse.button == Qt.RightButton) {
if (timeline.selection.indexOf(clipRoot.clipId) == -1) {
timeline.addSelection(clipRoot.clipId, true)
controller.requestAddToSelection(clipRoot.clipId, true)
}
clipMenu.clipId = clipRoot.clipId
clipMenu.clipStatus = clipRoot.clipStatus
......
......@@ -587,13 +587,6 @@ Rectangle {
border.color: selected? 'red' : 'transparent'
border.width: selected? 1 : 0
z: 1
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
//timeline.selectMultitrack()
}
}
}
Flickable {
// Non-slider scroll area for the track headers.
......@@ -774,7 +767,7 @@ Rectangle {
}
} else if (root.activeTool === 0 || mouse.y <= ruler.height) {
if (mouse.y > ruler.height) {
timeline.selection = []
controller.requestClearSelection();
}
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
......@@ -988,10 +981,10 @@ Rectangle {
if (mouse.modifiers & Qt.ShiftModifier) {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
console.log('ADD SELECTION: ', dragProxy.draggedItem)
timeline.addSelection(dragProxy.draggedItem)
controller.requestAddToSelection(dragProxy.draggedItem)
} else {
console.log('REMOVE SELECTION: ', dragProxy.draggedItem)
timeline.removeSelection(dragProxy.draggedItem)
controller.requestRemoveFromSelection(dragProxy.draggedItem)
//endDrag()
shiftClick = true
return
......@@ -999,7 +992,7 @@ Rectangle {
shiftClick = true
} else {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
timeline.selection = [ dragProxy.draggedItem ]
controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true)
}
shiftClick = false
}
......
This diff is collapsed.
......@@ -39,7 +39,7 @@ class TimelineController : public QObject
Q_OBJECT
/* @brief holds a list of currently selected clips (list of clipId's)
*/
Q_PROPERTY(QList<int> selection READ selection WRITE setSelection NOTIFY selectionChanged)
Q_PROPERTY(QList<int> selection READ selection NOTIFY selectionChanged)
/* @brief holds the timeline zoom factor
*/
Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged)
......@@ -78,27 +78,35 @@ public:
void setModel(std::shared_ptr<TimelineItemModel> model);
std::shared_ptr<TimelineItemModel> getModel() const;
void setRoot(QQuickItem *root);
Q_INVOKABLE bool isMultitrackSelected() const { return m_selection.isMultitrackSelected; }
Q_INVOKABLE int selectedTrack() const { return m_selection.selectedTrack; }
/** @brief Remove a clip id from current selection
*/
Q_INVOKABLE void removeSelection(int newSelection);
/** @brief Add a clip id to current selection
*/
Q_INVOKABLE void addSelection(int newSelection, bool clear = false);
/** @brief Edit an item's in/out points with a dialog
*/
Q_INVOKABLE void editItemDuration(int itemId);
/** @brief Clear current selection and inform the view
*/
void clearSelection();
/** @brief Returns the topmost track containing a selected item (-1 if selection is embty) */
Q_INVOKABLE int selectedTrack() const;
/** @brief Select the clip in active track under cursor
@param type is the type of the object (clip or composition)
@param select: true if the object should be selected and false if it should be deselected
@param addToCurrent: if true, the object will be added to the new selection
*/
void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false);