/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 .
***/
#include "widget/timelinewidget/timelinewidget.h"
#include
#include
#include "common/qtutils.h"
#include "common/range.h"
#include "config/config.h"
#include "core.h"
#include "node/block/gap/gap.h"
#include "node/block/transition/transition.h"
#include "node/nodeundo.h"
#include "pointer.h"
#include "timeline/timelineundopointer.h"
#include "widget/timeruler/timeruler.h"
namespace olive
{
PointerTool::PointerTool(TimelineWidget *parent)
: TimelineTool(parent)
, movement_allowed_(true)
, trimming_allowed_(true)
, track_movement_allowed_(true)
, gap_trimming_allowed_(false)
, can_rubberband_select_(false)
, rubberband_selecting_(false)
{
}
void PointerTool::MousePress(TimelineViewMouseEvent *event)
{
const Track::Reference &track_ref = event->GetTrack();
// Determine if item clicked on is selectable
clicked_item_ = parent()->GetItemAtScenePos(event->GetCoordinates());
ClipBlock *clip_clicked_item = dynamic_cast(clicked_item_);
can_rubberband_select_ = false;
bool selectable_item =
(clicked_item_ &&
!parent()->GetTrackFromReference(track_ref)->IsLocked());
if (selectable_item) {
// Cache the clip's type for use later
drag_track_type_ = track_ref.type();
// If we haven't started dragging yet, we'll initiate a drag here
// Record where the drag started in timeline coordinates
drag_start_ = event->GetCoordinates();
// Determine whether we're trimming or moving based on the position of the cursor
drag_movement_mode_ =
IsCursorInTrimHandle(clicked_item_, event->GetSceneX());
// If we're not in a trim mode, we must be in a move mode (provided the tool allows movement and
// the block is not a gap)
if (drag_movement_mode_ == Timeline::kNone && movement_allowed_ &&
!dynamic_cast(clicked_item_)) {
drag_movement_mode_ = Timeline::kMove;
}
// If this item is already selected, no further selection needs to be made
if (parent()->IsBlockSelected(clicked_item_)) {
// Collect item deselections
QVector deselected_blocks;
// If shift is held, deselect it
if (event->GetModifiers() & Qt::ShiftModifier) {
parent()->RemoveSelection(clicked_item_);
deselected_blocks.append(clicked_item_);
// If not holding alt, deselect all links as well
if (clip_clicked_item &&
!(event->GetModifiers() & Qt::AltModifier)) {
parent()->SetBlockLinksSelected(clip_clicked_item, false);
deselected_blocks.append(clip_clicked_item->block_links());
}
}
parent()->SignalDeselectedBlocks(deselected_blocks);
return;
}
}
// If not holding shift, deselect all clips
if (!(event->GetModifiers() & Qt::ShiftModifier)) {
parent()->DeselectAll();
}
if (selectable_item) {
// Collect item selections
QVector selected_blocks;
// Select this item
parent()->AddSelection(clicked_item_);
selected_blocks.append(clicked_item_);
// If not holding alt, select all links as well
if (clip_clicked_item && !(event->GetModifiers() & Qt::AltModifier)) {
parent()->SetBlockLinksSelected(clip_clicked_item, true);
selected_blocks.append(clip_clicked_item->block_links());
}
parent()->SignalSelectedBlocks(selected_blocks);
}
can_rubberband_select_ =
(event->GetButton() ==
Qt::LeftButton // Only rubberband select from the primary mouse button
&&
(!selectable_item ||
drag_movement_mode_ ==
Timeline::
kNone)); // And if no item was selected OR the item isn't draggable
if (can_rubberband_select_) {
drag_global_start_ = QCursor::pos();
}
// If we click anywhere other than a marker, deselect all markers
parent()->ruler()->DeselectAllMarkers();
}
void PointerTool::MouseMove(TimelineViewMouseEvent *event)
{
if (can_rubberband_select_) {
if (!rubberband_selecting_) {
// If we clicked an item but are rubberband selecting anyway, deselect it now
if (clicked_item_) {
parent()->RemoveSelection(clicked_item_);
parent()->SignalDeselectedBlocks({ clicked_item_ });
clicked_item_ = nullptr;
}
parent()->StartRubberBandSelect(drag_global_start_);
rubberband_selecting_ = true;
}
// Process rubberband select
parent()->MoveRubberBandSelect(true, !(event->GetModifiers() &
Qt::AltModifier));
} else {
// Process drag
if (!dragging_) {
// Now that the cursor has moved, we will assume the intention is to drag
// Clear snap points
snap_points_.clear();
// If we're performing an action, we can initiate ghosts
if (drag_movement_mode_ != Timeline::kNone) {
InitiateDrag(clicked_item_, drag_movement_mode_,
event->GetModifiers());
}
// Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed
dragging_ = true;
}
if (dragging_ && !parent()->GetGhostItems().isEmpty()) {
// We're already dragging AND we have ghosts to work with
ProcessDrag(event->GetCoordinates());
}
}
}
void PointerTool::MouseRelease(TimelineViewMouseEvent *event)
{
if (rubberband_selecting_) {
// Finish rubberband select
parent()->EndRubberBandSelect();
rubberband_selecting_ = false;
return;
}
if (dragging_) {
// If we were dragging, process the end of the drag
if (!parent()->GetGhostItems().isEmpty()) {
FinishDrag(event);
}
// Clean up
parent()->ClearGhosts();
snap_points_.clear();
dragging_ = false;
}
}
void PointerTool::HoverMove(TimelineViewMouseEvent *event)
{
if (trimming_allowed_) {
// No dragging, but we still want to process cursors
Block *block_at_cursor =
parent()->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();
}
} else {
parent()->unsetCursor();
}
}
void SetGhostToSlideMode(TimelineViewGhostItem *g)
{
g->SetCanMoveTracks(false);
g->SetData(TimelineViewGhostItem::kGhostIsSliding, true);
}
void PointerTool::InitiateDragInternal(Block *clicked_item,
Timeline::MovementMode trim_mode,
Qt::KeyboardModifiers modifiers,
bool dont_roll_trims,
bool allow_nongap_rolling,
bool slide_instead_of_moving)
{
// Get list of selected blocks
QVector clips = parent()->GetSelectedBlocks();
if (trim_mode == Timeline::kMove) {
// Gaps are not allowed to move, and since we only allow moving one block type at a time,
// dragging a gap is a no-op
if (dynamic_cast(clicked_item)) {
return;
}
bool sliding_due_to_transition = false;
if (!slide_instead_of_moving) {
// If the user tries to move a transition without moving the clip it belongs to, we turn
// this into a slide
foreach (Block *block, clips) {
if (TransitionBlock *transit =
dynamic_cast(block)) {
if (!CanTransitionMove(transit, clips)) {
slide_instead_of_moving = true;
break;
}
} else if (ClipBlock *clip = dynamic_cast(block)) {
if ((clip->in_transition() &&
!CanTransitionMove(clip->in_transition(), clips)) ||
(clip->out_transition() &&
!CanTransitionMove(clip->out_transition(), clips))) {
slide_instead_of_moving = true;
break;
}
}
}
sliding_due_to_transition = slide_instead_of_moving;
}
if (slide_instead_of_moving) {
// 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