diff --git a/src/plugins/score-lib-process/Process/Dataflow/NodeItem.cpp b/src/plugins/score-lib-process/Process/Dataflow/NodeItem.cpp index 4b8021b322..85f1e3b276 100644 --- a/src/plugins/score-lib-process/Process/Dataflow/NodeItem.cpp +++ b/src/plugins/score-lib-process/Process/Dataflow/NodeItem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -941,6 +942,8 @@ enum Interaction } nodeItemInteraction{}; QSizeF origNodeSize{}; bool nodeDidMove{}; +bool nodeSelectOnRelease{}; +bool nodeSelectCumulation{}; } void NodeItem::mousePressEvent(QGraphicsSceneMouseEvent* event) @@ -963,7 +966,21 @@ void NodeItem::mousePressEvent(QGraphicsSceneMouseEvent* event) { m_context.focusDispatcher.focus(m_presenter); } - score::SelectionDispatcher{m_context.selectionStack}.select(m_model); + { + const bool cumulation = event->modifiers() & Qt::ControlModifier; + if(m_model.selection.get()) + { + nodeSelectOnRelease = true; + nodeSelectCumulation = cumulation; + } + else + { + nodeSelectOnRelease = false; + Selection sel = filterSelections( + &m_model, m_context.selectionStack.currentSelection(), cumulation); + score::SelectionDispatcher{m_context.selectionStack}.select(sel); + } + } event->accept(); } else @@ -1028,7 +1045,16 @@ void NodeItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) mouseMoveEvent(event); if(nodeDidMove) + { m_dispatcher.commit(); + } + else if(nodeSelectOnRelease) + { + Selection sel = filterSelections( + &m_model, m_context.selectionStack.currentSelection(), nodeSelectCumulation); + score::SelectionDispatcher{m_context.selectionStack}.select(sel); + } + nodeSelectOnRelease = false; nodeDidMove = false; event->accept(); } diff --git a/src/plugins/score-plugin-nodal/Nodal/Presenter.cpp b/src/plugins/score-plugin-nodal/Nodal/Presenter.cpp index 40d7bf7e34..0135aa36fd 100644 --- a/src/plugins/score-plugin-nodal/Nodal/Presenter.cpp +++ b/src/plugins/score-plugin-nodal/Nodal/Presenter.cpp @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include @@ -31,6 +34,7 @@ Presenter::Presenter( bind(layer.nodes, *this); connect(view, &View::dropReceived, this, &Presenter::on_drop); + connect(view, &View::areaSelectRequested, this, &Presenter::on_areaSelect); if(auto itv = Scenario::closestParentInterval(m_model.parent())) { @@ -154,4 +158,17 @@ void Presenter::on_removing(const Process::ProcessModel& n) { m_nodes.erase(n.id()); } + +void Presenter::on_areaSelect(QRectF rect, bool cumulation) +{ + Selection sel; + for(Process::NodeItem& node : m_nodes) + { + const QRectF nodeRect = m_view->mapRectFromScene(node.sceneBoundingRect()); + if(rect.intersects(nodeRect)) + sel.append(node.model()); + } + sel = filterSelections(sel, m_context.context.selectionStack.currentSelection(), cumulation); + score::SelectionDispatcher{m_context.context.selectionStack}.select(sel); +} } diff --git a/src/plugins/score-plugin-nodal/Nodal/Presenter.hpp b/src/plugins/score-plugin-nodal/Nodal/Presenter.hpp index bd59692b87..292a5c011d 100644 --- a/src/plugins/score-plugin-nodal/Nodal/Presenter.hpp +++ b/src/plugins/score-plugin-nodal/Nodal/Presenter.hpp @@ -7,6 +7,9 @@ #include #include + +#include + namespace Nodal { class Model; @@ -33,6 +36,7 @@ class Presenter final void on_created(Process::ProcessModel& n); void on_removing(const Process::ProcessModel& n); void on_drop(const QPointF& pos, const QMimeData& mime); + void on_areaSelect(QRectF rect, bool cumulation); private: IdContainer m_nodes; diff --git a/src/plugins/score-plugin-nodal/Nodal/View.cpp b/src/plugins/score-plugin-nodal/Nodal/View.cpp index 27b289ac04..fe021f13e6 100644 --- a/src/plugins/score-plugin-nodal/Nodal/View.cpp +++ b/src/plugins/score-plugin-nodal/Nodal/View.cpp @@ -2,22 +2,87 @@ #include +#include #include #include + +#include +W_OBJECT_IMPL(Nodal::View) + namespace Nodal { View::View(QGraphicsItem* parent) : LayerView{parent} { - setFlag(ItemHasNoContents, true); setFlag(ItemClipsChildrenToShape); + + { + auto selColor = score::Skin::instance().Base2.darker.brush.color(); + auto selFill = selColor; + selFill.setAlpha(40); + m_selectionRect = new QGraphicsRectItem(this); + m_selectionRect->setZValue(1000.); + m_selectionRect->setPen(QPen{selColor, 1, Qt::DashLine, Qt::SquareCap, Qt::BevelJoin}); + m_selectionRect->setBrush(selFill); + m_selectionRect->setAcceptedMouseButtons(Qt::NoButton); + m_selectionRect->setVisible(false); + } } View::~View() { } void View::paint_impl(QPainter* painter) const { } +void View::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if(event->button() == Qt::LeftButton && (event->modifiers() & Qt::ControlModifier)) + { + m_rubberBanding = true; + m_rubberBandOrigin = event->pos(); + m_rubberBandRect = QRectF{m_rubberBandOrigin, m_rubberBandOrigin}; + m_selectionRect->setVisible(true); + m_selectionRect->setRect(QRectF{}); + event->accept(); + } + else + { + LayerView::mousePressEvent(event); + } +} + +void View::mouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if(m_rubberBanding && (event->buttons() & Qt::LeftButton)) + { + m_rubberBandRect = QRectF{m_rubberBandOrigin, event->pos()}.normalized(); + m_selectionRect->setRect(m_rubberBandRect); + event->accept(); + } + else + { + LayerView::mouseMoveEvent(event); + } +} + +void View::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if(event->button() == Qt::LeftButton && m_rubberBanding) + { + m_rubberBanding = false; + m_selectionRect->setVisible(false); + m_selectionRect->setRect(QRectF{}); + const bool cumulation = event->modifiers() & Qt::ControlModifier; + areaSelectRequested(m_rubberBandRect, cumulation); + m_rubberBandRect = {}; + event->accept(); + } + else + { + LayerView::mouseReleaseEvent(event); + } +} + void View::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { } void View::dragLeaveEvent(QGraphicsSceneDragDropEvent* event) { } diff --git a/src/plugins/score-plugin-nodal/Nodal/View.hpp b/src/plugins/score-plugin-nodal/Nodal/View.hpp index 132f229df7..eee72fc716 100644 --- a/src/plugins/score-plugin-nodal/Nodal/View.hpp +++ b/src/plugins/score-plugin-nodal/Nodal/View.hpp @@ -1,20 +1,42 @@ #pragma once #include +#include +#include + +class QGraphicsRectItem; + +#include + +#include + namespace Nodal { class View final : public Process::LayerView { + W_OBJECT(View) public: explicit View(QGraphicsItem* parent); ~View() override; + void areaSelectRequested(QRectF area, bool cumulation) + E_SIGNAL(SCORE_PLUGIN_NODAL_EXPORT, areaSelectRequested, area, cumulation) + private: void paint_impl(QPainter*) const override; + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + void dragEnterEvent(QGraphicsSceneDragDropEvent* event) override; void dragLeaveEvent(QGraphicsSceneDragDropEvent* event) override; void dragMoveEvent(QGraphicsSceneDragDropEvent* event) override; void dropEvent(QGraphicsSceneDragDropEvent* event) override; + + QGraphicsRectItem* m_selectionRect{}; + QPointF m_rubberBandOrigin{}; + QRectF m_rubberBandRect{}; + bool m_rubberBanding{false}; }; } diff --git a/src/plugins/score-plugin-scenario/Scenario/Commands/Interval/RemoveProcessFromInterval.hpp b/src/plugins/score-plugin-scenario/Scenario/Commands/Interval/RemoveProcessFromInterval.hpp index 27606b5aa9..01ebf50c2b 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Commands/Interval/RemoveProcessFromInterval.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Commands/Interval/RemoveProcessFromInterval.hpp @@ -51,5 +51,11 @@ class RemoveProcessAndKeepLinked final : public score::AggregateCommand SCORE_COMMAND_DECL( CommandFactoryName(), RemoveProcessAndKeepLinked, "Remove process from chain") }; + +class RemoveMultipleProcessesFromInterval final : public score::AggregateCommand +{ + SCORE_COMMAND_DECL( + CommandFactoryName(), RemoveMultipleProcessesFromInterval, "Remove processes") +}; } } diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.cpp index a18a8be005..f0fa83c676 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.cpp @@ -13,14 +13,18 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include +#include #include #include @@ -112,6 +116,18 @@ NodalIntervalView::NodalIntervalView( } QTimer::singleShot(1, this, &NodalIntervalView::recenterRelativeToView); + + { + auto selColor = score::Skin::instance().Base2.darker.brush.color(); + auto selFill = selColor; + selFill.setAlpha(40); + m_selectionRect = new QGraphicsRectItem(this); + m_selectionRect->setZValue(1000.); + m_selectionRect->setPen(QPen{selColor, 1, Qt::DashLine, Qt::SquareCap, Qt::BevelJoin}); + m_selectionRect->setBrush(selFill); + m_selectionRect->setAcceptedMouseButtons(Qt::NoButton); + m_selectionRect->setVisible(false); + } } void NodalIntervalView::zoomPlus() @@ -316,37 +332,73 @@ void NodalIntervalView::dropEvent(QGraphicsSceneDragDropEvent* event) void NodalIntervalView::mousePressEvent(QGraphicsSceneMouseEvent* e) { - this->m_context.selectionStack.deselect(); - auto focus = Process::ProcessFocusManager::get(this->m_context); - focus->focusNothing(); - - m_pressedPos = e->scenePos(); + if(e->button() == Qt::LeftButton) + { + if(e->modifiers() & Qt::ControlModifier) + { + m_rubberBanding = true; + m_rubberBandOrigin = e->pos(); + m_rubberBandRect = QRectF{m_rubberBandOrigin, m_rubberBandOrigin}; + m_selectionRect->setVisible(true); + m_selectionRect->setRect(QRectF{}); + } + else + { + this->m_context.selectionStack.deselect(); + auto focus = Process::ProcessFocusManager::get(this->m_context); + if(focus) + focus->focusNothing(); + m_pressedPos = e->scenePos(); + } + } + else if(e->button() == Qt::MiddleButton) + { + m_pressedPos = e->scenePos(); + } e->accept(); - - score::SelectionDispatcher disp{m_context.selectionStack}; - disp.select(m_model); } void NodalIntervalView::mouseMoveEvent(QGraphicsSceneMouseEvent* e) { - const auto delta = e->scenePos() - m_pressedPos; - m_container->setPos(m_container->pos() + delta); - m_pressedPos = e->scenePos(); + if(m_rubberBanding && (e->buttons() & Qt::LeftButton)) + { + m_rubberBandRect = QRectF{m_rubberBandOrigin, e->pos()}.normalized(); + m_selectionRect->setRect(m_rubberBandRect); + } + else if(e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) + { + const auto delta = e->scenePos() - m_pressedPos; + m_container->setPos(m_container->pos() + delta); + m_pressedPos = e->scenePos(); + const_cast(m_model).setNodalOffset(m_model.nodalOffset() + delta); + } e->accept(); - const_cast(m_model).setNodalOffset(m_model.nodalOffset() + delta); } void NodalIntervalView::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) { - const auto delta = e->scenePos() - m_pressedPos; + if(e->button() == Qt::LeftButton && m_rubberBanding) + { + m_rubberBanding = false; + m_selectionRect->setVisible(false); + m_selectionRect->setRect(QRectF{}); - m_container->setPos(m_container->pos() + delta); - m_pressedPos = e->scenePos(); + const bool cumulation = e->modifiers() & Qt::ControlModifier; + Selection sel; + for(auto* node : m_nodeItems) + { + const QRectF nodeRect = mapRectFromScene(node->sceneBoundingRect()); + if(m_rubberBandRect.intersects(nodeRect)) + sel.append(node->model()); + } + sel = filterSelections(sel, m_context.selectionStack.currentSelection(), cumulation); + score::SelectionDispatcher{m_context.selectionStack}.select(sel); + m_rubberBandRect = {}; + } e->accept(); - - const_cast(m_model).setNodalOffset(m_model.nodalOffset() + delta); } + void NodalIntervalView::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { event->accept(); diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.hpp b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.hpp index 8a620a14d7..3f87fe1cfe 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/Interval/FullView/NodalIntervalView.hpp @@ -5,6 +5,10 @@ #include +#include + +class QGraphicsRectItem; + #include namespace Scenario @@ -63,8 +67,13 @@ class NodalIntervalView final ItemsToShow m_itemsToShow{}; std::vector m_nodeItems; QGraphicsItem* m_container{}; + QGraphicsRectItem* m_selectionRect{}; QPointF m_pressedPos{}; double m_zoomLevel = 0; + + QPointF m_rubberBandOrigin{}; + QRectF m_rubberBandRect{}; + bool m_rubberBanding{false}; }; } diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioEditor.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioEditor.cpp index 3409e5ec6d..eca460f624 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioEditor.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioEditor.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include #include #include @@ -117,35 +119,46 @@ static bool pasteInScenario( auto& interval = m.createBox(sm, origin.date, TimeVal(origin.date.impl + t.impl), origin.y); + // Load each process into a slot, collecting old→new ID mapping for cable remapping. + // loadProcessInSlot always creates a view slot so processes are visible in the timeline. + std::vector> proc_id_map; for(auto& proc : processes) { - if(proc.IsObject()) - { - if(proc.HasMember(score::StringConstant().uuid)) - { - m.loadProcessInSlot(interval, proc); - } - } + if(!proc.IsObject() || !proc.HasMember(score::StringConstant().uuid)) + continue; + auto id_it = proc.FindMember("id"); + if(id_it == proc.MemberEnd()) + continue; + int32_t old_id = id_it->value.GetInt(); + if(auto* new_proc = m.loadProcessInSlot(interval, proc)) + proc_id_map.emplace_back(old_id, new_proc->id().val()); } + // Remap cable endpoints from old process IDs to new ones, then load. { - auto new_path = score::IDocument::path(interval).unsafePath(); - - // !!! FIXME this looks like it's not valid, use - // serializedCablesFromCableJson instead, no ? auto cables = JsonValue{cables_it->value}.to(); - - for(auto& cable : cables) - { - qDebug() << cable.second.source.unsafePath().toString(); - qDebug() << cable.second.sink.unsafePath().toString(); - } + auto new_path = score::IDocument::path(interval).unsafePath(); auto& document = score::IDocument::get(ctx.document); - for(auto& c : cables) + for(auto& [cable_id, cable_data] : cables) { - c.first = getStrongId(document.cables); + auto remap = [&](ObjectPath& path) { + auto& vec = path.vec(); + if(vec.empty()) + return; + for(auto& [old_id, new_id] : proc_id_map) + { + if(vec.front().id() == old_id) + { + vec.front() = ObjectIdentifier{vec.front().objectName(), new_id}; + break; + } + } + }; + remap(cable_data.source.unsafePath()); + remap(cable_data.sink.unsafePath()); + cable_id = getStrongId(document.cables); } m.loadCables(new_path, cables); } @@ -314,6 +327,39 @@ bool ScenarioEditor::paste( bool ScenarioEditor::remove(const Selection& s, const score::DocumentContext& ctx) { + if(s.size() > 1) + { + std::vector> procs; + procs.reserve(s.size()); + bool only_interval_processes = true; + for(auto& elt : s) + { + auto proc = qobject_cast(elt.data()); + if(!proc) + { + only_interval_processes = false; + break; + } + auto itv = qobject_cast(proc->parent()); + if(!itv) + { + only_interval_processes = false; + break; + } + procs.push_back({itv, proc}); + } + + if(only_interval_processes && !procs.empty()) + { + Scenario::Command::Macro m{ + new Scenario::Command::RemoveMultipleProcessesFromInterval, ctx}; + for(auto& [itv, proc] : procs) + m.removeProcess(*itv, proc->id()); + m.commit(); + return true; + } + } + if(s.size() == 1) { auto first = s.begin()->data();