timeline: vastly improved snapping

This commit is contained in:
itsmattkc
2020-06-06 13:59:49 +10:00
parent 5055c136b6
commit aaee2ab9ed
13 changed files with 285 additions and 99 deletions
+2
View File
@@ -21,6 +21,8 @@ add_subdirectory(view)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
widget/timelinewidget/snapservice.h
widget/timelinewidget/snapservice.cpp
widget/timelinewidget/timelineandtrackview.h
widget/timelinewidget/timelineandtrackview.cpp
widget/timelinewidget/timelinescaledobject.h
@@ -0,0 +1 @@
#include "snapservice.h"
+31
View File
@@ -0,0 +1,31 @@
#ifndef SNAPSERVICE_H
#define SNAPSERVICE_H
#include "common/rational.h"
OLIVE_NAMESPACE_ENTER
class SnapService
{
public:
SnapService() = default;
enum SnapPoints {
kSnapToClips = 0x1,
kSnapToPlayhead = 0x2,
kSnapToMarkers = 0x4,
kSnapAll = 0xFF
};
/**
* @brief Snaps point `start_point` that is moving by `movement` to currently existing clips
*/
virtual bool SnapPoint(QList<rational> start_times, rational *movement, int snap_points = kSnapAll) = 0;
virtual void HideSnaps() = 0;
};
OLIVE_NAMESPACE_EXIT
#endif // SNAPSERVICE_H
+124 -1
View File
@@ -20,11 +20,13 @@
#include "timelinewidget.h"
#include <cfloat>
#include <QSplitter>
#include <QVBoxLayout>
#include <QtMath>
#include "core.h"
#include "common/range.h"
#include "common/timecodefunctions.h"
#include "dialog/sequence/sequence.h"
#include "dialog/speedduration/speedduration.h"
@@ -56,6 +58,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
ruler_and_time_layout->addWidget(timecode_label_);
ruler_and_time_layout->addWidget(ruler());
ruler()->SetSnapService(this);
// Create list of TimelineViews - these MUST correspond to the ViewType enum
@@ -103,6 +106,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
view->SetSnapService(this);
view_splitter->addWidget(tview);
@@ -906,6 +910,8 @@ void TimelineWidget::ClearGhosts()
ghost_items_.clear();
}
HideSnaps();
}
bool TimelineWidget::HasGhosts()
@@ -1266,7 +1272,7 @@ void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected)
}
QVector<Timeline::EditToInfo> TimelineWidget::GetEditToInfo(const rational& playhead_time,
Timeline::MovementMode mode)
Timeline::MovementMode mode)
{
// Get list of unlocked tracks
QVector<TrackOutput*> tracks = GetConnectedNode()->GetUnlockedTracks();
@@ -1399,6 +1405,20 @@ void TimelineWidget::EditTo(Timeline::MovementMode mode)
Core::instance()->undo_stack()->pushIfHasChildren(command);
}
void TimelineWidget::ShowSnap(const QList<rational> &times)
{
foreach (TimelineAndTrackView* tview, views_) {
tview->view()->EnableSnap(times);
}
}
void TimelineWidget::HideSnaps()
{
foreach (TimelineAndTrackView* tview, views_) {
tview->view()->DisableSnap();
}
}
void TimelineWidget::StartRubberBandSelect(bool enable_selecting, bool select_links)
{
drag_origin_ = QCursor::pos();
@@ -1478,4 +1498,107 @@ void TimelineWidget::EndRubberBandSelect(bool enable_selecting, bool select_link
ViewSelectionChanged();
}
struct SnapData {
rational time;
rational movement;
};
QList<SnapData> AttemptSnap(const QList<double>& screen_pt,
double compare_pt,
const QList<rational>& start_times,
const rational& compare_time) {
const qreal kSnapRange = 10; // FIXME: Hardcoded number
QList<SnapData> snap_data;
for (int i=0;i<screen_pt.size();i++) {
// Attempt snapping to clip out point
if (InRange(screen_pt.at(i), compare_pt, kSnapRange)) {
snap_data.append({compare_time, compare_time - start_times.at(i)});
}
}
return snap_data;
}
bool TimelineWidget::SnapPoint(QList<rational> start_times, rational* movement, int snap_points)
{
QList<double> screen_pt;
foreach (const rational& s, start_times) {
screen_pt.append(TimeToScene(s + *movement));
}
QList<SnapData> potential_snaps;
if (snap_points & kSnapToPlayhead) {
rational playhead_abs_time = GetTime();
qreal playhead_pos = TimeToScene(playhead_abs_time);
potential_snaps.append(AttemptSnap(screen_pt, playhead_pos, start_times, playhead_abs_time));
}
if (snap_points & kSnapToClips) {
QMap<Block*, TimelineViewBlockItem*>::const_iterator i;
for (i=block_items_.constBegin(); i!=block_items_.constEnd(); i++) {
TimelineViewBlockItem* item = i.value();
if (item) {
qreal rect_left = item->x();
qreal rect_right = rect_left + item->rect().width();
// Attempt snapping to clip in point
potential_snaps.append(AttemptSnap(screen_pt, rect_left, start_times, item->block()->in()));
// Attempt snapping to clip out point
potential_snaps.append(AttemptSnap(screen_pt, rect_right, start_times, item->block()->out()));
}
}
}
if ((snap_points & kSnapToMarkers) && GetConnectedTimelinePoints()) {
foreach (TimelineMarker* m, GetConnectedTimelinePoints()->markers()->list()) {
qreal marker_pos = TimeToScene(m->time().in());
potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().in()));
if (m->time().in() != m->time().out()) {
marker_pos = TimeToScene(m->time().out());
potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().out()));
}
}
}
if (potential_snaps.isEmpty()) {
HideSnaps();
return false;
}
int closest_snap = 0;
rational closest_diff = qAbs(potential_snaps.at(0).movement - *movement);
// Determine which snap point was the closest
for (int i=1; i<potential_snaps.size(); i++) {
rational this_diff = qAbs(potential_snaps.at(i).movement - *movement);
if (this_diff < closest_diff) {
closest_snap = i;
closest_diff = this_diff;
}
}
*movement = potential_snaps.at(closest_snap).movement;
// Find all points at this movement
QList<rational> snap_times;
foreach (const SnapData& data, potential_snaps) {
if (data.movement == *movement) {
snap_times.append(data.time);
}
}
ShowSnap(snap_times);
return true;
}
OLIVE_NAMESPACE_EXIT
+8 -12
View File
@@ -27,6 +27,7 @@
#include "core.h"
#include "node/output/viewer/viewer.h"
#include "snapservice.h"
#include "timeline/timelinecommon.h"
#include "timelineandtrackview.h"
#include "widget/nodecopypaste/nodecopypaste.h"
@@ -40,7 +41,7 @@ OLIVE_NAMESPACE_ENTER
*
* Encapsulates TimelineViews, TimeRulers, and scrollbars for a complete widget to manipulate Timelines
*/
class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteWidget
class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteWidget, public SnapService
{
Q_OBJECT
public:
@@ -93,6 +94,10 @@ public:
QList<TimelineViewBlockItem*> GetSelectedBlocks();
virtual bool SnapPoint(QList<rational> start_times, rational *movement, int snap_points = kSnapAll) override;
virtual void HideSnaps() override;
signals:
void SelectionChanged(const QList<Block*>& selected_blocks);
@@ -189,17 +194,6 @@ private:
*/
int ValidateTrackMovement(int movement, const QVector<TimelineViewGhostItem*> ghosts);
enum SnapPoints {
kSnapToClips = 0x1,
kSnapToPlayhead = 0x2,
kSnapAll = 0xFF
};
/**
* @brief Snaps point `start_point` that is moving by `movement` to currently existing clips
*/
bool SnapPoint(QList<rational> start_times, rational *movement, int snap_points = kSnapAll);
void GetGhostData(const QVector<TimelineViewGhostItem*>& ghosts, rational *earliest_point, rational *latest_point);
void InsertGapsAtGhostDestination(const QVector<TimelineViewGhostItem*>& ghosts, QUndoCommand* command);
@@ -455,6 +449,8 @@ private:
void EditTo(Timeline::MovementMode mode);
void ShowSnap(const QList<rational>& times);
QPoint drag_origin_;
void StartRubberBandSelect(bool enable_selecting, bool select_links);
+2 -2
View File
@@ -150,13 +150,13 @@ void TimelineWidget::AddTool::MouseMoveInternal(const rational &cursor_frame, bo
rational movement = cursor_frame - drag_start_point_;
// Snap movement
bool snapped = SnapPoint(snap_points_, &movement);
bool snapped = parent()->SnapPoint(snap_points_, &movement);
// If alt is held, our movement goes both ways (outwards)
if (!snapped && outwards) {
// Snap backwards too
movement = -movement;
SnapPoint(snap_points_, &movement);
parent()->SnapPoint(snap_points_, &movement);
// We don't need to un-neg here because outwards means all future processing will be done both pos and neg
}
+8 -5
View File
@@ -122,14 +122,17 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event)
rational time_movement = event->GetFrame() - drag_start_.GetFrame();
int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index();
// If snapping is enabled, check for snap points
if (Core::instance()->snapping()) {
SnapPoint(snap_points_, &time_movement);
}
time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_);
track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_);
// If snapping is enabled, check for snap points
if (Core::instance()->snapping()) {
parent()->SnapPoint(snap_points_, &time_movement);
time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_);
track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_);
}
rational earliest_ghost = RATIONAL_MAX;
// Move ghosts to the mouse cursor
+9 -6
View File
@@ -314,17 +314,20 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po
// 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)
// NOTE: Always do this after snapping to ensure the snap hasn't made an illegal movement.
time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_);
time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_);
time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_);
// Perform snapping if enabled (adjusts time_movement if it's close to any potential snap points)
if (Core::instance()->snapping()) {
parent()->SnapPoint(snap_points_, &time_movement);
time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_);
time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_);
time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_);
}
// Validate ghosts that are being moved (clips from other track types do NOT get moved)
{
QVector<TimelineViewGhostItem*> validate_track_ghosts = parent()->ghost_items_;
-71
View File
@@ -20,9 +20,6 @@
#include "widget/timelinewidget/timelinewidget.h"
#include <cfloat>
#include "common/range.h"
#include "node/block/transition/transition.h"
#include "widget/nodeview/nodeviewundo.h"
@@ -76,28 +73,6 @@ TimelineViewBlockItem *TimelineWidget::Tool::GetItemAtScenePos(const TimelineCoo
return nullptr;
}
void AttemptSnap(const QList<double>& proposed_pts,
double compare_point,
const QList<rational>& start_times,
rational compare_time,
rational* movement,
double* diff) {
const qreal kSnapRange = 10; // FIXME: Hardcoded number
for (int i=0;i<proposed_pts.size();i++) {
// Attempt snapping to clip out point
if (InRange(proposed_pts.at(i), compare_point, kSnapRange)) {
double this_diff = qAbs(compare_point - proposed_pts.at(i));
if (this_diff < *diff
&& start_times.at(i) + *movement >= 0) {
*movement = compare_time - start_times.at(i);
*diff = this_diff;
}
}
}
}
rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVector<TimelineViewGhostItem *> ghosts)
{
foreach (TimelineViewGhostItem* ghost, ghosts) {
@@ -153,52 +128,6 @@ int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector<Time
return movement;
}
bool TimelineWidget::Tool::SnapPoint(QList<rational> start_times, rational* movement, int snap_points)
{
double diff = DBL_MAX;
QList<double> proposed_pts;
foreach (rational s, start_times) {
proposed_pts.append((s + *movement).toDouble() * parent()->GetScale());
}
if (snap_points & kSnapToPlayhead) {
rational playhead_abs_time = rational(parent()->GetTimestamp() * parent()->timebase().numerator(),
parent()->timebase().denominator());
qreal playhead_pos = playhead_abs_time.toDouble() * parent()->GetScale();
AttemptSnap(proposed_pts, playhead_pos, start_times, playhead_abs_time, movement, &diff);
}
if (snap_points & kSnapToClips) {
QMapIterator<Block*, TimelineViewBlockItem*> iterator(parent()->block_items_);
while (iterator.hasNext()) {
iterator.next();
TimelineViewBlockItem* item = iterator.value();
if (item != nullptr) {
qreal rect_left = item->x();
qreal rect_right = rect_left + item->rect().width();
// Attempt snapping to clip in point
AttemptSnap(proposed_pts, rect_left, start_times, item->block()->in(), movement, &diff);
// Attempt snapping to clip out point
AttemptSnap(proposed_pts, rect_right, start_times, item->block()->out(), movement, &diff);
}
}
}
return (diff < DBL_MAX);
}
void TimelineWidget::Tool::GetGhostData(const QVector<TimelineViewGhostItem *> &ghosts, rational *earliest_point, rational *latest_point)
{
rational ep = RATIONAL_MAX;
@@ -39,7 +39,9 @@ TimelineViewBase::TimelineViewBase(QWidget *parent) :
playhead_scene_left_(-1),
playhead_scene_right_(-1),
dragging_playhead_(false),
limit_y_axis_(false)
limit_y_axis_(false),
snapped_(false),
snap_service_(nullptr)
{
setScene(&scene_);
@@ -59,6 +61,26 @@ void TimelineViewBase::TimebaseChangedEvent(const rational &)
viewport()->update();
}
void TimelineViewBase::EnableSnap(const QList<rational> &points)
{
snapped_ = true;
snap_time_ = points;
viewport()->update();
}
void TimelineViewBase::DisableSnap()
{
snapped_ = false;
viewport()->update();
}
void TimelineViewBase::SetSnapService(SnapService *service)
{
snap_service_ = service;
}
void TimelineViewBase::SetTime(const int64_t time)
{
playhead_ = time;
@@ -91,6 +113,16 @@ void TimelineViewBase::drawForeground(QPainter *painter, const QRectF &rect)
playhead_style_.Draw(painter, QRectF(playhead_scene_left_, rect.top(), width, rect.height()));
}
if (snapped_) {
painter->setPen(palette().text().color());
foreach (const rational& r, snap_time_) {
double x = TimeToScene(r);
painter->drawLine(x, rect.top(), x, rect.height());
}
}
}
rational TimelineViewBase::GetPlayheadTime() const
@@ -118,6 +150,17 @@ bool TimelineViewBase::PlayheadMove(QMouseEvent *event)
int64_t target_ts = qMax(static_cast<int64_t>(0), Timecode::time_to_timestamp(mouse_time, timebase()));
if (Core::instance()->snapping() && snap_service_) {
rational target_time = Timecode::timestamp_to_time(target_ts, timebase());
rational movement;
snap_service_->SnapPoint({target_time}, &movement, SnapService::kSnapAll & ~SnapService::kSnapToPlayhead);
if (!movement.isNull()) {
target_ts = Timecode::time_to_timestamp(target_time + movement, timebase());
}
}
SetTime(target_ts);
emit TimeChanged(target_ts);
@@ -129,6 +172,10 @@ bool TimelineViewBase::PlayheadRelease(QMouseEvent*)
if (dragging_playhead_) {
dragging_playhead_ = false;
if (snap_service_) {
snap_service_->HideSnaps();
}
return true;
}
@@ -26,6 +26,7 @@
#include "core.h"
#include "handmovableview.h"
#include "timelineplayhead.h"
#include "widget/timelinewidget/snapservice.h"
#include "widget/timelinewidget/timelinescaledobject.h"
OLIVE_NAMESPACE_ENTER
@@ -38,6 +39,15 @@ public:
static const double kMaximumScale;
void EnableSnap(const QList<rational>& points);
void DisableSnap();
bool IsSnapped() const
{
return snapped_;
}
void SetSnapService(SnapService* service);
public slots:
void SetTime(const int64_t time);
@@ -89,8 +99,13 @@ private:
bool limit_y_axis_;
bool snapped_;
QList<rational> snap_time_;
rational end_time_;
SnapService* snap_service_;
private slots:
/**
* @brief Slot called whenever the view resizes or the scene contents change to enforce minimum scene sizes
+31 -1
View File
@@ -25,6 +25,7 @@
#include <QtMath>
#include "common/qtutils.h"
#include "core.h"
OLIVE_NAMESPACE_ENTER
@@ -32,7 +33,8 @@ SeekableWidget::SeekableWidget(QWidget* parent) :
TimelineScaledWidget(parent),
time_(0),
timeline_points_(nullptr),
scroll_(0)
scroll_(0),
snap_service_(nullptr)
{
QFontMetrics fm = fontMetrics();
@@ -59,6 +61,11 @@ void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points)
update();
}
void SeekableWidget::SetSnapService(SnapService *service)
{
snap_service_ = service;
}
const int64_t &SeekableWidget::GetTime() const
{
return time_;
@@ -81,6 +88,15 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event)
}
}
void SeekableWidget::mouseReleaseEvent(QMouseEvent *event)
{
Q_UNUSED(event)
if (snap_service_) {
snap_service_->HideSnaps();
}
}
void SeekableWidget::ScaleChangedEvent(const double &)
{
update();
@@ -134,6 +150,20 @@ void SeekableWidget::SeekToScreenPoint(int screen)
{
int64_t timestamp = qMax(static_cast<int64_t>(0), ScreenToUnitRounded(screen));
if (Core::instance()->snapping() && snap_service_) {
rational playhead_time = Timecode::timestamp_to_time(timestamp, timebase());
rational movement;
snap_service_->SnapPoint({playhead_time},
&movement,
SnapService::kSnapAll & ~SnapService::kSnapToPlayhead);
if (!movement.isNull()) {
timestamp = Timecode::time_to_timestamp(playhead_time + movement,
timebase());
}
}
SetTime(timestamp);
emit TimeChanged(timestamp);
+6
View File
@@ -23,6 +23,7 @@
#include "common/rational.h"
#include "timeline/timelinepoints.h"
#include "widget/timelinewidget/snapservice.h"
#include "widget/timelinewidget/view/timelineplayhead.h"
#include "widget/timelinewidget/timelinescaledobject.h"
@@ -40,6 +41,8 @@ public:
void ConnectTimelinePoints(TimelinePoints* points);
void SetSnapService(SnapService* service);
public slots:
void SetTime(const int64_t &r);
@@ -50,6 +53,7 @@ protected:
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
virtual void mouseReleaseEvent(QMouseEvent *event) override;
virtual void ScaleChangedEvent(const double&) override;
@@ -105,6 +109,8 @@ private:
int playhead_width_;
SnapService* snap_service_;
};
OLIVE_NAMESPACE_EXIT