diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index 5fb5fb1fd..cc770e894 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -204,9 +204,7 @@ void TimelineMarkerList::HandleMarkerTimeChange() auto it = std::find(markers_.begin(), markers_.end(), m); - if ((it+1 != markers_.end() && (*(it+1))->time() < m->time()) - || (it != markers_.begin() && (*(it-1))->time() > m->time())) { - // Re-sort into list + if (it != markers_.end()) { markers_.erase(it); InsertIntoList(m); } @@ -304,8 +302,9 @@ void MarkerChangeNameCommand::undo() marker_->set_name(old_name_); } -MarkerChangeTimeCommand::MarkerChangeTimeCommand(TimelineMarker* marker, TimeRange time) : +MarkerChangeTimeCommand::MarkerChangeTimeCommand(TimelineMarker* marker, const TimeRange &time, const TimeRange &old_time) : marker_(marker), + old_time_(old_time), new_time_(time) { } diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h index 492c61b9b..ae7cc4484 100644 --- a/app/timeline/timelinemarker.h +++ b/app/timeline/timelinemarker.h @@ -227,7 +227,10 @@ private: class MarkerChangeTimeCommand : public UndoCommand { public: - MarkerChangeTimeCommand(TimelineMarker* marker, TimeRange time); + MarkerChangeTimeCommand(TimelineMarker* marker, const TimeRange &time, const TimeRange &old_time); + MarkerChangeTimeCommand(TimelineMarker* marker, const TimeRange &time) : + MarkerChangeTimeCommand(marker, time, marker->time_range()) + {} virtual Project* GetRelevantProject() const override; diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index 106eef182..8c8a0cb6a 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -195,8 +195,26 @@ public: rational time_diff = view_->SceneToTimeNoGrid(view_->mapToScene(event->pos()).x() - drag_mouse_start_.x()); // Snap points + rational presnap_time_diff = time_diff; SnapPoints(&time_diff); + // Validate snapping + if (Core::instance()->snapping() && view_->GetSnapService()) { + for (size_t i=0; ihas_sibling_at_time(proposed_time)) { + // Unsnap + time_diff = presnap_time_diff; + if (view_->GetSnapService()) { + view_->GetSnapService()->HideSnaps(); + } + break; + } + } + } + // Validate movement for (size_t i=0; iadd_child(new WorkareaSetRangeCommand(viewer_node_->project(), points, TimeRange(in_point, out_point))); + command->add_child(new WorkareaSetRangeCommand(points->workarea(), TimeRange(in_point, out_point))); Core::instance()->undo_stack()->push(command); } @@ -511,7 +511,7 @@ void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) r.set_out(TimelineWorkArea::kResetOut); } - Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(viewer_node_->project(), points, r)); + Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points->workarea(), r)); } void TimeBasedWidget::PageScrollInternal(QScrollBar *bar, int maximum, int screen_position, bool whole_page_scroll) @@ -772,6 +772,11 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration for (auto it=ruler()->GetTimelinePoints()->markers()->cbegin(); it!=ruler()->GetTimelinePoints()->markers()->cend(); it++) { TimelineMarker* m = *it; + // Ignore selected markers + if (std::find(ruler()->GetSelectedMarkers().cbegin(), ruler()->GetSelectedMarkers().cend(), m) != ruler()->GetSelectedMarkers().cend()) { + continue; + } + qreal marker_pos = TimeToScene(m->time_range().in()); AttemptSnap(potential_snaps, screen_pt, marker_pos, start_times, m->time_range().in()); diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index b2c2dee75..3555cad98 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -141,6 +141,7 @@ protected: virtual const QVector *GetSnapBlocks() const { return nullptr; } virtual const QVector *GetSnapKeyframes() const { return nullptr; } virtual const std::vector *GetSnapIgnoreKeyframes() const { return nullptr; } + virtual const std::vector *GetSnapIgnoreMarkers() const { return nullptr; } protected slots: /** @@ -164,8 +165,6 @@ signals: void ConnectedNodeChanged(ViewerOutput* old, ViewerOutput* now); private: - - /** * @brief Set either in or out point to the current playhead * diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.h b/app/widget/timelinewidget/undo/timelineundoworkarea.h index c431178ec..a7b1e2655 100644 --- a/app/widget/timelinewidget/undo/timelineundoworkarea.h +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -65,34 +65,36 @@ private: class WorkareaSetRangeCommand : public UndoCommand { public: - WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : - project_(project), - points_(points), - old_range_(points_->workarea()->range()), + WorkareaSetRangeCommand(TimelineWorkArea *workarea, const TimeRange& range, const TimeRange &old_range) : + workarea_(workarea), + old_range_(old_range), new_range_(range) { } + WorkareaSetRangeCommand(TimelineWorkArea *workarea, const TimeRange& range) : + WorkareaSetRangeCommand(workarea, range, workarea->range()) + { + } + virtual Project* GetRelevantProject() const override { - return project_; + return Project::GetProjectFromObject(workarea_); } protected: virtual void redo() override { - points_->workarea()->set_range(new_range_); + workarea_->set_range(new_range_); } virtual void undo() override { - points_->workarea()->set_range(old_range_); + workarea_->set_range(old_range_); } private: - Project* project_; - - TimelinePoints* points_; + TimelineWorkArea *workarea_; TimeRange old_range_; diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 5af1f030a..8e0a657b5 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -26,12 +26,14 @@ #include #include "common/qtutils.h" +#include "common/range.h" #include "core.h" #include "dialog/markerproperties/markerpropertiesdialog.h" #include "node/project/serializer/serializer.h" #include "widget/colorlabelmenu/colorlabelmenu.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedwidget.h" +#include "widget/timelinewidget/undo/timelineundoworkarea.h" namespace olive { @@ -42,7 +44,10 @@ SeekableWidget::SeekableWidget(QWidget* parent) : timeline_points_(nullptr), dragging_(false), ignore_next_focus_out_(false), - selection_manager_(this) + selection_manager_(this), + resize_item_(nullptr), + marker_top_(0), + marker_bottom_(0) { QFontMetrics fm = fontMetrics(); @@ -53,8 +58,9 @@ SeekableWidget::SeekableWidget(QWidget* parent) : setContextMenuPolicy(Qt::CustomContextMenu); setFocusPolicy(Qt::ClickFocus); + setMouseTracking(true); - selection_manager_.SetSnapMask(TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToMarkers); + selection_manager_.SetSnapMask(TimeBasedWidget::kSnapAll); } void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) @@ -159,7 +165,17 @@ bool SeekableWidget::PasteMarkers(bool insert, rational insert_time) void SeekableWidget::mousePressEvent(QMouseEvent *event) { - if (TimelineMarker *initial = selection_manager_.MousePress(event)) { + if (resize_item_) { + // Handle selection, even though we won't be using it for dragging + if (!(event->modifiers() & Qt::ShiftModifier)) { + selection_manager_.ClearSelection(); + } + if (TimelineMarker *m = dynamic_cast(resize_item_)) { + selection_manager_.Select(m); + } + dragging_ = true; + resize_start_ = mapToScene(event->pos()); + } else if (TimelineMarker *initial = selection_manager_.MousePress(event)) { selection_manager_.DragStart(initial, event); } else if (!selection_manager_.GetObjectAtPoint(event->pos()) && event->button() == Qt::LeftButton) { SeekToScenePoint(mapToScene(event->pos()).x()); @@ -174,7 +190,15 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event) if (selection_manager_.IsDragging()) { selection_manager_.DragMove(event); } else if (dragging_) { - SeekToScenePoint(mapToScene(event->pos()).x()); + QPointF scene = mapToScene(event->pos()); + if (resize_item_) { + DragResizeHandle(scene); + } else { + SeekToScenePoint(scene.x()); + } + } else if (timeline_points_) { + // Look for resize points + setCursor(FindResizeHandle(event) ? Qt::SizeHorCursor : Qt::ArrowCursor); } } @@ -190,6 +214,11 @@ void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) GetSnapService()->HideSnaps(); } + if (resize_item_) { + CommitResizeHandle(); + resize_item_ = nullptr; + } + dragging_ = false; } @@ -326,9 +355,12 @@ void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) } QRect marker_rect = marker->Draw(p, QPoint(marker_left, marker_bottom), GetScale(), selection_manager_.IsSelected(marker)); + marker_top_ = marker_rect.top(); selection_manager_.DeclareDrawnObject(marker, marker_rect); } } + + marker_bottom_ = marker_bottom; } void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) @@ -384,4 +416,115 @@ bool SeekableWidget::ShowContextMenu(const QPoint &p) } } +bool SeekableWidget::FindResizeHandle(QMouseEvent *event) +{ + resize_item_ = nullptr; + resize_mode_ = kResizeNone; + + QPointF scene = mapToScene(event->pos()); + const int border = 10; + rational min = SceneToTimeNoGrid(scene.x() - border); + rational max = SceneToTimeNoGrid(scene.x() + border); + + // Test for workarea + if (timeline_points_->workarea()->in() >= min && timeline_points_->workarea()->in() < max) { + resize_mode_ = kResizeIn; + } else if (timeline_points_->workarea()->out() >= min && timeline_points_->workarea()->out() < max) { + resize_mode_ = kResizeOut; + } + + if (resize_mode_ != kResizeNone) { + resize_item_ = timeline_points_->workarea(); + resize_item_range_ = timeline_points_->workarea()->range(); + resize_snap_mask_ = TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToWorkarea; + } else if (event->pos().y() >= marker_top_ && event->pos().y() < marker_bottom_) { + // Check for markers + for (auto it=timeline_points_->markers()->cbegin(); it!=timeline_points_->markers()->cend(); it++) { + TimelineMarker *m = *it; + if (m->time_range().in() != m->time_range().out()) { + if (m->time_range().in() >= min && m->time_range().in() < max) { + resize_mode_ = kResizeIn; + } else if (m->time_range().out() >= min && m->time_range().out() < max) { + resize_mode_ = kResizeOut; + } + + if (resize_mode_ != kResizeNone) { + resize_item_ = m; + resize_item_range_ = m->time_range(); + resize_snap_mask_ = TimeBasedWidget::kSnapAll; + break; + } + } + } + } + + return resize_item_; +} + +void SeekableWidget::DragResizeHandle(const QPointF &scene) +{ + qreal diff = scene.x() - resize_start_.x(); + + rational proposed_time; + + if (resize_mode_ == kResizeIn) { + proposed_time = qMax(rational(0), qMin(resize_item_range_.out(), resize_item_range_.in() + SceneToTimeNoGrid(diff))); + } else { + proposed_time = qMax(resize_item_range_.in(), resize_item_range_.out() + SceneToTimeNoGrid(diff)); + } + + rational presnap_time = proposed_time; + + if (Core::instance()->snapping() && GetSnapService()) { + rational movement; + + GetSnapService()->SnapPoint({proposed_time}, &movement, resize_snap_mask_); + + proposed_time += movement; + } + + TimeRange new_range = resize_item_range_; + if (resize_mode_ == kResizeIn) { + // Markers should not have the same time as anything else + // NOTE: This code is largely duplicated from TimeBasedViewSelectionManager::DragMove. Not ideal, + // but I'm not sure if there's a good way to re-use that code + if (TimelineMarker *marker = dynamic_cast(resize_item_)) { + if (marker->has_sibling_at_time(proposed_time)) { + proposed_time = presnap_time; + + if (GetSnapService()) { + GetSnapService()->HideSnaps(); + } + } + + while (marker->has_sibling_at_time(proposed_time)) { + proposed_time += rational(1, 1000); + } + } + + new_range.set_in(proposed_time); + } else { + new_range.set_out(proposed_time); + } + + if (TimelineMarker *marker = dynamic_cast(resize_item_)) { + marker->set_time(new_range); + } else if (TimelineWorkArea *workarea = dynamic_cast(resize_item_)) { + workarea->set_range(new_range); + } +} + +void SeekableWidget::CommitResizeHandle() +{ + MultiUndoCommand *command = new MultiUndoCommand(); + + if (TimelineMarker *marker = dynamic_cast(resize_item_)) { + command->add_child(new MarkerChangeTimeCommand(marker, marker->time_range(), resize_item_range_)); + } else if (TimelineWorkArea *workarea = dynamic_cast(resize_item_)) { + command->add_child(new WorkareaSetRangeCommand(workarea, workarea->range(), resize_item_range_)); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + } diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index 1bc8e61de..e151bbc4d 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -60,6 +60,11 @@ public: void SeekToScenePoint(qreal scene); + const std::vector &GetSelectedMarkers() const + { + return selection_manager_.GetSelectedObjects(); + } + virtual void SelectionManagerSelectEvent(void *obj) override; virtual void SelectionManagerDeselectEvent(void *obj) override; @@ -95,6 +100,18 @@ protected slots: virtual bool ShowContextMenu(const QPoint &p); private: + enum ResizeMode { + kResizeNone, + kResizeIn, + kResizeOut + }; + + bool FindResizeHandle(QMouseEvent *event); + + void DragResizeHandle(const QPointF &scene_pos); + + void CommitResizeHandle(); + TimelinePoints* timeline_points_; int text_height_; @@ -107,6 +124,15 @@ private: TimeBasedViewSelectionManager selection_manager_; + QObject *resize_item_; + ResizeMode resize_mode_; + TimeRange resize_item_range_; + QPointF resize_start_; + uint32_t resize_snap_mask_; + + int marker_top_; + int marker_bottom_; + private slots: void SetMarkerColor(int c);