From 023ded1fb56456eed31cae1a05041bd9d21ec92b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 23 Jul 2020 18:37:57 +1000 Subject: [PATCH] timeline: overhauled pointer tool again Allows dynamic switching between pointer/roll/slide since occasionally this behavior is interchangeable. Fixes a lot of transition behavior (but not all of it). --- app/timeline/trackreference.cpp | 8 + app/timeline/trackreference.h | 4 + app/widget/timelinewidget/timelinewidget.cpp | 6 +- app/widget/timelinewidget/timelinewidget.h | 16 +- app/widget/timelinewidget/tool/pointer.cpp | 531 +++++++++--------- app/widget/timelinewidget/tool/ripple.cpp | 8 +- app/widget/timelinewidget/tool/rolling.cpp | 2 +- app/widget/timelinewidget/tool/slide.cpp | 2 +- app/widget/timelinewidget/tool/tool.cpp | 17 - app/widget/timelinewidget/undo/undo.cpp | 183 +++--- app/widget/timelinewidget/undo/undo.h | 27 +- .../view/timelineviewghostitem.h | 5 +- 12 files changed, 406 insertions(+), 403 deletions(-) diff --git a/app/timeline/trackreference.cpp b/app/timeline/trackreference.cpp index 8360d0fd8..6c046df7a 100644 --- a/app/timeline/trackreference.cpp +++ b/app/timeline/trackreference.cpp @@ -49,4 +49,12 @@ bool TrackReference::operator==(const TrackReference &ref) const return type_ == ref.type_ && index_ == ref.index_; } +uint qHash(const TrackReference &r, uint seed) +{ + // Not super efficient, but couldn't think of any better way to ensure a different hash each time + return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()), + QString::number(r.index())), + seed); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/timeline/trackreference.h b/app/timeline/trackreference.h index 4aea1e3c2..842985168 100644 --- a/app/timeline/trackreference.h +++ b/app/timeline/trackreference.h @@ -36,6 +36,8 @@ public: const int& index() const; + bool operator<(const TrackReference& ref) const; + bool operator==(const TrackReference& ref) const; private: @@ -44,6 +46,8 @@ private: int index_; }; +uint qHash(const TrackReference& r, uint seed); + OLIVE_NAMESPACE_EXIT #endif // TRACKREFERENCE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 63163da0e..3a89da8fc 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -523,9 +523,6 @@ void TimelineWidget::DeleteSelected(bool ripple) } } - // Replace clips with gaps (effectively deleting them) - ReplaceBlocksWithGaps(clips_to_delete, true, command); - // For transitions, remove them but extend their attached blocks to fill their place foreach (TransitionBlock* transition, transitions_to_delete) { new TransitionRemoveCommand(TrackOutput::TrackFromBlock(transition), @@ -537,6 +534,9 @@ void TimelineWidget::DeleteSelected(bool ripple) command); } + // Replace clips with gaps (effectively deleting them) + ReplaceBlocksWithGaps(clips_to_delete, true, command); + // Insert ripple command now that it's all cleaned up gaps if (ripple) { TimeRangeList range_list; diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index c3d6d5912..7b957c67c 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -243,9 +243,9 @@ private: virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode); - TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed); + TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists = false); - TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed); + TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode); /** * @brief Validates Ghosts that are getting their in points trimmed @@ -265,16 +265,10 @@ private: virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); - enum GhostMode { - kPointer, - kRolling, - kSlide - }; - void InitiateDragInternal(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode, - GhostMode pointer_mode, - bool trim_overwrite_allowed); + bool dont_roll_trims, + bool allow_nongap_rolling, bool slide_instead_of_moving); const Timeline::MovementMode& drag_movement_mode() const { @@ -304,7 +298,7 @@ private: private: Timeline::MovementMode IsCursorInTrimHandle(TimelineViewBlockItem* block, qreal cursor_x); - void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode, bool trim_overwrite_allowed); + void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode); bool IsClipTrimmable(TimelineViewBlockItem* clip, const QList& items, diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 9c17f4e8d..85e1884f5 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -194,10 +194,17 @@ void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) } } +void SetGhostToSlideMode(TimelineViewGhostItem* g) +{ + g->SetCanMoveTracks(false); + g->setData(TimelineViewGhostItem::kGhostIsSliding, true); +} + void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode, - GhostMode pointer_mode, - bool trim_overwrite_allowed) + bool dont_roll_trims, + bool allow_nongap_rolling, + bool slide_instead_of_moving) { // Get list of selected blocks QList clips = parent()->GetSelectedBlocks(); @@ -214,42 +221,108 @@ void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *cl return; } - // Create ghosts for moving - foreach (TimelineViewBlockItem* clip_item, clips) { - Block* block = clip_item->block(); + // Determine if this move is a slide, which is determined by either + bool clips_are_sliding = (slide_instead_of_moving || clicked_block_type == Block::kTransition); - if (block->type() == Block::kGap) { - // Gaps cannot move, ignore this block - continue; + if (clips_are_sliding) { + // This is a slide. What we do here is move clips within their own track, between the clips + // that they're already next to. We don't allow changing tracks or changing the order of + // blocks. + // + // For slides to be legal, we make all blocks "contiguous". This means that only one series + // of blocks can move at a time and prevents. + + QHash earliest_block_on_track; + QHash latest_block_on_track; + + foreach (TimelineViewBlockItem* item, clips) { + Block* this_block = item->block(); + const TrackReference& track = item->Track(); + + Block* current_earliest = earliest_block_on_track.value(track, nullptr); + if (!current_earliest || this_block->in() < current_earliest->in()) { + earliest_block_on_track.insert(track, item->block()); + } + + Block* current_latest = latest_block_on_track.value(track, nullptr); + if (!current_latest || this_block->out() > current_earliest->out()) { + latest_block_on_track.insert(track, item->block()); + } } - if (clicked_block_type == Block::kTransition && block->type() != Block::kTransition) { - // Transitions always slide rather than move, so if we clicked a transition, ignore any - // non-transitions - continue; + QHash::const_iterator i; + for (i=earliest_block_on_track.constBegin(); i!=earliest_block_on_track.constEnd(); i++) { + // Make a contiguous stream + const TrackReference& track = i.key(); + Block* earliest = i.value(); + Block* latest = latest_block_on_track.value(i.key()); + + // First we add the block that's out trimming, the one prior to the earliest + TimelineViewGhostItem* earliest_ghost; + if (earliest->previous()) { + earliest_ghost = AddGhostFromBlock(earliest->previous(), track, Timeline::kTrimOut); + } else { + earliest_ghost = AddGhostFromNull(earliest->in(), earliest->in(), track, Timeline::kTrimOut); + } + SetGhostToSlideMode(earliest_ghost); + + // Then we add the block that's in trimming, the one after the latest + if (latest->next()) { + TimelineViewGhostItem* latest_ghost = AddGhostFromBlock(latest->next(), track, Timeline::kTrimIn); + SetGhostToSlideMode(latest_ghost); + } + + // Finally, we add all of the moving blocks in between + Block* b = nullptr; + do { + // On first run-through, set to earliest only. From then on, set to the next of the last + // in the loop. + if (b) { + b = b->next(); + } else { + b = earliest; + } + + TimelineViewGhostItem* between_ghost = AddGhostFromBlock(b, track, Timeline::kMove); + SetGhostToSlideMode(between_ghost); + } while (b != latest); } + } else { + // Prepare for a standard pointer move + foreach (TimelineViewBlockItem* clip_item, clips) { + Block* block = clip_item->block(); - // Create ghost - TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), - trim_mode, trim_overwrite_allowed); + if (block->type() == Block::kGap || block->type() == Block::kTransition) { + // Gaps cannot move, and we handle transitions further down + continue; + } - if (clicked_block_type == Block::kTransition) { - // Transition moves are always a slide - ghost->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); - } else { - // Set to default behavior - ghost->setData(TimelineViewGhostItem::kPointerToolMode, pointer_mode); + // Create ghost + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, + clip_item->Track(), + trim_mode); + Q_UNUSED(ghost) - // Include transitions (if any) - AddMovingTransitionsToClipGhost(block, clip_item->Track(), trim_mode, clips); + // Add transitions if this has any + TransitionBlock* opening_transition = TransitionBlock::GetBlockInTransition(block); + TransitionBlock* closing_transition = TransitionBlock::GetBlockOutTransition(block); + + if (opening_transition) { + TimelineViewGhostItem* ot_ghost = AddGhostFromBlock(opening_transition, + clip_item->Track(), + trim_mode); + Q_UNUSED(ot_ghost) + } + + if (closing_transition) { + TimelineViewGhostItem* cl_ghost = AddGhostFromBlock(closing_transition, + clip_item->Track(), + trim_mode); + Q_UNUSED(cl_ghost) + } } } - // If we slid the blocks, we must process them as such - if (clicked_block_type == Block::kTransition || pointer_mode == kSlide) { - ProcessGhostsForSliding(); - } - } else { // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) @@ -267,66 +340,88 @@ void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *cl } Block* block = clip_item->block(); - Timeline::MovementMode block_mode = trim_mode; - bool block_trim_overwrite_allowed = trim_overwrite_allowed; - - // Some tools interpret "gap trimming" as equivalent to resizing the adjacent block. In that - // scenario, we include the adjacent block instead. - if (block->type() == Block::kGap && !gap_trimming_allowed_) { - block = (trim_mode == Timeline::kTrimIn) ? block->previous() : block->next(); - block_mode = FlipTrimMode(trim_mode); - - // If there's no adjacent block, do nothing here - if (!block) { - continue; - } - } - - // For transitions, we create a rolling edit with the attached clip - if (block->type() == Block::kTransition) { - TransitionBlock* transition = static_cast(block); - TimelineViewGhostItem* g = nullptr; - - Block* previous = transition->previous(); - Block* next = transition->next(); - - if (block_mode == Timeline::kTrimIn - && previous - && (previous == transition->connected_out_block() || previous->type() == Block::kGap)) { - g = AddGhostFromBlock(previous, clip_item->Track(), Timeline::kTrimOut, true); - } else if (block_mode == Timeline::kTrimOut - && next - && (next == transition->connected_in_block() || next->type() == Block::kGap)) { - g = AddGhostFromBlock(next, clip_item->Track(), Timeline::kTrimIn, true); - } - - if (g) { - g->setData(TimelineViewGhostItem::kPointerToolMode, kRolling); - block_trim_overwrite_allowed = true; - } - } // Create ghost for this block - TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), block_mode, block_trim_overwrite_allowed); + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), trim_mode); - if (block->type() == Block::kTransition) { + // If this side of the clip has a transition, we treat it more like a slide for that + // transition than a trim/roll + bool treat_trim_as_slide = false; - // If this is a transition, set to rolling as above - ghost->setData(TimelineViewGhostItem::kPointerToolMode, kRolling); + if (block->type() == Block::kClip) { + // See if this clip has a transition attached, and move it with the trim if so + TransitionBlock* connected_transition; - } else { - - // For trimmed clips, we also "move" the transitions if any are attached - if (AddMovingTransitionsToClipGhost(block, clip_item->Track(), trim_mode, clips)) { - ghost->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + // Get appropriate transition for the side of the clip + if (trim_mode == Timeline::kTrimIn) { + connected_transition = TransitionBlock::GetBlockInTransition(block); } else { - ghost->setData(TimelineViewGhostItem::kPointerToolMode, kPointer); + connected_transition = TransitionBlock::GetBlockOutTransition(block); + } + + if (connected_transition) { + // We found a transition, we'll make this a "slide" action + TimelineViewGhostItem* transition_ghost = AddGhostFromBlock(connected_transition, clip_item->Track(), Timeline::kMove); + + // This will in effect be a slide with the transition moving between two other blocks + SetGhostToSlideMode(ghost); + SetGhostToSlideMode(transition_ghost); + treat_trim_as_slide = true; + + // Further processing will apply to this transition rather than the clip + block = connected_transition; } } - } - if (pointer_mode == kRolling) { - ProcessGhostsForRolling(); + // Standard pointer trimming in reality is a "roll" edit with an adjacent gap (one that may + // or may not exist already) + if (!dont_roll_trims) { + Block* adjacent = nullptr; + + // Determine which block is adjacent + if (trim_mode == Timeline::kTrimIn) { + adjacent = block->previous(); + } else { + adjacent = block->next(); + } + + // See if we can roll the adjacent or if we'll need to create our own gap + if (block->type() != Block::kGap + && !allow_nongap_rolling && adjacent && adjacent->type() != Block::kGap + && !(block->type() == Block::kTransition + && ((trim_mode == Timeline::kTrimIn && static_cast(block)->connected_out_block() == adjacent) + || (trim_mode == Timeline::kTrimOut && static_cast(block)->connected_in_block() == adjacent)))) { + adjacent = nullptr; + } + + Timeline::MovementMode flipped_mode = FlipTrimMode(trim_mode); + TimelineViewGhostItem* adjacent_ghost; + + if (adjacent) { + adjacent_ghost = AddGhostFromBlock(adjacent, clip_item->Track(), flipped_mode); + } else if (trim_mode == Timeline::kTrimIn || block->next()) { + rational null_ghost_pos = (trim_mode == Timeline::kTrimIn) ? block->in() : block->out(); + + adjacent_ghost = AddGhostFromNull(null_ghost_pos, null_ghost_pos, clip_item->Track(), flipped_mode); + } else { + adjacent_ghost = nullptr; + } + + // If we have an adjacent block (for any reason), this is a roll edit and the adjacent is + // expected to fill the remaining space (no gap needs to be created) + ghost->setData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast(adjacent)); + + if (adjacent_ghost) { + if (treat_trim_as_slide) { + // We're sliding a transition rather than a pure trim/roll + SetGhostToSlideMode(adjacent_ghost); + } else if (block->type() == Block::kGap) { + ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + } else { + adjacent_ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + } + } + } } } } @@ -418,16 +513,15 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) QList blocks_sliding; QList blocks_trimming; + // Sort ghosts depending on which ones are trimming, which are moving, and which are sliding foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->HasBeenAdjusted()) { Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - if (ghost->mode() == Timeline::kMove) { - if (ghost->data(TimelineViewGhostItem::kPointerToolMode) == kSlide) { - blocks_sliding.append({ghost, b}); - } else { - blocks_moving.append({ghost, b}); - } + if (ghost->data(TimelineViewGhostItem::kGhostIsSliding).toBool()) { + blocks_sliding.append({ghost, b}); + } else if (ghost->mode() == Timeline::kMove) { + blocks_moving.append({ghost, b}); } else if (Timeline::IsATrimMode(ghost->mode())) { blocks_trimming.append({ghost, b}); } @@ -441,46 +535,28 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) return; } - // See if we're duplicated because ALT is held (only moved blocks can duplicate) - bool duplicate_clips = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::AltModifier); - bool inserting = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::ControlModifier); - - // Slide info - QVector slide_info; - QUndoCommand* command = new QUndoCommand(); foreach (const GhostBlockPair& p, blocks_trimming) { TimelineViewGhostItem* ghost = p.ghost; - GhostMode m = static_cast(ghost->data(TimelineViewGhostItem::kPointerToolMode).toInt()); + if (!ghost->data(TimelineViewGhostItem::kTrimShouldBeIgnored).toBool()) { + // Must be an ordinary trim/roll + BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), + p.block, + ghost->AdjustedLength(), + ghost->mode(), + command); - switch (m) { - case kPointer: - case kRolling: - if (m != kRolling || ghost->mode() == drag_movement_mode()) { - BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), - p.block, - ghost->AdjustedLength(), - ghost->mode(), - command); - - if (m == kRolling) { - c->SetAllowNonGapTrimming(true); - } - } - break; - case kSlide: - slide_info.append({parent()->GetTrackFromReference(ghost->Track()), - p.block, - ghost->mode(), - ghost->AdjustedLength(), - ghost->Length()}); - break; + c->SetTrimIsARollEdit(ghost->data(TimelineViewGhostItem::kTrimIsARollEdit).toBool()); } } if (!blocks_moving.isEmpty()) { + // See if we're duplicated because ALT is held (only moved blocks can duplicate) + bool duplicate_clips = (event->GetModifiers() & Qt::AltModifier); + bool inserting = (event->GetModifiers() & Qt::ControlModifier); + // If we're not duplicating, "remove" the clips and replace them with gaps if (!duplicate_clips) { QList blocks_to_delete; @@ -525,17 +601,61 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) } if (!blocks_sliding.isEmpty()) { - foreach (const GhostBlockPair& p, blocks_sliding) { - slide_info.append({parent()->GetTrackFromReference(p.ghost->Track()), - p.block, - p.ghost->mode(), - p.ghost->GetAdjustedIn(), - p.ghost->In()}); - } - } + // Assume that the blocks are contiguous per track as set up in InitiateGhostsInternal() - if (!slide_info.isEmpty()) { - new TrackSlideCommand(slide_info, command); + // All we need to do is sort them by track and order them + QHash > slide_info; + QHash in_adjacents; + QHash out_adjacents; + rational movement; + + foreach (const GhostBlockPair& p, blocks_sliding) { + const TrackReference& track = p.ghost->Track(); + + switch (p.ghost->mode()) { + case Timeline::kNone: + break; + case Timeline::kMove: + { + // These all should have moved uniformly, so as long as this is set, it should be fine + movement = p.ghost->InAdjustment(); + + QList& blocks_on_this_track = slide_info[track]; + bool inserted = false; + + for (int i=0;iin() > p.block->in()) { + blocks_on_this_track.insert(i, p.block); + inserted = true; + break; + } + } + + if (!inserted) { + blocks_on_this_track.append(p.block); + } + break; + } + case Timeline::kTrimIn: + out_adjacents.insert(track, p.block); + break; + case Timeline::kTrimOut: + in_adjacents.insert(track, p.block); + break; + } + } + + if (!movement.isNull()) { + QHash >::const_iterator i; + for (i=slide_info.constBegin(); i!=slide_info.constEnd(); i++) { + new TrackSlideCommand(parent()->GetTrackFromReference(i.key()), + i.value(), + in_adjacents.value(i.key()), + out_adjacents.value(i.key()), + movement, + command); + } + } } Core::instance()->undo_stack()->pushIfHasChildren(command); @@ -562,22 +682,38 @@ Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(Timelin void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) { - InitiateDragInternal(clicked_item, trim_mode, kPointer, false); + InitiateDragInternal(clicked_item, trim_mode, false, false, false); } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed) +//#define HIDE_GAP_GHOSTS + +TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists) { + if (check_if_exists) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)) == block) { + return ghost; + } + } + } + TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, track, parent()->GetTrackY(track), parent()->GetTrackHeight(track)); - AddGhostInternal(ghost, mode, trim_overwrite_allowed); +#ifdef HIDE_GAP_GHOSTS + if (block->type() == Block::kGap) { + ghost->SetInvisible(true); + } +#endif + + AddGhostInternal(ghost, mode); return ghost; } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed) +TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode) { TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); @@ -586,15 +722,18 @@ TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const ratio ghost->SetTrack(track); ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); - AddGhostInternal(ghost, mode, trim_overwrite_allowed); +#ifdef HIDE_GAP_GHOSTS + ghost->SetInvisible(true); +#endif + + AddGhostInternal(ghost, mode); return ghost; } -void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode, bool trim_overwrite_allowed) +void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode) { ghost->SetMode(mode); - ghost->setData(TimelineViewGhostItem::kTrimOverwriteAllowed, trim_overwrite_allowed); // Prepare snap points (optimizes snapping for later) switch (mode) { @@ -631,93 +770,6 @@ bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, return true; } -struct TrackBlockListPair { - TrackReference track; - QList blocks; -}; - -void TimelineWidget::PointerTool::ProcessGhostsForSliding() -{ - // Sort blocks into tracks - QList blocks_per_track; - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - bool found = false; - - for (int i=0;iTrack()) { - blocks_per_track[i].blocks.append(b); - found = true; - break; - } - } - - if (!found) { - blocks_per_track.append({ghost->Track(), {b}}); - } - } - - // Make contiguous runs of blocks per each track - foreach (const TrackBlockListPair& p, blocks_per_track) { - // Blocks must be merged if any are non-adjacent - const TrackReference& track = p.track; - const QList& blocks = p.blocks; - - Block* earliest_block = blocks.first(); - Block* latest_block = blocks.first(); - - // Find the earliest and latest selected blocks - for (int j=1;jin() < earliest_block->in()) { - earliest_block = compare; - } - - if (compare->in() > latest_block->in()) { - latest_block = compare; - } - } - - // Add any blocks between these blocks that aren't already in the list - if (earliest_block != latest_block) { - Block* b = earliest_block; - while ((b = b->next()) != latest_block) { - if (!blocks.contains(b)) { - TimelineViewGhostItem* g = AddGhostFromBlock(b, track, Timeline::kMove, true); - g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); - } - } - } - - // Add surrounding blocks that will be trimming instead of moving - if (earliest_block->previous()) { - TimelineViewGhostItem* g = AddGhostFromBlock(earliest_block->previous(), track, Timeline::kTrimOut, true); - g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); - } - - if (latest_block->next()) { - TimelineViewGhostItem* g = AddGhostFromBlock(latest_block->next(), track, Timeline::kTrimIn, true); - g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); - } - } -} - -void TimelineWidget::PointerTool::ProcessGhostsForRolling() -{ - // For each ghost, we make an equivalent Ghost on the next/previous block - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous()) { - // Add an extra Ghost for the previous block - AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut, true); - } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next()) { - AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn, true); - } - } -} - bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, const TrackReference& track, Timeline::MovementMode movement, @@ -757,8 +809,9 @@ bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, if (!found) { TimelineViewGhostItem* transition_ghost = AddGhostFromBlock(transitions[i], track, - Timeline::kMove, false); - transition_ghost->setData(TimelineViewGhostItem::kPointerToolMode, kPointer); + Timeline::kMove); + + Q_UNUSED(transition_ghost) ret = true; } @@ -774,8 +827,6 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement) continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - rational earliest_in = RATIONAL_MIN; rational latest_in = ghost->Out(); @@ -783,23 +834,6 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement) latest_in -= parent()->timebase(); } - if (block) { - if (!ghost->data(TimelineViewGhostItem::kTrimOverwriteAllowed).toBool()) { - // Look for a Block in the way - Block* prev = block->previous(); - while (prev != nullptr) { - if (prev->type() == Block::kClip) { - earliest_in = qMax(earliest_in, prev->out()); - break; - } - prev = prev->previous(); - } - - // Limit in point at 0 on the timeline - earliest_in = qMax(rational(), earliest_in); - } - } - // Clamp adjusted value between the earliest and latest values rational adjusted = ghost->In() + movement; rational clamped = clamp(adjusted, earliest_in, latest_in); @@ -819,8 +853,6 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - // Determine earliest and latest out points rational earliest_out = ghost->In(); @@ -830,21 +862,6 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) rational latest_out = RATIONAL_MAX; - // Ripple tool creates block-less ghosts and creates gaps with them later - if (block) { - if (!ghost->data(TimelineViewGhostItem::kTrimOverwriteAllowed).toBool()) { - // Determine if there's a block in the way - Block* next = block->next(); - while (next != nullptr) { - if (next->type() == Block::kClip) { - latest_out = qMin(latest_out, next->in()); - break; - } - next = next->next(); - } - } - } - // Clamp adjusted value between the earliest and latest values rational adjusted = ghost->Out() + movement; rational clamped = clamp(adjusted, earliest_out, latest_out); diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index a03303b1a..919c0d908 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -35,7 +35,7 @@ TimelineWidget::RippleTool::RippleTool(TimelineWidget* parent) : void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - InitiateDragInternal(clicked_item, trim_mode, kPointer, true); + InitiateDragInternal(clicked_item, trim_mode, true, true, false); if (parent()->ghost_items_.isEmpty()) { return; @@ -85,16 +85,16 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite if (block_before_ripple->type() == Block::kGap) { // If this Block is already a Gap, ghost it now - ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode, true); + ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode); } else if (block_before_ripple->next()) { // Assuming this block is NOT at the end of the track (i.e. next != null) // We're going to create a gap after it. If next is a gap, we can just use that if (block_before_ripple->next()->type() == Block::kGap) { - ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode, true); + ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode); } else { // If next is NOT a gap, we'll need to create one, for which we'll use a null ghost - ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode, true); + ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); } } diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index 8f9f132c9..2aa55e43e 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -35,7 +35,7 @@ TimelineWidget::RollingTool::RollingTool(TimelineWidget* parent) : void TimelineWidget::RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - InitiateDragInternal(clicked_item, trim_mode, kRolling, true); + InitiateDragInternal(clicked_item, trim_mode, false, true, false); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index f9bd8f357..c64d9ec43 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -36,7 +36,7 @@ TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : void TimelineWidget::SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - InitiateDragInternal(clicked_item, trim_mode, kSlide, true); + InitiateDragInternal(clicked_item, trim_mode, false, true, true); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 502755acb..04ce25b32 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -80,23 +80,6 @@ rational TimelineWidget::Tool::ValidateTimeMovement(rational movement) continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (block && block->type() == Block::kTransition) { - TransitionBlock* transition = static_cast(block); - - // Dual transitions are only allowed to move so that neither of their offsets are < 0 - if (transition->connected_in_block() && transition->connected_out_block()) { - if (movement > transition->out_offset()) { - movement = transition->out_offset(); - } - - if (movement < -transition->in_offset()) { - movement = -transition->in_offset(); - } - } - } - // Prevents any ghosts from going below 0:00:00 time if (ghost->In() + movement < 0) { movement = -ghost->In(); diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index bbad9ffcd..d3603911e 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -842,7 +842,7 @@ BlockTrimCommand::BlockTrimCommand(TrackOutput* track, Block *block, rational ne adjacent_(nullptr), we_created_adjacent_(false), we_deleted_adjacent_(false), - allow_nongap_trimming_(false) + trim_is_a_roll_edit_(false) { } @@ -872,7 +872,7 @@ void BlockTrimCommand::redo_internal() if (trim_diff > rational()) { // If trimming SHORTER, we'll need to create/modify a gap - if (adjacent_ && (adjacent_->type() == Block::kGap || allow_nongap_trimming_)) { + if (adjacent_ && (adjacent_->type() == Block::kGap || trim_is_a_roll_edit_)) { // A gap (or equivalent) exists, simply increase the size of it if (mode_ == Timeline::kTrimIn) { @@ -1134,15 +1134,22 @@ void TrackReplaceBlockWithGapCommand::undo_internal() track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } -TrackSlideCommand::TrackSlideCommand(const QVector &blocks, QUndoCommand *parent) : +TrackSlideCommand::TrackSlideCommand(TrackOutput* track, const QList& moving_blocks, Block *in_adjacent, Block *out_adjacent, const rational& movement, QUndoCommand* parent) : UndoCommand(parent), - blocks_(blocks) + track_(track), + blocks_(moving_blocks), + movement_(movement), + we_created_in_adjacent_(false), + in_adjacent_(in_adjacent), + we_created_out_adjacent_(false), + out_adjacent_(out_adjacent) { + Q_ASSERT(!movement_.isNull()); } Project *TrackSlideCommand::GetRelevantProject() const { - return static_cast(blocks_.first().track->parent())->project(); + return static_cast(track_->parent())->project(); } void TrackSlideCommand::redo_internal() @@ -1157,106 +1164,98 @@ void TrackSlideCommand::undo_internal() void TrackSlideCommand::slide_internal(bool undo) { - QMap invalidate_ranges; - // Make sure all movement blocks' old positions are invalidated - foreach (const BlockSlideInfo& info, blocks_) { - if (info.mode == Timeline::kMove) { - invalidate_ranges.insert(info.track, - TimeRange(info.block->in(), - info.block->out())); - } - } + TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); - // Perform trims - int iterator = undo ? blocks_.size() - 1 : 0; - int limit = undo ? -1 : blocks_.size(); - - while (iterator != limit) { - const BlockSlideInfo& info = blocks_.at(iterator); - - info.track->BeginOperation(); - - if (info.mode == Timeline::kTrimIn || info.mode == Timeline::kTrimOut) { - qDebug() << "Trimming block as part of a slide operation:" << info.old_time << info.new_time; - if (info.new_time.isNull()) { - // Assume this was a gap that was reduced to nothing - // On undo, restore it, on redo, remove it - if (undo) { - Block* next = removed_block_next.takeLast(); - static_cast(info.track->parent())->AddNode(info.block); - - if (next) { - info.track->InsertBlockBefore(info.block, next); - } else { - info.track->AppendBlock(info.block); - } - } else { - removed_block_next.append(info.block->next()); - info.track->RippleRemoveBlock(info.block); - TakeNodeFromParentGraph(info.block, &memory_manager_); - } - } else { - rational new_len = undo ? info.old_time : info.new_time; - - if (info.mode == Timeline::kTrimIn) { - info.block->set_length_and_media_in(new_len); - } else { - info.block->set_length_and_media_out(new_len); - } - } - } else if (!undo - && info.mode == Timeline::kMove - && !info.block->previous() - && info.new_time > info.old_time) { - // If this is a moving block and there was nothing before it to offset its time correctly, - // insert a gap here - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(info.new_time); - static_cast(info.block->parent())->AddNode(gap); - info.track->PrependBlock(gap); - added_gaps_.append(gap); - } - - info.track->EndOperation(); - - if (undo) { - iterator--; - } else { - iterator++; - } - } + track_->BeginOperation(); if (undo) { - // If undoing, remove added gaps - foreach (GapBlock* gap, added_gaps_) { - TrackOutput* track = TrackOutput::TrackFromBlock(gap); - track->BeginOperation(); + // Undo code + if (we_created_in_adjacent_) { + // This is a gap we made, we can just delete it entirely + track_->RippleRemoveBlock(in_adjacent_); + delete TakeNodeFromParentGraph(in_adjacent_); + we_created_in_adjacent_ = false; + in_adjacent_ = nullptr; + } else if (in_adjacent_->parent() == &memory_manager_) { + // This is a gap we removed, we can re-insert it now + static_cast(track_->parent())->AddNode(in_adjacent_); + track_->InsertBlockBefore(in_adjacent_, blocks_.first()); + } else { + // We must have just resized this block + in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); + } - track->RippleRemoveBlock(gap); - delete TakeNodeFromParentGraph(gap); + if (we_created_out_adjacent_) { + // This is a gap we made, we can just delete it entirely + track_->RippleRemoveBlock(out_adjacent_); + delete TakeNodeFromParentGraph(out_adjacent_); + we_created_out_adjacent_ = false; + out_adjacent_ = nullptr; + } else if (out_adjacent_) { + // We may not have created an out adjacent if this was the last clip in the track so we have + // to check here + if (out_adjacent_->parent() == &memory_manager_) { + // This is a gap we removed, we can re-insert it now + static_cast(track_->parent())->AddNode(out_adjacent_); + track_->InsertBlockAfter(out_adjacent_, blocks_.last()); + } else { + // We must have just resized this block + out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); + } + } - track->EndOperation(); + } else { + + // Redo code + if (!in_adjacent_) { + // For any slide operation to have occurred at all with no in_adjacent, a gap will need to + // be created + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(movement_); + static_cast(track_->parent())->AddNode(gap); + track_->InsertBlockBefore(gap, blocks_.first()); + we_created_in_adjacent_ = true; + in_adjacent_ = gap; + } else if (-movement_ == in_adjacent_->length()) { + // Remove in adjacent entirely + track_->RippleRemoveBlock(in_adjacent_); + TakeNodeFromParentGraph(in_adjacent_, &memory_manager_); + } else { + // Resize in adjacent + in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); + } + + if (!out_adjacent_) { + // For any slide operation to have occurred at all with no out_adjacent, a gap will need to + // be created UNLESS this is at the end of the track already + if (blocks_.last()->next()) { + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(-movement_); + static_cast(track_->parent())->AddNode(gap); + track_->InsertBlockAfter(gap, blocks_.last()); + we_created_out_adjacent_ = true; + out_adjacent_ = gap; + } + } else if (movement_ == out_adjacent_->length()) { + // Remove out adjacent entirely + track_->RippleRemoveBlock(out_adjacent_); + TakeNodeFromParentGraph(out_adjacent_, &memory_manager_); + } else { + // Resize out adjacent + out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); } - added_gaps_.clear(); } + track_->EndOperation(); + // Make sure all movement blocks' new positions are invalidated - foreach (const BlockSlideInfo& info, blocks_) { - if (info.mode == Timeline::kMove) { - TimeRange& range = invalidate_ranges[info.track]; + invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), + qMax(invalidate_range.out(), blocks_.last()->out())); - range.set_range(qMin(range.in(), info.block->in()), - qMax(range.out(), info.block->out())); - } - } - - QMap::const_iterator i; - for (i=invalidate_ranges.constBegin(); i!=invalidate_ranges.constEnd(); i++) { - i.key()->InvalidateCache(i.value(), i.key()->block_input(), i.key()->block_input()); - } + track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } TrackListRippleRemoveAreaCommand::TrackListRippleRemoveAreaCommand(TrackList *list, rational in, rational out, QUndoCommand *parent) : diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index 14ada4d5a..4193bb66d 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -71,9 +71,9 @@ public: virtual Project* GetRelevantProject() const override; - void SetAllowNonGapTrimming(bool e) + void SetTrimIsARollEdit(bool e) { - allow_nongap_trimming_ = e; + trim_is_a_roll_edit_ = e; } protected: @@ -91,7 +91,7 @@ private: bool we_created_adjacent_; bool we_deleted_adjacent_; - bool allow_nongap_trimming_; + bool trim_is_a_roll_edit_; QObject memory_manager_; @@ -554,15 +554,7 @@ private: class TrackSlideCommand : public UndoCommand { public: - struct BlockSlideInfo { - TrackOutput* track; - Block* block; - Timeline::MovementMode mode; - rational new_time; - rational old_time; - }; - - TrackSlideCommand(const QVector& blocks, QUndoCommand* parent = nullptr); + TrackSlideCommand(TrackOutput* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement, QUndoCommand* parent = nullptr); virtual Project* GetRelevantProject() const override; @@ -573,9 +565,14 @@ protected: private: void slide_internal(bool undo); - QVector blocks_; - QList added_gaps_; - QList removed_block_next; + TrackOutput* track_; + QList blocks_; + rational movement_; + + bool we_created_in_adjacent_; + Block* in_adjacent_; + bool we_created_out_adjacent_; + Block* out_adjacent_; QObject memory_manager_; diff --git a/app/widget/timelinewidget/view/timelineviewghostitem.h b/app/widget/timelinewidget/view/timelineviewghostitem.h index f61cb2030..160cdb4fb 100644 --- a/app/widget/timelinewidget/view/timelineviewghostitem.h +++ b/app/widget/timelinewidget/view/timelineviewghostitem.h @@ -39,8 +39,9 @@ public: kAttachedBlock, kReferenceBlock, kAttachedFootage, - kTrimOverwriteAllowed, - kPointerToolMode + kGhostIsSliding, + kTrimIsARollEdit, + kTrimShouldBeIgnored }; TimelineViewGhostItem(QGraphicsItem* parent = nullptr);