diff --git a/app/widget/timelineview/CMakeLists.txt b/app/widget/timelineview/CMakeLists.txt index db2aaaa44..d3327fc2a 100644 --- a/app/widget/timelineview/CMakeLists.txt +++ b/app/widget/timelineview/CMakeLists.txt @@ -24,8 +24,8 @@ set(OLIVE_SOURCES widget/timelineview/timelineview.cpp widget/timelineview/timelineviewrect.h widget/timelineview/timelineviewrect.cpp - widget/timelineview/timelineviewclipitem.h - widget/timelineview/timelineviewclipitem.cpp + widget/timelineview/timelineviewblockitem.h + widget/timelineview/timelineviewblockitem.cpp widget/timelineview/timelineviewghostitem.h widget/timelineview/timelineviewghostitem.cpp widget/timelineview/timelineviewplayheaditem.h diff --git a/app/widget/timelineview/timelineview.cpp b/app/widget/timelineview/timelineview.cpp index dd741bf96..eaf47a47c 100644 --- a/app/widget/timelineview/timelineview.cpp +++ b/app/widget/timelineview/timelineview.cpp @@ -63,29 +63,24 @@ void TimelineView::AddBlock(Block *block, int track) { switch (block->type()) { case Block::kClip: + case Block::kGap: { - TimelineViewClipItem* clip_item = new TimelineViewClipItem(); - ClipBlock* clip = static_cast(block); + TimelineViewBlockItem* clip_item = new TimelineViewBlockItem(); // Set up clip with view parameters (clip item will automatically size its rect accordingly) - clip_item->SetClip(clip); + clip_item->SetBlock(block); clip_item->SetY(GetTrackY(track)); clip_item->SetHeight(GetTrackHeight(track)); clip_item->SetScale(scale_); clip_item->SetTrack(track); // Add to list of clip items that can be iterated through - clip_items_.insert(clip, clip_item); + clip_items_.insert(block, clip_item); // Add item to graphics scene scene_.addItem(clip_item); - connect(clip, SIGNAL(Refreshed()), this, SLOT(BlockChanged())); - break; - } - case Block::kGap: - { - clip_items_.insert(block, nullptr); + connect(block, SIGNAL(Refreshed()), this, SLOT(BlockChanged())); break; } case Block::kEnd: @@ -131,6 +126,10 @@ void TimelineView::SetScale(const double &scale) } } + foreach (TimelineViewGhostItem* ghost, ghost_items_) { + ghost->SetScale(scale_); + } + playhead_line_->SetScale(scale_); } diff --git a/app/widget/timelineview/timelineview.h b/app/widget/timelineview/timelineview.h index 7c399b863..c117e8ff0 100644 --- a/app/widget/timelineview/timelineview.h +++ b/app/widget/timelineview/timelineview.h @@ -29,7 +29,7 @@ #include "node/block/clip/clip.h" #include "node/output/timeline/timeline.h" -#include "timelineviewclipitem.h" +#include "timelineviewblockitem.h" #include "timelineviewghostitem.h" #include "timelineviewplayheaditem.h" @@ -101,6 +101,8 @@ private: TimelineView* parent(); + static olive::timeline::MovementMode FlipTrimMode(const olive::timeline::MovementMode& trim_mode); + protected: /** * @brief Convert a integer screen point to a float scene point @@ -133,22 +135,6 @@ private: */ int ValidateTrackMovement(int movement, const QVector ghosts); - /** - * @brief Validates Ghosts that are getting their in points trimmed - * - * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no - * Ghost's length becomes 0 or negative. - */ - rational ValidateInTrimming(rational movement, const QVector ghosts); - - /** - * @brief Validates Ghosts that are getting their out points trimmed - * - * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no - * Ghost's length becomes 0 or negative. - */ - rational ValidateOutTrimming(rational movement, const QVector ghosts); - enum SnapPoints { kSnapToClips = 0x1, kSnapToPlayhead = 0x2, @@ -182,15 +168,41 @@ private: protected: void SetMovementAllowed(bool allowed); virtual void MouseReleaseInternal(QMouseEvent *event); - virtual rational FrameValidateInternal(rational time_movement, QVector); + virtual rational FrameValidateInternal(rational time_movement, const QVector &ghosts); + + virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming); + + TimelineViewGhostItem* AddGhostFromBlock(Block *block, int track, olive::timeline::MovementMode mode); + + TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, int track, olive::timeline::MovementMode mode); + + /** + * @brief Validates Ghosts that are getting their in points trimmed + * + * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no + * Ghost's length becomes 0 or negative. + */ + rational ValidateInTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); + + /** + * @brief Validates Ghosts that are getting their out points trimmed + * + * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no + * Ghost's length becomes 0 or negative. + */ + rational ValidateOutTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); private: void InitiateDrag(const QPoint &mouse_pos); void ProcessDrag(const QPoint &mouse_pos); - QList GetSelectedClips(); + void AddGhostInternal(TimelineViewGhostItem* ghost, olive::timeline::MovementMode mode); - bool IsClipTrimmable(TimelineViewClipItem* clip, - const QList& items, + QList GetSelectedClips(); + + bool IsClipTrimmable(TimelineViewBlockItem* clip, + const QList& items, const olive::timeline::MovementMode& mode); int track_start_; @@ -225,8 +237,12 @@ private: public: RippleTool(TimelineView* parent); protected: - virtual void MouseReleaseInternal(QMouseEvent *event); - virtual rational FrameValidateInternal(rational time_movement, QVector ghosts); + virtual void MouseReleaseInternal(QMouseEvent *event) override; + virtual rational FrameValidateInternal(rational time_movement, const QVector& ghosts) override; + + virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming) override; }; class HandTool : public Tool diff --git a/app/widget/timelineview/timelineviewclipitem.cpp b/app/widget/timelineview/timelineviewblockitem.cpp similarity index 76% rename from app/widget/timelineview/timelineviewclipitem.cpp rename to app/widget/timelineview/timelineviewblockitem.cpp index 709cb36a7..902a99cf6 100644 --- a/app/widget/timelineview/timelineviewclipitem.cpp +++ b/app/widget/timelineview/timelineviewblockitem.cpp @@ -18,7 +18,7 @@ ***/ -#include "timelineviewclipitem.h" +#include "timelineviewblockitem.h" #include #include @@ -26,44 +26,48 @@ #include #include -TimelineViewClipItem::TimelineViewClipItem(QGraphicsItem* parent) : +TimelineViewBlockItem::TimelineViewBlockItem(QGraphicsItem* parent) : TimelineViewRect(parent), - clip_(nullptr) + block_(nullptr) { setBrush(Qt::white); setFlag(QGraphicsItem::ItemIsSelectable, true); } -ClipBlock *TimelineViewClipItem::clip() +Block *TimelineViewBlockItem::block() { - return clip_; + return block_; } -void TimelineViewClipItem::SetClip(ClipBlock *clip) +void TimelineViewBlockItem::SetBlock(Block *block) { - clip_ = clip; + block_ = block; UpdateRect(); } -void TimelineViewClipItem::UpdateRect() +void TimelineViewBlockItem::UpdateRect() { - if (clip_ == nullptr) { + if (block_ == nullptr) { return; } - double item_left = TimeToScreenCoord(clip_->in()); - double item_width = TimeToScreenCoord(clip_->length()); + double item_left = TimeToScreenCoord(block_->in()); + double item_width = TimeToScreenCoord(block_->length()); // -1 on width and height so we don't overlap any adjacent clips setRect(0, y_, item_width - 1, height_ - 1); setPos(item_left, 0.0); } -void TimelineViewClipItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(widget) + if (block_ == nullptr || block_->type() == Block::kGap) { + return; + } + QLinearGradient grad; grad.setStart(0, rect().top()); grad.setFinalStop(0, rect().bottom()); diff --git a/app/widget/timelineview/timelineviewclipitem.h b/app/widget/timelineview/timelineviewblockitem.h similarity index 85% rename from app/widget/timelineview/timelineviewclipitem.h rename to app/widget/timelineview/timelineviewblockitem.h index 6ee20ab3c..50c9a8ccb 100644 --- a/app/widget/timelineview/timelineviewclipitem.h +++ b/app/widget/timelineview/timelineviewblockitem.h @@ -27,13 +27,13 @@ /** * @brief A graphical representation of a ClipBlock */ -class TimelineViewClipItem : public TimelineViewRect +class TimelineViewBlockItem : public TimelineViewRect { public: - TimelineViewClipItem(QGraphicsItem* parent = nullptr); + TimelineViewBlockItem(QGraphicsItem* parent = nullptr); - ClipBlock* clip(); - void SetClip(ClipBlock* clip); + Block* block(); + void SetBlock(Block *block); virtual void UpdateRect() override; @@ -41,7 +41,7 @@ protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; private: - ClipBlock* clip_; + Block* block_; }; diff --git a/app/widget/timelineview/timelineviewghostitem.cpp b/app/widget/timelineview/timelineviewghostitem.cpp index c8d7e92a4..5fb050e41 100644 --- a/app/widget/timelineview/timelineviewghostitem.cpp +++ b/app/widget/timelineview/timelineviewghostitem.cpp @@ -28,30 +28,46 @@ TimelineViewGhostItem::TimelineViewGhostItem(QGraphicsItem *parent) : out_adj_(0), track_adj_(0), stream_(nullptr), - mode_(olive::timeline::kNone) + mode_(olive::timeline::kNone), + can_have_zero_length_(false) { - setBrush(Qt::NoBrush); - setPen(QPen(Qt::yellow, 2)); // FIXME: Make customizable via CSS + SetInvisible(false); } -TimelineViewGhostItem *TimelineViewGhostItem::FromClip(TimelineViewClipItem *clip_item) +TimelineViewGhostItem *TimelineViewGhostItem::FromBlock(Block *block, int track, int y, int height) { TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); - ghost->SetY(clip_item->Y()); - ghost->SetHeight(clip_item->Height()); - ghost->SetTrack(clip_item->Track()); - ghost->setPos(clip_item->pos()); + ghost->SetIn(block->in()); + ghost->SetOut(block->out()); + ghost->SetTrack(track); + ghost->SetY(y); + ghost->SetHeight(height); + ghost->setData(kAttachedBlock, Node::PtrToValue(block)); - ClipBlock* clip = clip_item->clip(); - - ghost->SetIn(clip->in()); - ghost->SetOut(clip->out()); - ghost->setData(0, Node::PtrToValue(clip)); + if (block->type() == Block::kGap) { + ghost->can_have_zero_length_ = true; + } return ghost; } +bool TimelineViewGhostItem::CanHaveZeroLength() +{ + return can_have_zero_length_; +} + +void TimelineViewGhostItem::SetInvisible(bool invisible) +{ + setBrush(Qt::NoBrush); + + if (invisible) { + setPen(Qt::NoPen); + } else { + setPen(QPen(Qt::yellow, 2)); // FIXME: Make customizable via CSS + } +} + const rational &TimelineViewGhostItem::In() const { return in_; diff --git a/app/widget/timelineview/timelineviewghostitem.h b/app/widget/timelineview/timelineviewghostitem.h index 5bb62932b..b33e3a13e 100644 --- a/app/widget/timelineview/timelineviewghostitem.h +++ b/app/widget/timelineview/timelineviewghostitem.h @@ -25,7 +25,7 @@ #include "common/timelinecommon.h" #include "project/item/footage/footage.h" -#include "timelineviewclipitem.h" +#include "timelineviewblockitem.h" #include "timelineviewrect.h" /** @@ -34,11 +34,19 @@ class TimelineViewGhostItem : public TimelineViewRect { public: - + enum DataType { + kAttachedBlock, + kReferenceBlock, + kAttachedFootage + }; TimelineViewGhostItem(QGraphicsItem* parent = nullptr); - static TimelineViewGhostItem* FromClip(TimelineViewClipItem* clip_item); + static TimelineViewGhostItem* FromBlock(Block *block, int track, int y, int height); + + bool CanHaveZeroLength(); + + void SetInvisible(bool invisible); const rational& In() const; const rational& Out() const; @@ -80,6 +88,8 @@ private: StreamPtr stream_; olive::timeline::MovementMode mode_; + + bool can_have_zero_length_; }; #endif // TIMELINEVIEWGHOSTITEM_H diff --git a/app/widget/timelineview/tool/import.cpp b/app/widget/timelineview/tool/import.cpp index f84d18131..630ee18b3 100644 --- a/app/widget/timelineview/tool/import.cpp +++ b/app/widget/timelineview/tool/import.cpp @@ -95,7 +95,7 @@ void TimelineView::ImportTool::DragEnter(QDragEnterEvent *event) snap_points_.append(ghost->In()); snap_points_.append(ghost->Out()); - ghost->setData(0, QVariant::fromValue(stream)); + ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); ghost->SetMode(olive::timeline::kMove); parent()->AddGhost(ghost); @@ -179,7 +179,7 @@ void TimelineView::ImportTool::DragDrop(QDropEvent *event) opacity->setParent(&node_memory_manager); clip->set_length(ghost->Length()); - media->SetFootage(ghost->data(0).value()); + media->SetFootage(ghost->data(TimelineViewGhostItem::kAttachedFootage).value()); NodeParam::ConnectEdge(opacity->texture_output(), clip->texture_input()); NodeParam::ConnectEdge(media->texture_output(), opacity->texture_input()); diff --git a/app/widget/timelineview/tool/pointer.cpp b/app/widget/timelineview/tool/pointer.cpp index 5bdc13933..922340519 100644 --- a/app/widget/timelineview/tool/pointer.cpp +++ b/app/widget/timelineview/tool/pointer.cpp @@ -22,6 +22,7 @@ #include +#include "common/clamp.h" #include "common/range.h" #include "core.h" #include "node/block/gap/gap.h" @@ -114,7 +115,7 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) // Since all the ghosts will be leaving their old position in some way, we replace all of them with gaps here so the // entire timeline isn't disrupted in the process foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(0)); + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); // Replace old Block with a new Gap GapBlock* gap = new GapBlock(); @@ -127,8 +128,9 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) // Now we place the clips back in the timeline where the user moved them. It's legal for them to overwrite parts or // all of the gaps we inserted earlier foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(0)); + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + // Normal blocks work in conjunction with the gap made above if (ghost->mode() == olive::timeline::kTrimIn || ghost->mode() == olive::timeline::kTrimOut) { // If we were trimming, we'll need to change the length @@ -144,12 +146,12 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) } } -rational TimelineView::PointerTool::FrameValidateInternal(rational time_movement, QVector) +rational TimelineView::PointerTool::FrameValidateInternal(rational time_movement, const QVector& ghosts) { // Default behavior is to validate all movement and trimming - time_movement = ValidateFrameMovement(time_movement, parent()->ghost_items_); - time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_); - time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_); + time_movement = ValidateFrameMovement(time_movement, ghosts); + time_movement = ValidateInTrimming(time_movement, ghosts, true); + time_movement = ValidateOutTrimming(time_movement, ghosts, true); return time_movement; } @@ -160,7 +162,7 @@ void TimelineView::PointerTool::InitiateDrag(const QPoint& mouse_pos) drag_start_ = GetScenePos(mouse_pos); // Get the item that was clicked - TimelineViewClipItem* clicked_item = dynamic_cast(GetItemAtScenePos(drag_start_)); + TimelineViewBlockItem* clicked_item = dynamic_cast(GetItemAtScenePos(drag_start_)); // We only initiate a pointer drag if the user actually dragged an item, otherwise if they dragged on empty space // QGraphicsView default behavior would initiate a rubberband drag @@ -187,61 +189,14 @@ void TimelineView::PointerTool::InitiateDrag(const QPoint& mouse_pos) trim_mode = olive::timeline::kMove; } + // Gaps can't be moved, only trimmed + if (clicked_item->block()->type() == Block::kGap && trim_mode == olive::timeline::kMove) { + trim_mode = olive::timeline::kNone; + } + // Make sure we can actually perform an action here if (trim_mode != olive::timeline::kNone) { - // Convert selected items list to clips list - QList clips = GetSelectedClips(); - - // If trimming multiple clips, we only trim the earliest in each track (trimming in) or the latest in each track - // (trimming out). If the current clip is NOT one of these, we only trim it. - bool multitrim_enabled = true; - - // Determine if the clicked item is the earliest/latest in the track for in/out trimming respectively - if (trim_mode == olive::timeline::kTrimIn - || trim_mode == olive::timeline::kTrimOut) { - multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); - } - - // For each selected item, create a "ghost", a visual representation of the action before it gets performed - foreach (TimelineViewClipItem* clip_item, clips) { - // Determine correct mode for ghost - // - // Movement is indiscriminate, all the ghosts can be set to do this, however trimming is limited to one block - // PER TRACK - - bool include_this_clip = true; - - if (clip_item != clicked_item - && (trim_mode == olive::timeline::kTrimIn || trim_mode == olive::timeline::kTrimOut)) { - include_this_clip = multitrim_enabled ? IsClipTrimmable(clip_item, clips, trim_mode) : false; - } - - if (include_this_clip) { - TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromClip(clip_item); - - ghost->SetScale(parent()->scale_); - ghost->SetMode(trim_mode); - - // Prepare snap points (optimizes snapping for later) - switch (trim_mode) { - case olive::timeline::kMove: - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); - break; - case olive::timeline::kTrimIn: - snap_points_.append(ghost->In()); - break; - case olive::timeline::kTrimOut: - snap_points_.append(ghost->Out()); - break; - default: - break; - } - - parent()->ghost_items_.append(ghost); - parent()->scene_.addItem(ghost); - } - } + InitiateGhosts(clicked_item, trim_mode, false); } } } @@ -259,15 +214,15 @@ void TimelineView::PointerTool::ProcessDrag(const QPoint &mouse_pos) // Determine frame movement rational time_movement = parent()->SceneToTime(movement.x()); - // Validate movement (enforce all ghosts moving in legal ways) - time_movement = FrameValidateInternal(time_movement, parent()->ghost_items_); - track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); - // Perform snapping if enabled (adjusts time_movement if it's close to any potential snap points) if (olive::core.snapping()) { SnapPoint(snap_points_, &time_movement); } + // Validate movement (enforce all ghosts moving in legal ways) + time_movement = FrameValidateInternal(time_movement, parent()->ghost_items_); + track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); + // Perform movement foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { switch (ghost->mode()) { @@ -295,14 +250,119 @@ void TimelineView::PointerTool::ProcessDrag(const QPoint &mouse_pos) } } -QList TimelineView::PointerTool::GetSelectedClips() +void TimelineView::PointerTool::InitiateGhosts(TimelineViewBlockItem* clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming) +{ + // Convert selected items list to clips list + QList clips = GetSelectedClips(); + + // If trimming multiple clips, we only trim the earliest in each track (trimming in) or the latest in each track + // (trimming out). If the current clip is NOT one of these, we only trim it. + bool multitrim_enabled = true; + + // Determine if the clicked item is the earliest/latest in the track for in/out trimming respectively + if (trim_mode == olive::timeline::kTrimIn + || trim_mode == olive::timeline::kTrimOut) { + multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); + } + + // For each selected item, create a "ghost", a visual representation of the action before it gets performed + foreach (TimelineViewBlockItem* clip_item, clips) { + // Determine correct mode for ghost + // + // Movement is indiscriminate, all the ghosts can be set to do this, however trimming is limited to one block + // PER TRACK + + bool include_this_clip = true; + + if (clip_item != clicked_item + && (trim_mode == olive::timeline::kTrimIn || trim_mode == olive::timeline::kTrimOut)) { + include_this_clip = multitrim_enabled ? IsClipTrimmable(clip_item, clips, trim_mode) : false; + } + + if (include_this_clip) { + Block* block = clip_item->block(); + olive::timeline::MovementMode block_mode = trim_mode; + + if (block->type() == Block::kGap && !allow_gap_trimming) { + if (trim_mode == olive::timeline::kTrimIn) { + // Trim the previous clip's out point instead + block = block->previous(); + } else { + // Assume kTrimOut + block = block->next(); + } + block_mode = FlipTrimMode(trim_mode); + } + + if (block != nullptr) { + AddGhostFromBlock(block, clip_item->Track(), block_mode); + } + } + } +} + +TimelineViewGhostItem* TimelineView::PointerTool::AddGhostFromBlock(Block* block, int track, olive::timeline::MovementMode mode) +{ + TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, + track, + parent()->GetTrackY(track), + parent()->GetTrackHeight(track)); + + AddGhostInternal(ghost, mode); + + return ghost; +} + +TimelineViewGhostItem* TimelineView::PointerTool::AddGhostFromNull(const rational &in, const rational &out, int track, olive::timeline::MovementMode mode) +{ + TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); + + ghost->SetIn(in); + ghost->SetOut(out); + ghost->SetTrack(track); + ghost->SetY(parent()->GetTrackY(track)); + ghost->SetHeight(parent()->GetTrackHeight(track)); + + AddGhostInternal(ghost, mode); + + return ghost; +} + +void TimelineView::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, olive::timeline::MovementMode mode) +{ + ghost->SetScale(parent()->scale_); + ghost->SetMode(mode); + + // Prepare snap points (optimizes snapping for later) + switch (mode) { + case olive::timeline::kMove: + snap_points_.append(ghost->In()); + snap_points_.append(ghost->Out()); + break; + case olive::timeline::kTrimIn: + snap_points_.append(ghost->In()); + break; + case olive::timeline::kTrimOut: + snap_points_.append(ghost->Out()); + break; + default: + break; + } + + parent()->ghost_items_.append(ghost); + parent()->scene_.addItem(ghost); +} + +QList TimelineView::PointerTool::GetSelectedClips() { QList selected_items = parent()->scene_.selectedItems(); // Convert selected items list to clips list - QList clips; + QList clips; foreach (QGraphicsItem* item, selected_items) { - TimelineViewClipItem* clip_cast = dynamic_cast(item); + TimelineViewBlockItem* clip_cast = dynamic_cast(item); if (clip_cast != nullptr) { clips.append(clip_cast); @@ -312,18 +372,107 @@ QList TimelineView::PointerTool::GetSelectedClips() return clips; } -bool TimelineView::PointerTool::IsClipTrimmable(TimelineViewClipItem* clip, - const QList& items, +bool TimelineView::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, + const QList& items, const olive::timeline::MovementMode& mode) { - foreach (TimelineViewClipItem* compare, items) { + foreach (TimelineViewBlockItem* compare, items) { if (clip->Track() == compare->Track() && clip != compare - && ((compare->clip()->in() < clip->clip()->in() && mode == olive::timeline::kTrimIn) - || (compare->clip()->out() > clip->clip()->out() && mode == olive::timeline::kTrimOut))) { + && ((compare->block()->in() < clip->block()->in() && mode == olive::timeline::kTrimIn) + || (compare->block()->out() > clip->block()->out() && mode == olive::timeline::kTrimOut))) { return false; } } return true; } + +rational TimelineView::PointerTool::ValidateInTrimming(rational movement, + const QVector ghosts, + bool prevent_overwriting) +{ + foreach (TimelineViewGhostItem* ghost, ghosts) { + if (ghost->mode() != olive::timeline::kTrimIn) { + continue; + } + + Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + // Determine the earliest in point this block could have + rational earliest_in = qMax(rational(0), block->in() - block->media_in()); + + if (prevent_overwriting) { + // 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(); + } + } + + // Determine the latest point this block could have + rational latest_in = ghost->Out(); + + if (!ghost->CanHaveZeroLength()) { + latest_in -= parent()->timebase_; + } + + // Clamp adjusted value between the earliest and latest values + rational adjusted = ghost->In() + movement; + rational clamped = clamp(adjusted, earliest_in, latest_in); + + if (clamped != adjusted) { + movement = clamped - ghost->In(); + } + } + + return movement; +} + +rational TimelineView::PointerTool::ValidateOutTrimming(rational movement, + const QVector ghosts, + bool prevent_overwriting) +{ + foreach (TimelineViewGhostItem* ghost, ghosts) { + if (ghost->mode() != olive::timeline::kTrimOut) { + continue; + } + + Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + // Determine earliest and latest out points + rational earliest_out = ghost->In(); + + if (!ghost->CanHaveZeroLength()) { + earliest_out += parent()->timebase_; + } + + rational latest_out = RATIONAL_MAX; + + if (prevent_overwriting) { + // Determine if there's a block in the way + Block* next = block->next(); + while (next != nullptr && next->type() != Block::kEnd) { + 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); + + if (clamped != adjusted) { + movement = clamped - ghost->Out(); + } + } + + return movement; +} diff --git a/app/widget/timelineview/tool/ripple.cpp b/app/widget/timelineview/tool/ripple.cpp index 5a785fcae..8985abc40 100644 --- a/app/widget/timelineview/tool/ripple.cpp +++ b/app/widget/timelineview/tool/ripple.cpp @@ -20,6 +20,8 @@ #include "widget/timelineview/timelineview.h" +#include "node/block/gap/gap.h" + TimelineView::RippleTool::RippleTool(TimelineView* parent) : PointerTool(parent) { @@ -44,21 +46,108 @@ void TimelineView::RippleTool::MouseReleaseInternal(QMouseEvent *event) // The amount to ripple by rational ripple_length = parent()->SceneToTime(movement.x()); - QList blocks_to_ripple; - // Find earliest point to ripple around foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(0)); + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - blocks_to_ripple.append(b); + if (b == nullptr) { + // This is a gap we are creating + + // Make sure there's actually a gap being created + if (ghost->AdjustedLength() > 0) { + GapBlock* gap = new GapBlock(); + gap->set_length(ghost->AdjustedLength()); + + Block* block_to_append_gap_to = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)); + + parent()->timeline_node_->Tracks().at(ghost->Track())->InsertBlockAfter(gap, + block_to_append_gap_to); + } + } else { + // This was a Block that already existed + if (ghost->AdjustedLength() > 0) { + b->set_length(ghost->AdjustedLength()); + + if (movement_mode == olive::timeline::kTrimIn) { + // We'll need to shift the media in point too + b->set_media_in(b->media_in() + ghost->InAdjustment()); + } + } else { + // Assumed the Block was a Gap and it was reduced to zero length, remove it here + parent()->timeline_node_->Tracks().at(ghost->Track())->RippleRemoveBlock(b); + } + } + } +} + +rational TimelineView::RippleTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) +{ + // Only validate trimming, and we don't care about "overwriting" since the ripple tool is nondestructive + time_movement = ValidateInTrimming(time_movement, ghosts, false); + time_movement = ValidateOutTrimming(time_movement, ghosts, false); + + return time_movement; +} + +void TimelineView::RippleTool::InitiateGhosts(TimelineViewBlockItem *clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming) +{ + Q_UNUSED(allow_gap_trimming) + + PointerTool::InitiateGhosts(clicked_item, trim_mode, true); + + if (parent()->ghost_items_.isEmpty()) { + return; } - parent()->timeline_node_->RippleBlocks(blocks_to_ripple, ripple_length, movement_mode); -} + // Find the earliest ripple + rational earliest_ripple = RATIONAL_MAX; -rational TimelineView::RippleTool::FrameValidateInternal(rational time_movement, QVector ghosts) -{ - // FIXME: Validate rippling + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + rational ghost_ripple_point; - return PointerTool::FrameValidateInternal(time_movement, ghosts); + if (trim_mode == olive::timeline::kTrimIn) { + ghost_ripple_point = ghost->In(); + } else { + ghost_ripple_point = ghost->Out(); + } + + earliest_ripple = qMin(earliest_ripple, ghost_ripple_point); + } + + // For each track that does NOT have a ghost, we need to make one for Gaps + foreach (TrackOutput* track, parent()->timeline_node_->Tracks()) { + // Determine if we've already created a ghost on this track + bool ghost_on_this_track_exists = false; + + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (ghost->Track() == track->Index()) { + ghost_on_this_track_exists = true; + break; + } + } + + // If there's no ghost on this track, create one + if (!ghost_on_this_track_exists) { + // Find the block that starts just before the ripple point, and ends either on or just after it + Block* block_before_ripple = track->NearestBlockBefore(earliest_ripple); + + // If block is null, there will be no blocks after to ripple + if (block_before_ripple != nullptr && block_before_ripple->type() != Block::kEnd) { + TimelineViewGhostItem* ghost; + + if (block_before_ripple->type() == Block::kGap) { + // If this Block is already a Gap, ghost it now + ghost = AddGhostFromBlock(block_before_ripple, track->Index(), trim_mode); + } else { + // If there's no gap here, we'll need to create one + ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track->Index(), trim_mode); + ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); + } + +// ghost->SetInvisible(true); + } + } + } } diff --git a/app/widget/timelineview/tool/tool.cpp b/app/widget/timelineview/tool/tool.cpp index b5dc87f06..49581b92a 100644 --- a/app/widget/timelineview/tool/tool.cpp +++ b/app/widget/timelineview/tool/tool.cpp @@ -51,6 +51,19 @@ TimelineView *TimelineView::Tool::parent() return parent_; } +olive::timeline::MovementMode TimelineView::Tool::FlipTrimMode(const olive::timeline::MovementMode &trim_mode) +{ + if (trim_mode == olive::timeline::kTrimIn) { + return olive::timeline::kTrimOut; + } + + if (trim_mode == olive::timeline::kTrimOut) { + return olive::timeline::kTrimIn; + } + + return trim_mode; +} + QPointF TimelineView::Tool::GetScenePos(const QPoint &screen_pos) { return parent()->mapToScene(screen_pos); @@ -61,6 +74,28 @@ QGraphicsItem *TimelineView::Tool::GetItemAtScenePos(const QPointF &scene_pos) return parent()->scene_.itemAt(scene_pos, parent()->transform()); } +void AttemptSnap(const QList& proposed_pts, + double compare_point, + const QList& start_times, + rational compare_time, + rational* movement, + double* diff) { + const qreal kSnapRange = 10; // FIXME: Hardcoded number + + for (int i=0;i= 0) { + *movement = compare_time - start_times.at(i); + *diff = this_diff; + } + } + } +} + rational TimelineView::Tool::ValidateFrameMovement(rational movement, const QVector ghosts) { foreach (TimelineViewGhostItem* ghost, ghosts) { @@ -93,71 +128,6 @@ int TimelineView::Tool::ValidateTrackMovement(int movement, const QVector ghosts) -{ - foreach (TimelineViewGhostItem* ghost, ghosts) { - if (ghost->mode() != olive::timeline::kTrimIn) { - continue; - } - - Block* block = Node::ValueToPtr(ghost->data(0)); - - // Prevents any media_in points from becoming negative - if (block->media_in() + movement < 0) { - movement = -block->media_in(); - } - - // Prevents any clip length's becoming infinitely small (or negative length) - if (ghost->In() + movement >= ghost->Out()) { - // Since the timebase is considered more or less the "minimum unit", we adjust the movement to make the proposed - // length precisely one timebase unit in size - movement = ghost->Out() - parent()->timebase_ - ghost->In(); - } - } - - return movement; -} - -rational TimelineView::Tool::ValidateOutTrimming(rational movement, const QVector ghosts) -{ - foreach (TimelineViewGhostItem* ghost, ghosts) { - if (ghost->mode() != olive::timeline::kTrimOut) { - continue; - } - - // Prevents any clip length's becoming infinitely small (or negative length) - if (ghost->Out() + movement <= ghost->In()) { - // Since the timebase is considered more or less the "minimum unit", we adjust the movement to make the proposed - // length precisely one timebase unit in size - movement = ghost->In() + parent()->timebase_ - ghost->Out(); - } - } - - return movement; -} - -void AttemptSnap(const QList& proposed_pts, - double compare_point, - const QList& start_times, - rational compare_time, - rational* movement, - double* diff) { - const qreal kSnapRange = 10; // FIXME: Hardcoded number - - for (int i=0;i= 0) { - *movement = compare_time - start_times.at(i); - *diff = this_diff; - } - } - } -} - bool TimelineView::Tool::SnapPoint(QList start_times, rational* movement, int snap_points) { QList items = parent()->scene_.items(); @@ -181,17 +151,17 @@ bool TimelineView::Tool::SnapPoint(QList start_times, rational* moveme if (snap_points & kSnapToClips) { foreach (QGraphicsItem* it, items) { - TimelineViewClipItem* timeline_rect = dynamic_cast(it); + TimelineViewBlockItem* timeline_rect = dynamic_cast(it); if (timeline_rect != nullptr) { qreal rect_left = timeline_rect->x(); qreal rect_right = rect_left + timeline_rect->rect().width(); // Attempt snapping to clip in point - AttemptSnap(proposed_pts, rect_left, start_times, timeline_rect->clip()->in(), movement, &diff); + AttemptSnap(proposed_pts, rect_left, start_times, timeline_rect->block()->in(), movement, &diff); // Attempt snapping to clip out point - AttemptSnap(proposed_pts, rect_right, start_times, timeline_rect->clip()->out(), movement, &diff); + AttemptSnap(proposed_pts, rect_right, start_times, timeline_rect->block()->out(), movement, &diff); } } }