diff --git a/src/engraving/dom/cmd.cpp b/src/engraving/dom/cmd.cpp index 0e975b5c5b507..b9b40d28715ef 100644 --- a/src/engraving/dom/cmd.cpp +++ b/src/engraving/dom/cmd.cpp @@ -598,7 +598,6 @@ void Score::cmdAddSpanner(Spanner* spanner, const PointF& pos, bool systemStaves bool ctrlModifier = isSystemTextLine(spanner) && !systemStavesOnly; undoAddElement(spanner, true /*addToLinkedStaves*/, ctrlModifier); - select(spanner, SelectType::SINGLE, 0); } //--------------------------------------------------------- diff --git a/src/engraving/dom/dynamic.cpp b/src/engraving/dom/dynamic.cpp index 2814c47b3f64d..08093c7ac18d3 100644 --- a/src/engraving/dom/dynamic.cpp +++ b/src/engraving/dom/dynamic.cpp @@ -38,7 +38,7 @@ #include "log.h" -using namespace mu; +using namespace muse::draw; using namespace mu::engraving; namespace mu::engraving { @@ -532,6 +532,39 @@ TranslatableString Dynamic::subtypeUserName() const } } +void Dynamic::editDrag(EditData& ed) +{ + const bool hasLeftGrip = this->hasLeftGrip(); + const bool hasRightGrip = this->hasRightGrip(); + + // Right grip (when two grips/when single grip) + if ((int(ed.curGrip) == 1 && hasLeftGrip && hasRightGrip) || (int(ed.curGrip) == 0 && !hasLeftGrip && hasRightGrip)) { + m_rightDragOffset += ed.evtDelta.x(); + if (m_rightDragOffset < 0) { + m_rightDragOffset = 0; + } + return; + } + + // Left grip (when two grips or single grip) + if (int(ed.curGrip) == 0 && hasLeftGrip) { + m_leftDragOffset += ed.evtDelta.x(); + if (m_leftDragOffset > 0) { + m_leftDragOffset = 0; + } + return; + } + + TextBase::editDrag(ed); +} + +void Dynamic::endEditDrag(EditData& ed) +{ + m_leftDragOffset = m_rightDragOffset = 0.0; + + TextBase::endEditDrag(ed); +} + //--------------------------------------------------------- // reset //--------------------------------------------------------- @@ -717,3 +750,115 @@ String Dynamic::screenReaderInfo() const return String(u"%1: %2").arg(EngravingItem::accessibleInfo(), s); } } + +//--------------------------------------------------------- +// drawEditMode +//--------------------------------------------------------- + +void Dynamic::drawEditMode(Painter* p, EditData& ed, double currentViewScaling) +{ + if (ed.editTextualProperties) { + TextBase::drawEditMode(p, ed, currentViewScaling); + } else { + EngravingItem::drawEditMode(p, ed, currentViewScaling); + } +} + +//--------------------------------------------------------- +// hasLeftHairpin +//--------------------------------------------------------- + +bool Dynamic::hasLeftGrip() const +{ + if (segment()->tick().isZero()) { + return false; // Don't show the left grip for the leftmost dynamic with tick zero + } + return m_leftHairpin == nullptr; +} + +//--------------------------------------------------------- +// hasRightHairpin +//--------------------------------------------------------- + +bool Dynamic::hasRightGrip() const +{ + return m_rightHairpin == nullptr; +} + +//--------------------------------------------------------- +// findAdjacentHairpins +//--------------------------------------------------------- + +void Dynamic::findAdjacentHairpins() +{ + m_leftHairpin = nullptr; + m_rightHairpin = nullptr; + + const Fraction tick = segment()->tick(); + const int intTick = tick.ticks(); + + const auto& spanners = score()->spannerMap().findOverlapping(intTick - 1, intTick + 1); + for (auto i : spanners) { + Spanner* sp = i.value; + if (sp->track() == track() && sp->isHairpin()) { + Hairpin* hp = toHairpin(sp); + if (hp->tick() == tick) { + m_rightHairpin = hp; + } else if (hp->tick2() == tick) { + m_leftHairpin = hp; + } + } + } +} + +//--------------------------------------------------------- +// gripsCount +//--------------------------------------------------------- + +int Dynamic::gripsCount() const +{ + if (empty()) { + return 0; + } + + const bool hasLeftGrip = this->hasLeftGrip(); + const bool hasRightGrip = this->hasRightGrip(); + + if (hasLeftGrip && hasRightGrip) { + return 2; + } else if (hasLeftGrip || hasRightGrip) { + return 1; + } else { + return 0; + } +} + +//--------------------------------------------------------- +// gripsPositions +//--------------------------------------------------------- + +std::vector Dynamic::gripsPositions(const EditData&) const +{ + const LayoutData* ldata = this->ldata(); + const PointF pp(pagePos()); + double md = score()->style().styleS(Sid::hairpinMinDistance).val() * spatium(); // Minimum distance between dynamic and grip + + // Calculated by subtracting the y-value of the dynamic's pagePos from the y-value of hairpin's Grip::START position in HairpinSegment::gripsPositions + const double GRIP_VERTICAL_OFFSET = -11.408; + + PointF leftOffset(-ldata->bbox().width() / 2 - md + m_leftDragOffset, GRIP_VERTICAL_OFFSET); + PointF rightOffset(ldata->bbox().width() / 2 + md + m_rightDragOffset, GRIP_VERTICAL_OFFSET); + + const bool hasLeftGrip = this->hasLeftGrip(); + const bool hasRightGrip = this->hasRightGrip(); + + if (hasLeftGrip && hasRightGrip) { + return { pp + leftOffset, pp + rightOffset }; + } else if (hasLeftGrip) { + return { pp + leftOffset }; + } else if (hasRightGrip) { + return { pp + rightOffset }; + } else { + return {}; + } +} diff --git a/src/engraving/dom/dynamic.h b/src/engraving/dom/dynamic.h index 27e24976d9b56..7073dd8e75839 100644 --- a/src/engraving/dom/dynamic.h +++ b/src/engraving/dom/dynamic.h @@ -25,6 +25,10 @@ #include "textbase.h" +namespace muse::draw { +class Painter; +} + namespace mu::engraving { class Measure; class Segment; @@ -114,6 +118,26 @@ class Dynamic final : public TextBase bool hasVoiceAssignmentProperties() const override { return true; } + int gripsCount() const override; + std::vector gripsPositions(const EditData& = EditData()) const override; + void editDrag(EditData& editData) override; + void endEditDrag(EditData&) override; + void drawEditMode(muse::draw::Painter* painter, EditData& editData, double currentViewScaling) override; + + Hairpin* leftHairpin() const { return m_leftHairpin; } + Hairpin* rightHairpin() const { return m_rightHairpin; } + + bool hasLeftGrip() const; + bool hasRightGrip() const; + + void resetLeftDragOffset() { m_leftDragOffset = 0.0; } + void resetRightDragOffset() { m_rightDragOffset = 0.0; } + + double leftDragOffset() const { return m_leftDragOffset; } + double rightDragOffset() const { return m_rightDragOffset; } + + void findAdjacentHairpins(); + private: M_PROPERTY(bool, avoidBarLines, setAvoidBarLines) @@ -132,6 +156,12 @@ class Dynamic final : public TextBase DynamicSpeed m_velChangeSpeed = DynamicSpeed::NORMAL; static const std::vector DYN_LIST; + + double m_leftDragOffset = 0.0; + double m_rightDragOffset = 0.0; + + Hairpin* m_leftHairpin = nullptr; + Hairpin* m_rightHairpin = nullptr; }; } // namespace mu::engraving diff --git a/src/engraving/dom/edit.cpp b/src/engraving/dom/edit.cpp index 7a00b9a3784da..c44a10eeb8399 100644 --- a/src/engraving/dom/edit.cpp +++ b/src/engraving/dom/edit.cpp @@ -27,6 +27,7 @@ #include "infrastructure/messagebox.h" #include "accidental.h" +#include "anchors.h" #include "articulation.h" #include "barline.h" #include "beam.h" @@ -3964,6 +3965,45 @@ void Score::addHairpinToDynamic(Hairpin* hairpin, Dynamic* dynamic) undoAddElement(hairpin); } +Hairpin* Score::addHairpinToDynamicOnGripDrag(Dynamic* dynamic, bool isLeftGrip, const PointF& pos) +{ + const track_idx_t track = dynamic->track(); + staff_idx_t staffIndex = dynamic->staffIdx(); + Segment* seg = nullptr; + constexpr double spacingFactor = 0.5; + + // Ensure time tick segments are created + EditTimeTickAnchors::updateAnchors(dynamic, track); + + // Find segment of type ChordRest or TimeTick near cursor postion + dragPosition(pos, &staffIndex, &seg, spacingFactor, /*allowTimeAnchor*/ true); + + const bool hasValidTick = seg && (isLeftGrip + ? seg->tick() < dynamic->tick() + : seg->tick() > dynamic->tick()); + if (!hasValidTick) { + return nullptr; + } + + Hairpin* hairpin = Factory::createHairpin(dummy()->segment()); + hairpin->setHairpinType(isLeftGrip ? HairpinType::DECRESC_HAIRPIN : HairpinType::CRESC_HAIRPIN); + + hairpin->setTrack(track); + hairpin->setTrack2(track); + + if (isLeftGrip) { + hairpin->setTick(seg->tick()); + hairpin->setTick2(dynamic->tick()); + } else { + hairpin->setTick(dynamic->tick()); + hairpin->setTick2(seg->tick()); + } + + undoAddElement(hairpin); + + return hairpin; +} + //--------------------------------------------------------- // cmdCreateTuplet // replace cr with tuplet diff --git a/src/engraving/dom/editdata.h b/src/engraving/dom/editdata.h index f3a796f952978..1a571862bcbba 100644 --- a/src/engraving/dom/editdata.h +++ b/src/engraving/dom/editdata.h @@ -213,6 +213,7 @@ DECLARE_OPERATORS_FOR_FLAGS(MouseButtons) enum class Grip { NO_GRIP = -1, START = 0, END = 1, // arpeggio etc. + LEFT = START, RIGHT = END, // aliases for dynamic MIDDLE = 2, APERTURE = 3, // Line /*START, END , */ BEZIER1 = 2, SHOULDER = 3, BEZIER2 = 4, DRAG = 5, // Slur diff --git a/src/engraving/dom/score.h b/src/engraving/dom/score.h index a014df7ba8b40..48e223339b313 100644 --- a/src/engraving/dom/score.h +++ b/src/engraving/dom/score.h @@ -98,6 +98,7 @@ class Bracket; class Chord; class ChordRest; class Clef; +class Dynamic; class Element; class EventsHolder; class Excerpt; @@ -932,6 +933,7 @@ class Score : public EngravingObject, public muse::Injectable Hairpin* addHairpin(HairpinType type, ChordRest* cr1, ChordRest* cr2 = nullptr); void addHairpin(Hairpin* hairpin, ChordRest* cr1, ChordRest* cr2 = nullptr); void addHairpinToDynamic(Hairpin* hairpin, Dynamic* dynamic); + Hairpin* addHairpinToDynamicOnGripDrag(Dynamic* dynamic, bool isLeftGrip, const PointF& pos); ChordRest* findCR(Fraction tick, track_idx_t track) const; ChordRest* findChordRestEndingBeforeTickInStaff(const Fraction& tick, staff_idx_t staffIdx) const; diff --git a/src/notation/inotationinteraction.h b/src/notation/inotationinteraction.h index fd977ed70ace9..eaa552376854c 100644 --- a/src/notation/inotationinteraction.h +++ b/src/notation/inotationinteraction.h @@ -186,6 +186,7 @@ class INotationInteraction virtual void addLaissezVibToSelection() = 0; virtual void addSlurToSelection() = 0; virtual void addOttavaToSelection(OttavaType type) = 0; + virtual void addHairpinOnGripDrag(engraving::Dynamic* dynamic, bool isLeftGrip) = 0; virtual void addHairpinsToSelection(HairpinType type) = 0; virtual void putRestToSelection() = 0; virtual void putRest(Duration duration) = 0; diff --git a/src/notation/internal/notationinteraction.cpp b/src/notation/internal/notationinteraction.cpp index f55e228092031..a549f372a4038 100644 --- a/src/notation/internal/notationinteraction.cpp +++ b/src/notation/internal/notationinteraction.cpp @@ -52,6 +52,7 @@ #include "engraving/dom/bracket.h" #include "engraving/dom/chord.h" #include "engraving/dom/drumset.h" +#include "engraving/dom/dynamic.h" #include "engraving/dom/elementgroup.h" #include "engraving/dom/factory.h" #include "engraving/dom/figuredbass.h" @@ -1132,6 +1133,14 @@ void NotationInteraction::drag(const PointF& fromPos, const PointF& toPos, DragM m_dragData.ed.moveDelta = m_dragData.ed.delta - m_dragData.elementOffset; m_dragData.ed.addData(m_editData.getData(m_editData.element)); m_editData.element->editDrag(m_dragData.ed); + + if (m_editData.element->isDynamic()) { + // When the dynamic has no left grip, the right grip will have index zero, a.k.a. Grip::LEFT. + // TODO: refactor all code that works with Grips, so that this is not necessary + Dynamic* dynamic = toDynamic(m_editData.element); + bool isLeftGrip = dynamic->hasLeftGrip() ? m_editData.curGrip == Grip::LEFT : false; + addHairpinOnGripDrag(toDynamic(m_editData.element), isLeftGrip); + } } else { if (m_editData.element) { m_editData.element->editDrag(m_dragData.ed); @@ -1144,7 +1153,11 @@ void NotationInteraction::drag(const PointF& fromPos, const PointF& toPos, DragM score()->update(); if (isGripEditStarted()) { - updateGripAnchorLines(); + if (m_editData.element->isDynamic() && !m_editData.isStartEndGrip()) { + updateDragAnchorLines(); + } else { + updateGripAnchorLines(); + } } else { updateDragAnchorLines(); } @@ -1454,12 +1467,12 @@ bool NotationInteraction::drop(const PointF& pos, Qt::KeyboardModifiers modifier switch (et) { case ElementType::TEXTLINE: systemStavesOnly = m_dropData.ed.dropElement->systemFlag(); - // fall-thru + [[fallthrough]]; case ElementType::VOLTA: case ElementType::GRADUAL_TEMPO_CHANGE: // voltas drop to system staves by default, or closest staff if Control is held systemStavesOnly = systemStavesOnly || !(m_dropData.ed.modifiers & Qt::ControlModifier); - // fall-thru + [[fallthrough]]; case ElementType::OTTAVA: case ElementType::TRILL: case ElementType::PEDAL: @@ -1478,7 +1491,7 @@ bool NotationInteraction::drop(const PointF& pos, Qt::KeyboardModifiers modifier case ElementType::FSYMBOL: case ElementType::IMAGE: applyUserOffset = true; - // fall-thru + [[fallthrough]]; case ElementType::DYNAMIC: case ElementType::FRET_DIAGRAM: case ElementType::HARMONY: @@ -1559,17 +1572,21 @@ bool NotationInteraction::drop(const PointF& pos, Qt::KeyboardModifiers modifier resetDropElement(); break; } + + EngravingItem* elementToSelect = m_dropData.ed.dropElement; m_dropData.ed.dropElement = nullptr; m_dropData.ed.pos = PointF(); m_dropData.ed.modifiers = {}; - setDropTarget(nullptr); // this also resets dropRectangle and dropAnchor + + setDropTarget(nullptr); // this also resets dropRectangle and dropAnchor apply(); - // update input cursor position (must be done after layout) -// if (noteEntryMode()) { -// moveCursor(); -// } + if (accepted) { notifyAboutDropChanged(); + + if (elementToSelect) { + selectAndStartEditIfNeeded(elementToSelect); + } } MScoreErrorsController(iocContext()).checkAndShowMScoreError(); @@ -2846,6 +2863,11 @@ void NotationInteraction::drawGripPoints(muse::draw::Painter* painter) } mu::engraving::EngravingItem* editedElement = m_editData.element; + + if (editedElement && editedElement->isDynamic()) { + toDynamic(editedElement)->findAdjacentHairpins(); + } + int gripsCount = editedElement ? editedElement->gripsCount() : 0; if (gripsCount == 0) { @@ -3756,6 +3778,7 @@ void NotationInteraction::startEditGrip(EngravingItem* element, mu::engraving::G m_editData.element = element; m_editData.curGrip = grip; + m_editData.editTextualProperties = false; updateGripAnchorLines(); m_editData.element->startEdit(m_editData); @@ -3956,7 +3979,11 @@ void NotationInteraction::editElement(QKeyEvent* event) if (!isShiftRelease) { if (isGripEditStarted()) { - updateGripAnchorLines(); + if (m_editData.element->isDynamic() && !m_editData.isStartEndGrip()) { + updateDragAnchorLines(); + } else { + updateGripAnchorLines(); + } } else if (isElementEditStarted() && !m_editData.editTextualProperties) { updateDragAnchorLines(); } @@ -4495,6 +4522,38 @@ void NotationInteraction::addOttavaToSelection(OttavaType type) apply(); } +void NotationInteraction::addHairpinOnGripDrag(Dynamic* dynamic, bool isLeftGrip) +{ + startEdit(TranslatableString("undoableAction", "Add hairpin")); + + const PointF pos = m_dragData.ed.pos; + Hairpin* hairpin = score()->addHairpinToDynamicOnGripDrag(dynamic, isLeftGrip, pos); + + if (!hairpin) { + rollback(); + return; + } + + apply(); + + // Reset grip offset to zero after drawing the hairpin + dynamic->resetRightDragOffset(); + + IF_ASSERT_FAILED(!hairpin->segmentsEmpty()) { + return; + } + + if (isLeftGrip) { + LineSegment* segment = hairpin->frontSegment(); + select({ segment }); + startEditGrip(segment, Grip::START); + } else { + LineSegment* segment = hairpin->backSegment(); + select({ segment }); + startEditGrip(segment, Grip::END); + } +} + void NotationInteraction::addHairpinsToSelection(HairpinType type) { if (selection()->isNone()) { diff --git a/src/notation/internal/notationinteraction.h b/src/notation/internal/notationinteraction.h index 8cc990d350911..5435c0cf8fa8a 100644 --- a/src/notation/internal/notationinteraction.h +++ b/src/notation/internal/notationinteraction.h @@ -189,6 +189,7 @@ class NotationInteraction : public INotationInteraction, public muse::Injectable void addTiedNoteToChord() override; void addSlurToSelection() override; void addOttavaToSelection(OttavaType type) override; + void addHairpinOnGripDrag(engraving::Dynamic* dynamic, bool isLeftGrip) override; void addHairpinsToSelection(HairpinType type) override; void putRestToSelection() override; void putRest(Duration duration) override; diff --git a/src/notation/tests/mocks/notationinteractionmock.h b/src/notation/tests/mocks/notationinteractionmock.h index 6dfd8891b6587..81cc412969193 100644 --- a/src/notation/tests/mocks/notationinteractionmock.h +++ b/src/notation/tests/mocks/notationinteractionmock.h @@ -148,6 +148,7 @@ class NotationInteractionMock : public INotationInteraction MOCK_METHOD(void, addTiedNoteToChord, (), (override)); MOCK_METHOD(void, addSlurToSelection, (), (override)); MOCK_METHOD(void, addOttavaToSelection, (OttavaType), (override)); + MOCK_METHOD(void, addHairpinOnGripDrag, (engraving::Dynamic*, bool), (override)); MOCK_METHOD(void, addHairpinsToSelection, (HairpinType), (override)); MOCK_METHOD(void, putRestToSelection, (), (override)); MOCK_METHOD(void, putRest, (Duration), (override));