/*** 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 . ***/ #ifndef TIMEBASEDVIEWSELECTIONMANAGER_H #define TIMEBASEDVIEWSELECTIONMANAGER_H #include #include #include #include #include "common/qtutils.h" #include "timebasedview.h" #include "timebasedwidget.h" #include "widget/timetarget/timetarget.h" namespace olive { template class TimeBasedViewSelectionManager { public: TimeBasedViewSelectionManager(TimeBasedView *view) : view_(view) , rubberband_(nullptr) , snap_mask_(TimeBasedWidget::kSnapAll) { } void SetSnapMask(TimeBasedWidget::SnapMask e) { snap_mask_ = e; } void ClearDrawnObjects() { drawn_objects_.clear(); } void DeclareDrawnObject(T *object, const QRectF &rect) { QRectF r(view_->UnscalePoint(rect.topLeft()), view_->UnscalePoint(rect.bottomRight())); drawn_objects_.push_back({ object, r }); } bool Select(T *key) { Q_ASSERT(key); if (!IsSelected(key)) { selected_.push_back(key); return true; } return false; } bool Deselect(T *key) { Q_ASSERT(key); auto it = std::find(selected_.cbegin(), selected_.cend(), key); if (it == selected_.cend()) { return false; } else { selected_.erase(it); return true; } } void ClearSelection() { selected_.clear(); } bool IsSelected(T *key) const { return std::find(selected_.cbegin(), selected_.cend(), key) != selected_.cend(); } const std::vector &GetSelectedObjects() const { return selected_; } void SetTimebase(const rational &tb) { timebase_ = tb; } T *GetObjectAtPoint(const QPointF &scene_pt) { // Iterate in reverse order because the objects drawn later will appear on top to the user QPointF unscaled = view_->UnscalePoint(scene_pt); for (auto it = drawn_objects_.crbegin(); it != drawn_objects_.crend(); it++) { const DrawnObject &kp = *it; if (kp.second.contains(unscaled)) { return kp.first; } } return nullptr; } T *GetObjectAtPoint(const QPoint &pt) { return GetObjectAtPoint(view_->mapToScene(pt)); } T *MousePress(QMouseEvent *event) { T *key_under_cursor = nullptr; if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) { // See if there's a keyframe in this position key_under_cursor = GetObjectAtPoint(event->pos()); bool holding_shift = event->modifiers() & Qt::ShiftModifier; if (!key_under_cursor || !IsSelected(key_under_cursor)) { if (!holding_shift) { // If not already selecting and not holding shift, clear the current selection ClearSelection(); } // Add item to selection, either nothing if shift wasn't held, or the existing selection if (key_under_cursor) { Select(key_under_cursor); view_->SelectionManagerSelectEvent(key_under_cursor); } } else if (holding_shift) { // If selected and holding shift, de-select this item but do nothing else Deselect(key_under_cursor); view_->SelectionManagerDeselectEvent(key_under_cursor); key_under_cursor = nullptr; } } return key_under_cursor; } bool IsDragging() const { return !dragging_.empty(); } void DragStart(T *initial_item, QMouseEvent *event, TimeTargetObject *target = nullptr) { if (event->button() != Qt::LeftButton) { return; } time_target_ = target; initial_drag_item_ = initial_item; dragging_.resize(selected_.size()); if constexpr (std::is_same_v) { snap_points_.resize(selected_.size() * 2); } else { snap_points_.resize(selected_.size()); } if (target) { time_targets_.resize(snap_points_.size()); memset(time_targets_.data(), 0, time_targets_.size() * sizeof(Node *)); } else { time_targets_.clear(); } for (size_t i = 0; i < selected_.size(); i++) { T *obj = selected_.at(i); if constexpr (std::is_same_v) { dragging_[i] = obj->time().in(); snap_points_[i] = obj->time().in(); snap_points_[i + selected_.size()] = obj->time().out(); if (target) { time_targets_[i] = time_targets_[i + selected_.size()] = QtUtils::GetParentOfType(obj); } } else { dragging_[i] = obj->time(); snap_points_[i] = obj->time(); if (target) { time_targets_[i] = QtUtils::GetParentOfType(obj); } } } drag_mouse_start_ = view_->UnscalePoint(view_->mapToScene(event->pos())); } void SnapPoints(rational *movement) { std::vector copy = snap_points_; if (time_target_) { for (size_t i = 0; i < copy.size(); i++) { if (Node *parent = time_targets_[i]) { copy[i] = time_target_->GetAdjustedTime( parent, time_target_->GetTimeTarget(), copy[i], Node::kTransformTowardsOutput); } } } if (Core::instance()->snapping() && view_->GetSnapService()) { view_->GetSnapService()->SnapPoint(copy, movement, snap_mask_); } } void Unsnap() { if (view_->GetSnapService()) { view_->GetSnapService()->HideSnaps(); } } void DragMove(const QPoint &local_pos, const QString &tip_format = QString()) { rational time_diff = view_->SceneToTimeNoGrid(view_->mapToScene(local_pos).x() - view_->ScalePoint(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; i < selected_.size(); i++) { rational proposed_time = dragging_.at(i) + time_diff; T *sel = selected_.at(i); if (sel->has_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; i < selected_.size(); i++) { rational proposed_time = dragging_.at(i) + time_diff; T *sel = selected_.at(i); // Magic number: use interval of 1ms to avoid collisions rational adj(1, 1000); if (dragging_.at(i) < proposed_time) { // Negate adjustment value if origin is less than proposed time adj = -adj; } bool loop; do { loop = false; while (sel->has_sibling_at_time(proposed_time)) { proposed_time += adj; Unsnap(); } if (proposed_time < 0) { // Prevent any object from going below zero proposed_time = 0; Unsnap(); // Setting our proposed time to zero may (re)introduce a conflict that we just avoided // with the sibling check above, so we request it to happen again. To avoid a negative // adj bringing us back below zero, we force adj to positive so it'll only nudge higher adj = qAbs(adj); loop = true; } } while (loop); time_diff = proposed_time - dragging_.at(i); } // Apply movement for (size_t i = 0; i < selected_.size(); i++) { selected_.at(i)->set_time(dragging_.at(i) + time_diff); } // Show information about this keyframe rational display_time; if constexpr (std::is_same_v) { display_time = initial_drag_item_->time().in(); } else { display_time = initial_drag_item_->time(); } QString tip = QString::fromStdString(Timecode::time_to_timecode( display_time, timebase_, Core::instance()->GetTimecodeDisplay(), false)); last_used_tip_format_ = tip_format; if (!tip_format.isEmpty()) { tip = tip_format.arg(tip); } QToolTip::hideText(); QToolTip::showText(QCursor::pos(), tip); } void DragStop(MultiUndoCommand *command) { QToolTip::hideText(); for (size_t i = 0; i < selected_.size(); i++) { rational current; if constexpr (std::is_same_v) { current = selected_.at(i)->time().in(); } else { current = selected_.at(i)->time(); } command->add_child( new SetTimeCommand(selected_.at(i), current, dragging_.at(i))); } dragging_.clear(); Unsnap(); } void RubberBandStart(QMouseEvent *event) { if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) { rubberband_scene_start_ = view_->UnscalePoint(view_->mapToScene(event->pos())); rubberband_ = new QRubberBand(QRubberBand::Rectangle, view_); rubberband_->setGeometry( QRect(event->pos().x(), event->pos().y(), 0, 0)); rubberband_->show(); rubberband_preselected_ = selected_; } } void RubberBandMove(const QPoint &pos) { if (IsRubberBanding()) { QRectF band_rect = QRectF(view_->mapFromScene(view_->ScalePoint( rubberband_scene_start_)), pos) .normalized(); rubberband_->setGeometry(band_rect.toRect()); QPointF current = view_->UnscalePoint(view_->mapToScene(pos)); QRectF scene_rect = QRectF(rubberband_scene_start_, current).normalized(); selected_ = rubberband_preselected_; foreach (const DrawnObject &kp, drawn_objects_) { if (scene_rect.intersects(kp.second)) { Select(kp.first); } } } } void RubberBandStop() { if (IsRubberBanding()) { delete rubberband_; rubberband_ = nullptr; } } bool IsRubberBanding() const { return rubberband_; } void ForceDragUpdate() { if (IsRubberBanding() || IsDragging()) { QPoint local_pos = view_->viewport()->mapFromGlobal(QCursor::pos()); if (IsRubberBanding()) { RubberBandMove(local_pos); } else { DragMove(local_pos, last_used_tip_format_); } } } private: class SetTimeCommand : public UndoCommand { public: SetTimeCommand(T *key, const rational &time) { key_ = key; new_time_ = time; old_time_ = key_->time(); } SetTimeCommand(T *key, const rational &new_time, const rational &old_time) { key_ = key; new_time_ = new_time; old_time_ = old_time; } virtual Project *GetRelevantProject() const override { return Project::GetProjectFromObject(key_); } protected: virtual void redo() override { key_->set_time(new_time_); } virtual void undo() override { key_->set_time(old_time_); } private: T *key_; rational old_time_; rational new_time_; }; TimeBasedView *view_; using DrawnObject = QPair; std::vector drawn_objects_; std::vector selected_; std::vector dragging_; std::vector snap_points_; std::vector time_targets_; T *initial_drag_item_; QPointF drag_mouse_start_; rational timebase_; QRubberBand *rubberband_; QPointF rubberband_scene_start_; std::vector rubberband_preselected_; TimeBasedWidget::SnapMask snap_mask_; TimeTargetObject *time_target_; QString last_used_tip_format_; }; } #endif // TIMEBASEDVIEWSELECTIONMANAGER_H