after the gaps are made and cleaned Results in more reliable ripple delete behavior. Also fixes bug that would cause gaps that were cleaned and then restored in an undo to crash when selected.
717 lines
24 KiB
C++
717 lines
24 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2019 Olive Team
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "widget/timelinewidget/timelinewidget.h"
|
|
|
|
#include <QDebug>
|
|
#include <QToolTip>
|
|
|
|
#include "common/clamp.h"
|
|
#include "common/flipmodifiers.h"
|
|
#include "common/qtutils.h"
|
|
#include "common/range.h"
|
|
#include "common/timecodefunctions.h"
|
|
#include "config/config.h"
|
|
#include "core.h"
|
|
#include "node/block/gap/gap.h"
|
|
#include "node/block/transition/transition.h"
|
|
#include "widget/nodeview/nodeviewundo.h"
|
|
|
|
TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) :
|
|
Tool(parent),
|
|
movement_allowed_(true),
|
|
trimming_allowed_(true),
|
|
track_movement_allowed_(true),
|
|
rubberband_selecting_(false)
|
|
{
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event)
|
|
{
|
|
// Main selection code
|
|
|
|
TimelineViewBlockItem* item = GetItemAtScenePos(event->GetCoordinates());
|
|
|
|
bool selectable_item = (item != nullptr
|
|
&& item->flags() & QGraphicsItem::ItemIsSelectable
|
|
&& !parent()->GetTrackFromReference(item->Track())->IsLocked());
|
|
|
|
if (selectable_item) {
|
|
// Cache the clip's type for use later
|
|
drag_track_type_ = item->Track().type();
|
|
}
|
|
|
|
// If this item is already selected
|
|
if (selectable_item
|
|
&& item->isSelected()) {
|
|
|
|
// If shift is held, deselect it
|
|
if (event->GetModifiers() & Qt::ShiftModifier) {
|
|
item->setSelected(false);
|
|
|
|
// If not holding alt, deselect all links as well
|
|
if (!(event->GetModifiers() & Qt::AltModifier)) {
|
|
parent()->SetBlockLinksSelected(item->block(), false);
|
|
}
|
|
}
|
|
|
|
// Otherwise do nothing
|
|
return;
|
|
}
|
|
|
|
// If not holding shift, deselect all clips
|
|
if (!(event->GetModifiers() & Qt::ShiftModifier)) {
|
|
parent()->DeselectAll();
|
|
}
|
|
|
|
if (selectable_item) {
|
|
// Select this item
|
|
item->setSelected(true);
|
|
|
|
// If not holding alt, select all links as well
|
|
if (!(event->GetModifiers() & Qt::AltModifier)) {
|
|
parent()->SetBlockLinksSelected(item->block(), true);
|
|
}
|
|
} else {
|
|
// Start rubberband drag
|
|
parent()->StartRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier));
|
|
|
|
rubberband_selecting_ = true;
|
|
}
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event)
|
|
{
|
|
if (rubberband_selecting_) {
|
|
|
|
// Process rubberband select
|
|
parent()->MoveRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier));
|
|
|
|
} else if (!dragging_) {
|
|
// Now that the cursor has moved, we will assume the intention is to drag
|
|
|
|
// If we haven't started dragging yet, we'll initiate a drag here
|
|
InitiateDrag(event);
|
|
|
|
// Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed
|
|
dragging_ = true;
|
|
|
|
} else if (!parent()->ghost_items_.isEmpty()) {
|
|
|
|
// We're already dragging AND we have ghosts to work with
|
|
ProcessDrag(event->GetCoordinates());
|
|
|
|
}
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event)
|
|
{
|
|
if (rubberband_selecting_) {
|
|
parent()->EndRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier));
|
|
|
|
rubberband_selecting_ = false;
|
|
return;
|
|
}
|
|
|
|
if (!parent()->ghost_items_.isEmpty()) {
|
|
MouseReleaseInternal(event);
|
|
}
|
|
|
|
if (dragging_) {
|
|
parent()->ClearGhosts();
|
|
snap_points_.clear();
|
|
}
|
|
|
|
dragging_ = false;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event)
|
|
{
|
|
// No dragging, but we still want to process cursors
|
|
TimelineViewBlockItem* block_at_cursor = GetItemAtScenePos(event->GetCoordinates());
|
|
|
|
if (block_at_cursor) {
|
|
switch (IsCursorInTrimHandle(block_at_cursor, event->GetSceneX())) {
|
|
case Timeline::kTrimIn:
|
|
parent()->setCursor(Qt::SizeHorCursor);
|
|
break;
|
|
case Timeline::kTrimOut:
|
|
parent()->setCursor(Qt::SizeHorCursor);
|
|
break;
|
|
default:
|
|
parent()->unsetCursor();
|
|
}
|
|
} else {
|
|
parent()->unsetCursor();
|
|
}
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::SetMovementAllowed(bool allowed)
|
|
{
|
|
movement_allowed_ = allowed;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::SetTrackMovementAllowed(bool allowed)
|
|
{
|
|
track_movement_allowed_ = allowed;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::SetTrimmingAllowed(bool allowed)
|
|
{
|
|
trimming_allowed_ = allowed;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::MouseReleaseInternal(TimelineViewMouseEvent *event)
|
|
{
|
|
QUndoCommand* command = new QUndoCommand();
|
|
|
|
QList<Block*> blocks_to_temp_remove;
|
|
QList<TrackReference> tracks_affected;
|
|
QList<TimelineViewGhostItem*> ignore_ghosts;
|
|
|
|
bool duplicate_clips = (event->GetModifiers() & Qt::AltModifier);
|
|
|
|
// 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
|
|
for (int i=0;i<parent()->ghost_items_.size();i++) {
|
|
TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i);
|
|
|
|
// If the ghost has not been adjusted nothing needs to be done
|
|
if (!ghost->HasBeenAdjusted()) {
|
|
ignore_ghosts.append(ghost);
|
|
continue;
|
|
}
|
|
|
|
Block* b = Node::ValueToPtr<Block>(ghost->data(TimelineViewGhostItem::kAttachedBlock));
|
|
|
|
if (!duplicate_clips || ghost->mode() != Timeline::kMove || b->type() == Block::kTransition) {
|
|
// If we're duplicating (user is holding ALT), no need to remove the original clip. However if the ghost was
|
|
// trimmed, it can't be duplicated.
|
|
blocks_to_temp_remove.append(b);
|
|
}
|
|
|
|
if (!tracks_affected.contains(ghost->Track())) {
|
|
tracks_affected.append(ghost->Track());
|
|
}
|
|
|
|
if (!tracks_affected.contains(ghost->GetAdjustedTrack())) {
|
|
tracks_affected.append(ghost->GetAdjustedTrack());
|
|
}
|
|
}
|
|
|
|
bool inserting = (event->GetModifiers() & Qt::ControlModifier);
|
|
|
|
// If there are any blocks to remove, remove them
|
|
parent()->DeleteSelectedInternal(blocks_to_temp_remove, false, false, command);
|
|
|
|
if (inserting) {
|
|
// Make room to insert clips to
|
|
InsertGapsAtGhostDestination(parent()->ghost_items_, command);
|
|
}
|
|
|
|
// 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
|
|
for (int i=0;i<parent()->ghost_items_.size();i++) {
|
|
TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i);
|
|
|
|
// If the ghost has not been adjusted nothing needs to be done
|
|
if (ignore_ghosts.contains(ghost)) {
|
|
continue;
|
|
}
|
|
|
|
const TrackReference& track_ref = ghost->GetAdjustedTrack();
|
|
//TrackOutput* track = parent()->GetTrackFromReference(track_ref);
|
|
|
|
Block* b = Node::ValueToPtr<Block>(ghost->data(TimelineViewGhostItem::kAttachedBlock));
|
|
|
|
// Normal blocks work in conjunction with the gap made above
|
|
if (Timeline::IsATrimMode(ghost->mode())) {
|
|
// If we were trimming, we'll need to change the length
|
|
|
|
// If we were trimming the in point, we'll need to adjust the media in too
|
|
if (ghost->mode() == Timeline::kTrimIn) {
|
|
new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command);
|
|
} else {
|
|
new BlockResizeCommand(b, ghost->AdjustedLength(), command);
|
|
}
|
|
} else if (duplicate_clips && ghost->mode() == Timeline::kMove && b->type() != Block::kTransition) {
|
|
// Duplicate rather than move
|
|
Node* copy = b->copy();
|
|
|
|
new NodeAddCommand(static_cast<NodeGraph*>(b->parent()),
|
|
copy,
|
|
command);
|
|
|
|
new NodeCopyInputsCommand(b, copy, true, command);
|
|
|
|
// Place the copy instead of the original block
|
|
b = static_cast<Block*>(copy);
|
|
} else if (b->type() == Block::kTransition) {
|
|
// If the block is a dual transition and we're moving it, the mid point should be moved
|
|
TransitionBlock* transition = static_cast<TransitionBlock*>(b);
|
|
|
|
if (transition->connected_in_block() && transition->connected_out_block()) {
|
|
new BlockSetMediaInCommand(transition,
|
|
transition->media_in() + ghost->InAdjustment(),
|
|
command);
|
|
}
|
|
}
|
|
|
|
if (b->type() == Block::kTransition && ghost->AdjustedLength() == 0) {
|
|
// Remove transitions that have been reduced to zero length
|
|
new NodeRemoveCommand(static_cast<NodeGraph*>(b->parent()),
|
|
{b},
|
|
command);
|
|
} else {
|
|
// Normal block placement
|
|
new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()),
|
|
track_ref.index(),
|
|
b,
|
|
ghost->GetAdjustedIn(),
|
|
command);
|
|
}
|
|
}
|
|
|
|
if (command->childCount() > 0) {
|
|
foreach (const TrackReference& t, tracks_affected) {
|
|
new TrackCleanGapsCommand(parent()->GetConnectedNode()->track_list(t.type()),
|
|
t.index(),
|
|
command);
|
|
}
|
|
}
|
|
|
|
Core::instance()->undo_stack()->pushIfHasChildren(command);
|
|
}
|
|
|
|
rational TimelineWidget::PointerTool::FrameValidateInternal(rational time_movement, const QVector<TimelineViewGhostItem *>& ghosts)
|
|
{
|
|
// Default behavior is to validate all movement and trimming
|
|
time_movement = ValidateFrameMovement(time_movement, ghosts);
|
|
time_movement = ValidateInTrimming(time_movement, ghosts, true);
|
|
time_movement = ValidateOutTrimming(time_movement, ghosts, true);
|
|
|
|
return time_movement;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::InitiateDrag(TimelineViewMouseEvent *mouse_pos)
|
|
{
|
|
// Record where the drag started in timeline coordinates
|
|
drag_start_ = mouse_pos->GetCoordinates();
|
|
|
|
// Get the item that was clicked
|
|
TimelineViewBlockItem* clicked_item = 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
|
|
if (clicked_item != nullptr) {
|
|
|
|
// Clear snap points
|
|
snap_points_.clear();
|
|
|
|
// Record where the drag started in timeline coordinates
|
|
track_start_ = mouse_pos->GetTrack();
|
|
|
|
// Determine whether we're trimming or moving based on the position of the cursor
|
|
Timeline::MovementMode trim_mode = IsCursorInTrimHandle(clicked_item, mouse_pos->GetSceneX());
|
|
|
|
// Some derived classes don't allow movement
|
|
if (trim_mode == Timeline::kNone && movement_allowed_) {
|
|
trim_mode = Timeline::kMove;
|
|
}
|
|
|
|
// Gaps can't be moved, only trimmed
|
|
if (clicked_item->block()->type() == Block::kGap && trim_mode == Timeline::kMove) {
|
|
trim_mode = Timeline::kNone;
|
|
}
|
|
|
|
// Make sure we can actually perform an action here
|
|
if (trim_mode != Timeline::kNone) {
|
|
InitiateGhosts(clicked_item, trim_mode, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos)
|
|
{
|
|
// Determine track movement
|
|
const TrackReference& cursor_track = mouse_pos.GetTrack();
|
|
int track_movement = 0;
|
|
|
|
if (track_movement_allowed_) {
|
|
track_movement = cursor_track.index() - track_start_.index();
|
|
}
|
|
|
|
// Determine frame movement
|
|
rational time_movement = mouse_pos.GetFrame() - drag_start_.GetFrame();
|
|
|
|
// Perform snapping if enabled (adjusts time_movement if it's close to any potential snap points)
|
|
if (Core::instance()->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()) {
|
|
case Timeline::kNone:
|
|
break;
|
|
case Timeline::kTrimIn:
|
|
ghost->SetInAdjustment(time_movement);
|
|
break;
|
|
case Timeline::kTrimOut:
|
|
ghost->SetOutAdjustment(time_movement);
|
|
break;
|
|
case Timeline::kMove:
|
|
{
|
|
ghost->SetInAdjustment(time_movement);
|
|
ghost->SetOutAdjustment(time_movement);
|
|
|
|
// Track movement is only legal for moving, not for trimming
|
|
// Also, we only move the clips on the same track type that the drag started from
|
|
if (ghost->Track().type() == drag_track_type_) {
|
|
ghost->SetTrackAdjustment(track_movement);
|
|
}
|
|
|
|
const TrackReference& track = ghost->GetAdjustedTrack();
|
|
ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show tooltip
|
|
// Generate tooltip (showing earliest in point of imported clip)
|
|
int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->timebase());
|
|
QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp,
|
|
parent()->timebase(),
|
|
Timecode::CurrentDisplay(),
|
|
true);
|
|
|
|
// Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way
|
|
// of the cursor)
|
|
QToolTip::hideText();
|
|
QToolTip::showText(QCursor::pos(),
|
|
tooltip_text,
|
|
parent());
|
|
}
|
|
|
|
Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(TimelineViewBlockItem *block, qreal cursor_x)
|
|
{
|
|
double kTrimHandle = QFontMetricsWidth(parent()->fontMetrics(), "H");
|
|
|
|
// Block is too narrow, no trimming allowed
|
|
if (block->rect().width() <= kTrimHandle * 2) {
|
|
return Timeline::kNone;
|
|
}
|
|
|
|
if (trimming_allowed_ && cursor_x <= block->x() + kTrimHandle) {
|
|
return Timeline::kTrimIn;
|
|
} else if (trimming_allowed_ && cursor_x >= block->x() + block->rect().right() - kTrimHandle) {
|
|
return Timeline::kTrimOut;
|
|
} else {
|
|
return Timeline::kNone;
|
|
}
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::InitiateGhosts(TimelineViewBlockItem* clicked_item,
|
|
Timeline::MovementMode trim_mode,
|
|
bool allow_gap_trimming)
|
|
{
|
|
// Convert selected items list to clips list
|
|
QList<TimelineViewBlockItem*> clips = parent()->GetSelectedBlocks();
|
|
|
|
// 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 (Timeline::IsATrimMode(trim_mode)) {
|
|
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->block()->type() == Block::kGap
|
|
&& !Timeline::IsATrimMode(trim_mode)) {
|
|
continue;
|
|
}
|
|
|
|
if (clip_item != clicked_item
|
|
&& (Timeline::IsATrimMode(trim_mode))) {
|
|
include_this_clip = multitrim_enabled ? IsClipTrimmable(clip_item, clips, trim_mode) : false;
|
|
}
|
|
|
|
if (include_this_clip) {
|
|
Block* block = clip_item->block();
|
|
Timeline::MovementMode block_mode = trim_mode;
|
|
|
|
// If we don't allow gap trimming, we automatically switch to the next/previous block to trim
|
|
if (block->type() == Block::kGap && !allow_gap_trimming) {
|
|
if (trim_mode == 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) {
|
|
TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), block_mode);
|
|
|
|
if (block->type() == Block::kTransition) {
|
|
TransitionBlock* transition = static_cast<TransitionBlock*>(block);
|
|
|
|
bool transition_can_move_tracks = false;
|
|
|
|
// Create a rolling effect with the attached block
|
|
if (transition->connected_in_block()) {
|
|
if (parent()->block_items_.value(transition->connected_in_block())->isSelected()) {
|
|
// We'll be moving this item too, no need to create a ghost for it here
|
|
transition_can_move_tracks = true;
|
|
} else if (block_mode == Timeline::kTrimOut || block_mode == Timeline::kMove) {
|
|
AddGhostFromBlock(transition->connected_in_block(), clip_item->Track(), Timeline::kTrimIn);
|
|
}
|
|
}
|
|
|
|
if (transition->connected_out_block()) {
|
|
if (parent()->block_items_.value(transition->connected_in_block())->isSelected()) {
|
|
// We'll be moving this item too, no need to create a ghost for it here
|
|
transition_can_move_tracks = true;
|
|
} else if (block_mode == Timeline::kTrimIn || block_mode == Timeline::kMove) {
|
|
AddGhostFromBlock(transition->connected_out_block(), clip_item->Track(), Timeline::kTrimOut);
|
|
}
|
|
}
|
|
|
|
ghost->SetCanMoveTracks(transition_can_move_tracks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode)
|
|
{
|
|
TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block,
|
|
track,
|
|
parent()->GetTrackY(track),
|
|
parent()->GetTrackHeight(track));
|
|
|
|
AddGhostInternal(ghost, mode);
|
|
|
|
return ghost;
|
|
}
|
|
|
|
TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode)
|
|
{
|
|
TimelineViewGhostItem* ghost = new TimelineViewGhostItem();
|
|
|
|
ghost->SetIn(in);
|
|
ghost->SetOut(out);
|
|
ghost->SetTrack(track);
|
|
ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track));
|
|
|
|
AddGhostInternal(ghost, mode);
|
|
|
|
return ghost;
|
|
}
|
|
|
|
void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode)
|
|
{
|
|
ghost->SetMode(mode);
|
|
|
|
// Prepare snap points (optimizes snapping for later)
|
|
switch (mode) {
|
|
case Timeline::kMove:
|
|
snap_points_.append(ghost->In());
|
|
snap_points_.append(ghost->Out());
|
|
break;
|
|
case Timeline::kTrimIn:
|
|
snap_points_.append(ghost->In());
|
|
break;
|
|
case Timeline::kTrimOut:
|
|
snap_points_.append(ghost->Out());
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
parent()->AddGhost(ghost);
|
|
}
|
|
|
|
bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip,
|
|
const QList<TimelineViewBlockItem*>& items,
|
|
const Timeline::MovementMode& mode)
|
|
{
|
|
foreach (TimelineViewBlockItem* compare, items) {
|
|
if (clip->Track() == compare->Track()
|
|
&& clip != compare
|
|
&& ((compare->block()->in() < clip->block()->in() && mode == Timeline::kTrimIn)
|
|
|| (compare->block()->out() > clip->block()->out() && mode == Timeline::kTrimOut))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
rational GetEarliestPointForClip(Block* block)
|
|
{
|
|
return qMax(rational(0), block->in() - block->media_in());
|
|
}
|
|
|
|
rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement,
|
|
const QVector<TimelineViewGhostItem *> ghosts,
|
|
bool prevent_overwriting)
|
|
{
|
|
foreach (TimelineViewGhostItem* ghost, ghosts) {
|
|
if (ghost->mode() != Timeline::kTrimIn) {
|
|
continue;
|
|
}
|
|
|
|
Block* block = Node::ValueToPtr<Block>(ghost->data(TimelineViewGhostItem::kAttachedBlock));
|
|
|
|
rational earliest_in = RATIONAL_MIN;
|
|
rational latest_in = ghost->Out();
|
|
|
|
if (block->type() == Block::kTransition) {
|
|
// For transitions, validate with the attached block
|
|
TransitionBlock* transition = static_cast<TransitionBlock*>(block);
|
|
|
|
if (transition->connected_in_block() && transition->connected_out_block()) {
|
|
// Here, we try to get the latest earliest point for both the in and out blocks, we do in here and out will
|
|
// be calculated later
|
|
earliest_in = GetEarliestPointForClip(transition->connected_in_block());
|
|
|
|
// We set the block to the out block since that will be before the in block and will be the one we use to
|
|
// prevent overwriting since we're trimming the in side of this transition
|
|
block = transition->connected_out_block();
|
|
|
|
latest_in = transition->in() + transition->out_offset();
|
|
} else {
|
|
// Use whatever block is attached
|
|
block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block();
|
|
}
|
|
}
|
|
|
|
earliest_in = qMax(earliest_in, GetEarliestPointForClip(block));
|
|
|
|
if (!ghost->CanHaveZeroLength()) {
|
|
latest_in -= parent()->timebase();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 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 TimelineWidget::PointerTool::ValidateOutTrimming(rational movement,
|
|
const QVector<TimelineViewGhostItem *> ghosts,
|
|
bool prevent_overwriting)
|
|
{
|
|
foreach (TimelineViewGhostItem* ghost, ghosts) {
|
|
if (ghost->mode() != Timeline::kTrimOut) {
|
|
continue;
|
|
}
|
|
|
|
Block* block = Node::ValueToPtr<Block>(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 (block->type() == Block::kTransition) {
|
|
// For transitions, validate with the attached block
|
|
TransitionBlock* transition = static_cast<TransitionBlock*>(block);
|
|
|
|
if (transition->connected_in_block() && transition->connected_out_block()) {
|
|
// We set the block to the out block since that will be before the in block and will be the one we use to
|
|
// prevent overwriting since we're trimming the in side of this transition
|
|
|
|
// FIXME: At some point we may add some better logic to `latest_out` akin to the logic in ValidateInTrimming
|
|
// which is why this hasn't yet been collapsed into the ternary below.
|
|
block = transition->connected_in_block();
|
|
|
|
earliest_out = transition->out() - transition->in_offset();
|
|
} else {
|
|
block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block();
|
|
}
|
|
}
|
|
|
|
if (prevent_overwriting) {
|
|
// 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);
|
|
|
|
if (clamped != adjusted) {
|
|
movement = clamped - ghost->Out();
|
|
}
|
|
}
|
|
|
|
return movement;
|
|
}
|