From 6a83e75aa27a999667e51714f6d980d46afa4ee5 Mon Sep 17 00:00:00 2001 From: Michele Spagnolo Date: Fri, 31 May 2024 18:52:31 +0200 Subject: [PATCH] Improvements around hairpins and dynamic --- src/engraving/dom/anchors.cpp | 7 + src/engraving/dom/dynamic.cpp | 30 ++- src/engraving/dom/dynamic.h | 7 +- src/engraving/dom/engravingitem.cpp | 81 ++++++ src/engraving/dom/engravingitem.h | 25 ++ src/engraving/dom/expression.cpp | 41 ++-- src/engraving/dom/expression.h | 11 +- src/engraving/dom/hairpin.cpp | 105 ++++++-- src/engraving/dom/hairpin.h | 16 +- src/engraving/dom/line.cpp | 28 +-- src/engraving/dom/property.cpp | 5 +- src/engraving/dom/property.h | 5 +- .../rendering/dev/alignmentlayout.cpp | 163 ++++++++++++ src/engraving/rendering/dev/alignmentlayout.h | 49 ++++ src/engraving/rendering/dev/rendering.cmake | 2 + src/engraving/rendering/dev/systemlayout.cpp | 97 ++++---- src/engraving/rendering/dev/systemlayout.h | 3 +- src/engraving/rendering/dev/tlayout.cpp | 232 ++++++------------ src/engraving/rendering/stable/tlayout.cpp | 11 +- src/engraving/rw/read410/tread.cpp | 8 +- src/engraving/rw/write/twrite.cpp | 13 +- .../expressions/expressionsettingsmodel.cpp | 8 +- .../expressions/expressionsettingsmodel.h | 4 +- .../lines/hairpinlinesettingsmodel.cpp | 29 +++ .../notation/lines/hairpinlinesettingsmodel.h | 11 + .../notation/lines/hairpinsettingsmodel.cpp | 25 ++ .../notation/lines/hairpinsettingsmodel.h | 8 + .../expressions/ExpressionsSettings.qml | 10 +- .../internal/CrescDimLineStyleSettings.qml | 14 ++ .../lines/internal/HairpinStyleSettings.qml | 15 ++ src/notation/internal/notationinteraction.cpp | 3 + 31 files changed, 741 insertions(+), 325 deletions(-) create mode 100644 src/engraving/rendering/dev/alignmentlayout.cpp create mode 100644 src/engraving/rendering/dev/alignmentlayout.h diff --git a/src/engraving/dom/anchors.cpp b/src/engraving/dom/anchors.cpp index 0a2d1089970a6..2a6ec3cd29571 100644 --- a/src/engraving/dom/anchors.cpp +++ b/src/engraving/dom/anchors.cpp @@ -27,6 +27,7 @@ #include "score.h" #include "spanner.h" #include "system.h" +#include "page.h" #include "rendering/dev/measurelayout.h" @@ -79,6 +80,7 @@ void EditTimeTickAnchors::updateAnchors(Measure* measure, staff_idx_t staffIdx) for (const Fraction& anchorTick : anchorTicks) { createTimeTickAnchor(measure, anchorTick, staffIdx); } + measure->computeTicks(); Score* score = measure->score(); @@ -100,6 +102,11 @@ TimeTickAnchor* EditTimeTickAnchors::createTimeTickAnchor(Measure* measure, Frac anchor->setParent(segment); anchor->setTrack(track); segment->add(anchor); + if (System* system = measure->system()) { + if (Page* page = system->page()) { + page->invalidateBspTree(); + } + } } return anchor; diff --git a/src/engraving/dom/dynamic.cpp b/src/engraving/dom/dynamic.cpp index 8ce58344945c0..8b37bbc11984d 100644 --- a/src/engraving/dom/dynamic.cpp +++ b/src/engraving/dom/dynamic.cpp @@ -427,6 +427,12 @@ String Dynamic::dynamicText(DynamicType t) return String::fromUtf8(DYN_LIST[int(t)].text); } +Expression* Dynamic::snappedExpression() const +{ + EngravingItem* item = ldata()->itemSnappedAfter(); + return item && item->isExpression() ? toExpression(item) : nullptr; +} + bool Dynamic::acceptDrop(EditData& ed) const { ElementType droppedType = ed.dropElement->type(); @@ -624,11 +630,16 @@ void Dynamic::editDrag(EditData& ed) if (km != (ShiftModifier | ControlModifier)) { staff_idx_t si = staffIdx(); Segment* seg = segment(); - score()->dragPosition(canvasPos(), &si, &seg, allowTimeAnchor()); + static constexpr double spacingFactor = 1.0; + score()->dragPosition(canvasPos(), &si, &seg, spacingFactor, allowTimeAnchor()); if (seg != segment() || staffIdx() != si) { const PointF oldOffset = offset(); PointF pos1(canvasPos()); - score()->undoChangeParent(this, seg, si); + score()->undoChangeParent(this, seg, staffIdx()); + Expression* snappedExpr = snappedExpression(); + if (snappedExpr) { + score()->undoChangeParent(snappedExpr, seg, staffIdx()); + } setOffset(PointF()); renderer()->layoutItem(this); @@ -666,6 +677,10 @@ void Dynamic::reset() undoResetProperty(Pid::DIRECTION); undoResetProperty(Pid::CENTER_BETWEEN_STAVES); TextBase::reset(); + Expression* snappedExp = snappedExpression(); + if (snappedExp && snappedExp->getProperty(Pid::OFFSET) != snappedExp->propertyDefault(Pid::OFFSET)) { + snappedExp->reset(); + } } //--------------------------------------------------------- @@ -815,17 +830,6 @@ PropertyValue Dynamic::propertyDefault(Pid id) const } } -void Dynamic::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) -{ - TextBase::undoChangeProperty(id, v, ps); - if (m_snappedExpression) { - if ((id == Pid::OFFSET && m_snappedExpression->offset() != v.value()) - || (id == Pid::PLACEMENT && m_snappedExpression->placement() != v.value())) { - m_snappedExpression->undoChangeProperty(id, v, ps); - } - } -} - //--------------------------------------------------------- // accessibleInfo //--------------------------------------------------------- diff --git a/src/engraving/dom/dynamic.h b/src/engraving/dom/dynamic.h index 7f98a028649cc..7377cd1241e78 100644 --- a/src/engraving/dom/dynamic.h +++ b/src/engraving/dom/dynamic.h @@ -100,20 +100,18 @@ class Dynamic final : public TextBase PropertyValue getProperty(Pid propertyId) const override; bool setProperty(Pid propertyId, const PropertyValue&) override; PropertyValue propertyDefault(Pid id) const override; - void undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) override; std::unique_ptr getDragGroup(std::function isDragged) override; String accessibleInfo() const override; String screenReaderInfo() const override; -// void doAutoplace(); + void manageBarlineCollisions(); static String dynamicText(DynamicType t); bool hasCustomText() const { return dynamicText(m_dynamicType) != xmlText(); } - void setSnappedExpression(Expression* e) { m_snappedExpression = e; } - Expression* snappedExpression() const { return m_snappedExpression; } + Expression* snappedExpression() const; bool playDynamic() const { return m_playDynamic; } void setPlayDynamic(bool v) { m_playDynamic = v; } @@ -140,7 +138,6 @@ class Dynamic final : public TextBase bool nudge(const EditData& ed); DynamicType m_dynamicType = DynamicType::OTHER; - Expression* m_snappedExpression = nullptr; bool m_playDynamic = true; mutable PointF m_dragOffset; diff --git a/src/engraving/dom/engravingitem.cpp b/src/engraving/dom/engravingitem.cpp index f69f8fc5447a1..5c95866fef78c 100644 --- a/src/engraving/dom/engravingitem.cpp +++ b/src/engraving/dom/engravingitem.cpp @@ -1349,6 +1349,51 @@ void EngravingItem::setPlacementBasedOnVoiceApplication(DirectionV styledDirecti } } +bool EngravingItem::shouldBeCenteredBetweenStaves(const System* system) const +{ + if (!isStyled(Pid::OFFSET)) { + // NOTE: because of current limitations of the offset system, we can't center an element that's been manually moved. + return false; + } + + const Part* itemPart = part(); + bool centerStyle = style().styleB(Sid::dynamicsHairpinsAutoCenterOnGrandStaff); + AutoOnOff centerProperty = getProperty(Pid::CENTER_BETWEEN_STAVES).value(); + if (itemPart->nstaves() <= 1 || centerProperty == AutoOnOff::OFF || (!centerStyle && centerProperty != AutoOnOff::ON)) { + return false; + } + + if (centerProperty != AutoOnOff::ON && !itemPart->instrument()->isNormallyMultiStaveInstrument()) { + return false; + } + + const Staff* thisStaff = staff(); + const std::vector& partStaves = itemPart->staves(); + IF_ASSERT_FAILED(partStaves.size() > 0) { + return false; + } + + if ((thisStaff == partStaves.front() && placeAbove()) || (thisStaff == partStaves.back() && placeBelow())) { + return false; + } + + staff_idx_t thisIdx = thisStaff->idx(); + if (placeAbove()) { + IF_ASSERT_FAILED(thisIdx > 0) { + return false; + } + } + staff_idx_t nextIdx = placeAbove() ? thisIdx - 1 : thisIdx + 1; + + const SysStaff* thisSystemStaff = system->staff(thisIdx); + const SysStaff* nextSystemStaff = system->staff(nextIdx); + if (!thisSystemStaff->show() || !nextSystemStaff->show()) { + return false; + } + + return centerProperty == AutoOnOff::ON || appliesToAllVoicesInInstrument(); +} + void EngravingItem::relinkPropertiesToMaster(PropertyGroup propGroup) { assert(!score()->isMaster()); @@ -2557,6 +2602,42 @@ void EngravingItem::LayoutData::setBbox(const RectF& r) m_shape.set_value(Shape(r, m_item, Shape::Type::Fixed)); } +void EngravingItem::LayoutData::connectItemSnappedBefore(EngravingItem* itemBefore) +{ + IF_ASSERT_FAILED(itemBefore) { + return; + } + m_itemSnappedBefore = itemBefore; + itemBefore->mutldata()->m_itemSnappedAfter = const_cast(m_item); +} + +void EngravingItem::LayoutData::disconnectItemSnappedBefore() +{ + if (!m_itemSnappedBefore) { + return; + } + m_itemSnappedBefore->mutldata()->m_itemSnappedAfter = nullptr; + m_itemSnappedBefore = nullptr; +} + +void EngravingItem::LayoutData::connectItemSnappedAfter(EngravingItem* itemAfter) +{ + IF_ASSERT_FAILED(itemAfter) { + return; + } + m_itemSnappedAfter = itemAfter; + itemAfter->mutldata()->m_itemSnappedBefore = const_cast(m_item); +} + +void EngravingItem::LayoutData::disconnectItemSnappedAfter() +{ + if (!m_itemSnappedAfter) { + return; + } + m_itemSnappedAfter->mutldata()->m_itemSnappedBefore = nullptr; + m_itemSnappedAfter = nullptr; +} + const RectF& EngravingItem::LayoutData::bbox(LD_ACCESS mode) const { //! NOTE Temporary disabled CHECK - a lot of messages diff --git a/src/engraving/dom/engravingitem.h b/src/engraving/dom/engravingitem.h index 9321f1ec115e4..515787e8686d7 100644 --- a/src/engraving/dom/engravingitem.h +++ b/src/engraving/dom/engravingitem.h @@ -575,6 +575,24 @@ class EngravingItem : public EngravingObject OffsetChange offsetChanged() const { return autoplace.offsetChanged; } + void connectItemSnappedBefore(EngravingItem* itemBefore); + void disconnectItemSnappedBefore(); + void connectItemSnappedAfter(EngravingItem* itemAfter); + void disconnectItemSnappedAfter(); + EngravingItem* itemSnappedBefore() const { return m_itemSnappedBefore; } + EngravingItem* itemSnappedAfter() const { return m_itemSnappedAfter; } + + struct StaffCenteringInfo { + double availableVertSpaceAbove = 0.0; + double availableVertSpaceBelow = 0.0; + }; + const StaffCenteringInfo& staffCenteringInfo() const { return m_staffCenteringInfo; } + void setStaffCenteringInfo(double availSpaceAbove, double availSpaceBelow) + { + m_staffCenteringInfo.availableVertSpaceAbove = availSpaceAbove; + m_staffCenteringInfo.availableVertSpaceBelow = availSpaceBelow; + } + void dump(std::stringstream& ss) const; protected: @@ -604,6 +622,11 @@ class EngravingItem : public EngravingObject double m_mag = 1.0; // standard magnification (derived value) ld_field m_pos = "pos"; // Reference position, relative to _parent, set by autoplace ld_field m_shape = "shape"; + + EngravingItem* m_itemSnappedBefore = nullptr; + EngravingItem* m_itemSnappedAfter = nullptr; + + StaffCenteringInfo m_staffCenteringInfo; }; const LayoutData* ldata() const; @@ -650,6 +673,8 @@ class EngravingItem : public EngravingObject void checkVoiceApplicationCompatibleWithTrack(); void setPlacementBasedOnVoiceApplication(DirectionV styledDirection); + bool shouldBeCenteredBetweenStaves(const System* system) const; + void setOffsetChanged(bool val, bool absolute = true, const PointF& diff = PointF()); //! --------------------- diff --git a/src/engraving/dom/expression.cpp b/src/engraving/dom/expression.cpp index 0916f6f997e6e..29546db912664 100644 --- a/src/engraving/dom/expression.cpp +++ b/src/engraving/dom/expression.cpp @@ -58,17 +58,17 @@ PropertyValue Expression::propertyDefault(Pid id) const } } -double Expression::computeDynamicExpressionDistance() const +double Expression::computeDynamicExpressionDistance(const Dynamic* snappedDyn) const { - if (!m_snappedDynamic) { + IF_ASSERT_FAILED(snappedDyn) { return 0.0; } // We are essentially faking the kerning behaviour of dynamic VS expression text // There's no other way to do this because the dynamic is a different font. - String dynamicTextString = m_snappedDynamic->xmlText(); + String dynamicTextString = snappedDyn->xmlText(); String f = String::fromStdString("dynamicForte"); double distance = (dynamicTextString.endsWith(f) ? 0.2 : 0.5) * spatium(); - distance *= 0.5 * (m_snappedDynamic->dynamicsSize() + (size() / 10)); + distance *= 0.5 * (snappedDyn->dynamicsSize() + (size() / 10)); return distance; } @@ -80,17 +80,6 @@ std::unique_ptr Expression::getDragGroup(std::functionoffset() != v.value()) - || (id == Pid::PLACEMENT && m_snappedDynamic->placement() != v.value())) { - m_snappedDynamic->undoChangeProperty(id, v, ps); - } - } -} - bool Expression::acceptDrop(EditData& ed) const { return ed.dropElement->type() == ElementType::DYNAMIC || TextBase::acceptDrop(ed); @@ -100,8 +89,9 @@ EngravingItem* Expression::drop(EditData& ed) { EngravingItem* item = ed.dropElement; if (item->isDynamic()) { - if (m_snappedDynamic) { - return m_snappedDynamic->drop(ed); + Dynamic* snappedDyn = snappedDynamic(); + if (snappedDyn) { + return snappedDyn->drop(ed); } item->setTrack(track()); @@ -167,4 +157,21 @@ void Expression::mapPropertiesFromOldExpressions(StaffText* staffText) setFrameRound(staffText->frameRound()); } } + +Dynamic* Expression::snappedDynamic() const +{ + EngravingItem* item = ldata()->itemSnappedBefore(); + return item && item->isDynamic() ? toDynamic(item) : nullptr; +} + +void Expression::reset() +{ + undoResetProperty(Pid::DIRECTION); + undoResetProperty(Pid::CENTER_BETWEEN_STAVES); + TextBase::reset(); + Dynamic* snappedDyn = snappedDynamic(); + if (snappedDyn && snappedDyn->getProperty(Pid::OFFSET) != snappedDyn->propertyDefault(Pid::OFFSET)) { + snappedDyn->reset(); + } +} } // namespace mu::engraving diff --git a/src/engraving/dom/expression.h b/src/engraving/dom/expression.h index 0696a01135375..f0e4182f46302 100644 --- a/src/engraving/dom/expression.h +++ b/src/engraving/dom/expression.h @@ -41,12 +41,10 @@ class Expression final : public TextBase PropertyValue propertyDefault(Pid id) const override; - double computeDynamicExpressionDistance() const; + double computeDynamicExpressionDistance(const Dynamic* snappedDyn) const; std::unique_ptr getDragGroup(std::function isDragged) override; - void undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) override; - bool acceptDrop(EditData& ed) const override; EngravingItem* drop(EditData& ed) override; @@ -54,12 +52,11 @@ class Expression final : public TextBase bool setProperty(Pid propertyId, const PropertyValue& v) override; void mapPropertiesFromOldExpressions(StaffText* staffText); - Dynamic* snappedDynamic() const { return m_snappedDynamic; } - void setSnappedDynamic(Dynamic* d) { m_snappedDynamic = d; } + Dynamic* snappedDynamic() const; -private: + bool hasVoiceApplicationProperties() const override { return true; } - Dynamic* m_snappedDynamic = nullptr; + void reset() override; }; } // namespace mu::engraving #endif // MU_ENGRAVING_EXPRESSION_H diff --git a/src/engraving/dom/hairpin.cpp b/src/engraving/dom/hairpin.cpp index f32f9743f21c8..ac9d770e0c0f4 100644 --- a/src/engraving/dom/hairpin.cpp +++ b/src/engraving/dom/hairpin.cpp @@ -279,7 +279,38 @@ Sid HairpinSegment::getPropertyStyle(Pid pid) const return TextLineBaseSegment::getPropertyStyle(pid); } -Dynamic* HairpinSegment::findStartDynamic() const +EngravingItem* HairpinSegment::findElementToSnapBefore() const +{ + TextBase* startDynOrExpr = findStartDynamicOrExpression(); + if (startDynOrExpr) { + return startDynOrExpr; + } + + Hairpin* hairp = hairpin(); + Fraction startTick = hairpin()->tick(); + auto intervals = score()->spannerMap().findOverlapping(startTick.ticks(), startTick.ticks()); + for (auto interval : intervals) { + Spanner* spanner = interval.value; + if (!spanner->isHairpin() || spanner->track() != hairp->track() || spanner->tick2() != startTick || !spanner->visible()) { + continue; + } + Hairpin* precedingHairpin = toHairpin(spanner); + bool canSnap = precedingHairpin->placeAbove() == hairp->placeAbove() + && toHairpin(spanner)->applyToVoice() == hairp->applyToVoice(); + if (canSnap && !precedingHairpin->segmentsEmpty()) { + return precedingHairpin->backSegment(); + } + } + + return nullptr; +} + +EngravingItem* HairpinSegment::findElementToSnapAfter() const +{ + return findEndDynamicOrExpression(); +} + +TextBase* HairpinSegment::findStartDynamicOrExpression() const { Fraction refTick = hairpin()->tick(); Measure* measure = score()->tick2measure(refTick); @@ -287,8 +318,8 @@ Dynamic* HairpinSegment::findStartDynamic() const return nullptr; } - std::vector dynamics; - dynamics.reserve(2); + std::vector dynamicsAndExpr; + dynamicsAndExpr.reserve(2); for (Segment* segment = measure->last(); segment; segment = segment->prev1()) { if (segment->system() != system()) { @@ -302,28 +333,35 @@ Dynamic* HairpinSegment::findStartDynamic() const break; } for (EngravingItem* item : segment->annotations()) { - if (item->isDynamic() && item->track() == track()) { - dynamics.push_back(toDynamic(item)); + if ((item->isDynamic() || item->isExpression()) + && item->visible() + && item->track() == hairpin()->track() + && item->placement() == placement() + && item->getProperty(Pid::APPLY_TO_VOICE) == hairpin()->getProperty(Pid::APPLY_TO_VOICE)) { + dynamicsAndExpr.push_back(toTextBase(item)); } } - if (dynamics.size() > 0) { + if (dynamicsAndExpr.size() > 0) { break; } } - if (dynamics.size() == 0) { + if (dynamicsAndExpr.size() == 0) { return nullptr; } - if (dynamics.size() > 1) { - std::sort(dynamics.begin(), dynamics.end(), - [](Dynamic* dyn1, Dynamic* dyn2) { return dyn1->anchorToEndOfPrevious() && !dyn2->anchorToEndOfPrevious(); }); + if (dynamicsAndExpr.size() > 1) { + std::sort(dynamicsAndExpr.begin(), dynamicsAndExpr.end(), [](TextBase* item1, TextBase* item2) { + return (item1->isDynamic() && item2->isExpression()) + || (item1->isDynamic() && toDynamic(item1)->anchorToEndOfPrevious() + && item2->isDynamic() && !toDynamic(item2)->anchorToEndOfPrevious()); + }); } - return dynamics.back(); + return dynamicsAndExpr.back(); } -Dynamic* HairpinSegment::findEndDynamic() const +TextBase* HairpinSegment::findEndDynamicOrExpression() const { Fraction refTick = hairpin()->tick2(); Measure* measure = score()->tick2measure(refTick - Fraction::eps()); @@ -331,8 +369,8 @@ Dynamic* HairpinSegment::findEndDynamic() const return nullptr; } - std::vector dynamics; - dynamics.reserve(2); + std::vector dynamicsAndExpr; + dynamicsAndExpr.reserve(2); for (Segment* segment = measure->first(); segment; segment = segment->next1()) { if (segment->system() != system()) { @@ -346,25 +384,32 @@ Dynamic* HairpinSegment::findEndDynamic() const break; } for (EngravingItem* item : segment->annotations()) { - if (item->isDynamic() && item->track() == track()) { - dynamics.push_back(toDynamic(item)); + if ((item->isDynamic() || item->isExpression()) + && item->visible() + && item->track() == hairpin()->track() + && item->placement() == placement() + && item->getProperty(Pid::APPLY_TO_VOICE) == hairpin()->getProperty(Pid::APPLY_TO_VOICE)) { + dynamicsAndExpr.push_back(toTextBase(item)); } } - if (dynamics.size() > 0) { + if (dynamicsAndExpr.size() > 0) { break; } } - if (dynamics.size() == 0) { + if (dynamicsAndExpr.size() == 0) { return nullptr; } - if (dynamics.size() > 1) { - std::sort(dynamics.begin(), dynamics.end(), - [](Dynamic* dyn1, Dynamic* dyn2) { return dyn1->anchorToEndOfPrevious() && !dyn2->anchorToEndOfPrevious(); }); + if (dynamicsAndExpr.size() > 1) { + std::sort(dynamicsAndExpr.begin(), dynamicsAndExpr.end(), [](TextBase* item1, TextBase* item2) { + return (item1->isDynamic() && item2->isExpression()) + || (item1->isDynamic() && toDynamic(item1)->anchorToEndOfPrevious() + && item2->isDynamic() && !toDynamic(item2)->anchorToEndOfPrevious()); + }); } - return dynamics.front(); + return dynamicsAndExpr.front(); } Sid Hairpin::getPropertyStyle(Pid pid) const @@ -507,6 +552,11 @@ PropertyValue Hairpin::getProperty(Pid id) const return centerBetweenStaves(); case Pid::DIRECTION: return direction(); + case Pid::SNAP_BEFORE: + return snapToItemBefore(); + case Pid::SNAP_AFTER: + return snapToItemAfter(); + default: return TextLineBase::getProperty(id); } @@ -555,6 +605,12 @@ bool Hairpin::setProperty(Pid id, const PropertyValue& v) case Pid::DIRECTION: setDirection(v.value()); break; + case Pid::SNAP_BEFORE: + setSnapToItemBefore(v.toBool()); + break; + case Pid::SNAP_AFTER: + setSnapToItemAfter(v.toBool()); + break; default: return TextLineBase::setProperty(id, v); } @@ -643,6 +699,11 @@ PropertyValue Hairpin::propertyDefault(Pid id) const case Pid::DIRECTION: return DirectionV::AUTO; + case Pid::SNAP_BEFORE: + return true; + case Pid::SNAP_AFTER: + return true; + default: return TextLineBase::propertyDefault(id); } diff --git a/src/engraving/dom/hairpin.h b/src/engraving/dom/hairpin.h index 22932746981dc..b8ae5905bbbf8 100644 --- a/src/engraving/dom/hairpin.h +++ b/src/engraving/dom/hairpin.h @@ -70,12 +70,14 @@ class HairpinSegment final : public TextLineBaseSegment std::unique_ptr getDragGroup(std::function isDragged) override; - Dynamic* findStartDynamic() const; - Dynamic* findEndDynamic() const; - bool hasVoiceApplicationProperties() const override { return spanner()->hasVoiceApplicationProperties(); } + EngravingItem* findElementToSnapBefore() const; + EngravingItem* findElementToSnapAfter() const; + private: + TextBase* findStartDynamicOrExpression() const; + TextBase* findEndDynamicOrExpression() const; void startEditDrag(EditData&) override; void editDrag(EditData&) override; @@ -175,6 +177,11 @@ class Hairpin final : public TextLineBase void setCenterBetweenStaves(AutoOnOff v) { m_centerBetweenStaves = v; } AutoOnOff centerBetweenStaves() const { return m_centerBetweenStaves; } + bool snapToItemBefore() const { return m_snapToItemBefore; } + void setSnapToItemBefore(bool v) { m_snapToItemBefore = v; } + bool snapToItemAfter() const { return m_snapToItemAfter; } + void setSnapToItemAfter(bool v) { m_snapToItemAfter = v; } + private: Sid getPropertyStyle(Pid) const override; @@ -193,6 +200,9 @@ class Hairpin final : public TextLineBase VoiceApplication m_applyToVoice = VoiceApplication::ALL_VOICE_IN_INSTRUMENT; DirectionV m_direction = DirectionV::AUTO; AutoOnOff m_centerBetweenStaves = AutoOnOff::AUTO; + + bool m_snapToItemBefore = true; + bool m_snapToItemAfter = true; }; } // namespace mu::engraving diff --git a/src/engraving/dom/line.cpp b/src/engraving/dom/line.cpp index d27a92d0aee56..c19bae2f347f8 100644 --- a/src/engraving/dom/line.cpp +++ b/src/engraving/dom/line.cpp @@ -450,7 +450,7 @@ Segment* LineSegment::findSegmentForGrip(Grip grip, PointF pos) const const staff_idx_t oldStaffIndex = left ? staffIdx() : track2staff(l->effectiveTrack2()); - const double spacingFactor = left ? 0.5 : 1.0; // defines the point where canvas is divided between segments, systems etc. + const double spacingFactor = 0.5; // defines the point where canvas is divided between segments, systems etc. System* sys = system(); const std::vector foundSystems = score()->searchSystem(pos, sys, spacingFactor); @@ -661,30 +661,8 @@ void LineSegment::rebaseAnchors(EditData& ed, Grip grip) } break; case Grip::MIDDLE: { - if (!isSingleType()) { - return; - } - - SLine* l = line(); - - // If dragging middle grip (or the entire hairpin), mouse position - // does not directly correspond to any sensible position, so use - // actual line coordinates instead. This method doesn't allow for - // system changes, but that seems OK when dragging the entire line: - // the line will just push away other systems according to autoplacement - // rules if necessary. - PointF cpos = canvasPos(); - cpos.setY(system()->staffCanvasYpage(l->staffIdx())); // prevent cross-system move - - Segment* seg1 = findSegmentForGrip(Grip::START, cpos); - Segment* seg2 = findSegmentForGrip(Grip::END, cpos + pos2()); - - if (!(seg1 && seg2 && seg1->system() == seg2->system() && seg1->system() == system())) { - return; - } - - rebaseAnchor(Grip::START, seg1); - rebaseAnchor(Grip::END, seg2); + // The middle grip is used for vertical movement 99% of the time, so don't try to rebase anchors. + return; } default: break; diff --git a/src/engraving/dom/property.cpp b/src/engraving/dom/property.cpp index 86c878972b5e6..dffa61f382648 100644 --- a/src/engraving/dom/property.cpp +++ b/src/engraving/dom/property.cpp @@ -352,9 +352,12 @@ static constexpr PropertyMetaData propertyList[] = { { Pid::AVOID_BARLINES, false, "avoidBarLines", P_TYPE::BOOL, PropertyGroup::APPEARANCE, DUMMY_QT_TR_NOOP("propertyName", "avoid barlines") }, { Pid::DYNAMICS_SIZE, false, "dynamicsSize", P_TYPE::REAL, PropertyGroup::APPEARANCE, DUMMY_QT_TR_NOOP("propertyName", "dynamic size") }, { Pid::CENTER_ON_NOTEHEAD, false, "centerOnNotehead", P_TYPE::BOOL, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "use text alignment") }, - { Pid::SNAP_TO_DYNAMICS, false, "snapToDynamics", P_TYPE::BOOL, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "snap expression") }, { Pid::ANCHOR_TO_END_OF_PREVIOUS, true, "anchorToEndOfPrevious", P_TYPE::BOOL, PropertyGroup::NONE, DUMMY_QT_TR_NOOP("propertyName", "anchor to end of previous") }, + { Pid::SNAP_TO_DYNAMICS, false, "snapToDynamics", P_TYPE::BOOL, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "snap expression") }, // for expressions + { Pid::SNAP_BEFORE, false, "snapBefore", P_TYPE::BOOL, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "snap before") }, // < + { Pid::SNAP_AFTER, false, "snapAfter", P_TYPE::BOOL, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "snap after") }, // < for hairpins + { Pid::APPLY_TO_VOICE, true, "applyToVoice", P_TYPE::VOICE_APPLICATION, PropertyGroup::NONE, DUMMY_QT_TR_NOOP("propertyName", "apply to voice") }, { Pid::CENTER_BETWEEN_STAVES, false, "centerBetweenStaves", P_TYPE::AUTO_ON_OFF, PropertyGroup::POSITION, DUMMY_QT_TR_NOOP("propertyName", "center between staves") }, diff --git a/src/engraving/dom/property.h b/src/engraving/dom/property.h index 752b1010bcca8..310d6a4c8c9da 100644 --- a/src/engraving/dom/property.h +++ b/src/engraving/dom/property.h @@ -362,9 +362,12 @@ enum class Pid { AVOID_BARLINES, // meant for Dynamics DYNAMICS_SIZE, CENTER_ON_NOTEHEAD, - SNAP_TO_DYNAMICS, ANCHOR_TO_END_OF_PREVIOUS, + SNAP_TO_DYNAMICS, // pre-4.4 version of the property, specific for expression + SNAP_BEFORE, + SNAP_AFTER, + APPLY_TO_VOICE, CENTER_BETWEEN_STAVES, diff --git a/src/engraving/rendering/dev/alignmentlayout.cpp b/src/engraving/rendering/dev/alignmentlayout.cpp new file mode 100644 index 0000000000000..eb6cb4a7a150a --- /dev/null +++ b/src/engraving/rendering/dev/alignmentlayout.cpp @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2023 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + */ + +#include "alignmentlayout.h" +#include "systemlayout.h" + +#include "dom/dynamic.h" +#include "dom/expression.h" +#include "dom/hairpin.h" +#include "dom/system.h" +#include "dom/text.h" + +namespace mu::engraving::rendering::dev { +void AlignmentLayout::alignItems(const std::vector& elements, const System* system) +{ + std::set alignedItems; + + double outermostY = 0.0; + bool firstItem = true; + auto computeOutermostY = [&outermostY, &firstItem, &alignedItems](EngravingItem* item) { + double curY = yOpticalCenter(item); + if (firstItem) { + outermostY = curY; + firstItem = false; + } else { + outermostY = item->placeAbove() ? std::min(outermostY, curY) : std::max(outermostY, curY); + } + }; + + auto moveElementsToOutermostY = [&outermostY, &alignedItems, system](EngravingItem* item) { + moveItemToY(item, outermostY, system, alignedItems); + }; + + for (EngravingItem* item : elements) { + if (muse::contains(alignedItems, item)) { + continue; + } + firstItem = true; + scanConnectedItems(item, computeOutermostY); + scanConnectedItems(item, moveElementsToOutermostY); + } +} + +void AlignmentLayout::alignStaffCenteredItems(const std::vector& elements, const System* system) +{ + std::vector vecOfCurrentY; + + auto collectCurrentYandComputeEdges = [&vecOfCurrentY](EngravingItem* item) { + vecOfCurrentY.push_back(yOpticalCenter(item)); + }; + + double averageY = 0.0; + auto limitAverageYInsideAvailableSpace = [&averageY](EngravingItem* item) { + double yCur = yOpticalCenter(item); + double intendedMove = averageY - yCur; + const EngravingItem::LayoutData::StaffCenteringInfo& staffCenteringInfo = item->ldata()->staffCenteringInfo(); + double availSpaceAbove = staffCenteringInfo.availableVertSpaceAbove; + double availSpaceBelow = staffCenteringInfo.availableVertSpaceBelow; + double maxAllowedMove = std::clamp(intendedMove, availSpaceAbove, availSpaceBelow); + if (!muse::RealIsEqual(maxAllowedMove, intendedMove)) { + averageY += -intendedMove + maxAllowedMove; + } + }; + + std::set alignedItems; + auto moveElementsToAverageY = [&averageY, &alignedItems, system](EngravingItem* item) { + moveItemToY(item, averageY, system, alignedItems); + }; + + for (EngravingItem* item : elements) { + if (muse::contains(alignedItems, item)) { + continue; + } + vecOfCurrentY.clear(); + scanConnectedItems(item, collectCurrentYandComputeEdges); + averageY = computeAverageY(vecOfCurrentY); + scanConnectedItems(item, limitAverageYInsideAvailableSpace); + scanConnectedItems(item, moveElementsToAverageY); + } +} + +void AlignmentLayout::moveItemToY(EngravingItem* item, double y, const System* system, std::set& alignedItems) +{ + double curY = yOpticalCenter(item); + double yDiff = y - curY; + item->mutldata()->moveY(yDiff); + alignedItems.insert(item); + SystemLayout::updateSkylineForElement(item, system, yDiff); +} + +double AlignmentLayout::yOpticalCenter(const EngravingItem* item) +{ + double curY = item->pos().y(); + switch (item->type()) { + case ElementType::DYNAMIC: + curY -= 0.46 * item->spatium() * toDynamic(item)->dynamicsSize(); // approximated half x-height of dynamic + break; + case ElementType::EXPRESSION: + curY -= 0.5 * toExpression(item)->fontMetrics().xHeight(); + break; + case ElementType::HAIRPIN_SEGMENT: + { + const HairpinSegment* hairpinSeg = toHairpinSegment(item); + if (hairpinSeg->hairpin()->isLineType()) { + Text* text = hairpinSeg->text(); + if (!text) { + text = hairpinSeg->endText(); + } + if (text) { + curY -= 0.5 * text->fontMetrics().xHeight(); + } else { + curY -= 0.5 * item->spatium(); + } + } + } + default: + break; + } + return curY; +} + +void AlignmentLayout::scanConnectedItems(EngravingItem* item, std::function func) +{ + func(item); + + EngravingItem* snappedBefore = item->ldata()->itemSnappedBefore(); + while (snappedBefore) { + func(snappedBefore); + snappedBefore = snappedBefore->ldata()->itemSnappedBefore(); + } + + EngravingItem* snappedAfter = item->ldata()->itemSnappedAfter(); + while (snappedAfter) { + func(snappedAfter); + snappedAfter = snappedAfter->ldata()->itemSnappedAfter(); + } +} + +double AlignmentLayout::computeAverageY(const std::vector vecOfY) +{ + double sum = std::accumulate(vecOfY.begin(), vecOfY.end(), 0.0); + return sum / static_cast(vecOfY.size()); +} +} // namespace mu::engraving::rendering::dev diff --git a/src/engraving/rendering/dev/alignmentlayout.h b/src/engraving/rendering/dev/alignmentlayout.h new file mode 100644 index 0000000000000..636d7c94d9b47 --- /dev/null +++ b/src/engraving/rendering/dev/alignmentlayout.h @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2023 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 MU_ENGRAVING_ALIGNMENTLAYOUT_DEV_H +#define MU_ENGRAVING_ALIGNMENTLAYOUT_DEV_H + +#include +#include +#include + +namespace mu::engraving { +class EngravingItem; +class System; +} + +namespace mu::engraving::rendering::dev { +class AlignmentLayout +{ +public: + static void alignItems(const std::vector& elements, const System* system); + static void alignStaffCenteredItems(const std::vector& elements, const System* system); + +private: + static void moveItemToY(EngravingItem* item, double y, const System* system, std::set& alignedItems); + static double yOpticalCenter(const EngravingItem* item); + static void scanConnectedItems(EngravingItem* item, std::function func); + static double computeAverageY(const std::vector vecOfY); +}; +} // namespace mu::engraving::rendering::dev +#endif // MU_ENGRAVING_ALIGNMENTLAYOUT_DEV_H diff --git a/src/engraving/rendering/dev/rendering.cmake b/src/engraving/rendering/dev/rendering.cmake index a74d9d88ad1f6..80803f85e40d1 100644 --- a/src/engraving/rendering/dev/rendering.cmake +++ b/src/engraving/rendering/dev/rendering.cmake @@ -61,6 +61,8 @@ set(RENDERING_DEV_SRC ${CMAKE_CURRENT_LIST_DIR}/segmentlayout.h ${CMAKE_CURRENT_LIST_DIR}/modifydom.cpp ${CMAKE_CURRENT_LIST_DIR}/modifydom.h + ${CMAKE_CURRENT_LIST_DIR}/alignmentlayout.cpp + ${CMAKE_CURRENT_LIST_DIR}/alignmentlayout.h ${CMAKE_CURRENT_LIST_DIR}/passbase.cpp ${CMAKE_CURRENT_LIST_DIR}/passbase.h diff --git a/src/engraving/rendering/dev/systemlayout.cpp b/src/engraving/rendering/dev/systemlayout.cpp index e24c64a4ab0e4..7cf29f8d599e7 100644 --- a/src/engraving/rendering/dev/systemlayout.cpp +++ b/src/engraving/rendering/dev/systemlayout.cpp @@ -62,6 +62,7 @@ #include "dom/volta.h" #include "tlayout.h" +#include "alignmentlayout.h" #include "autoplace.h" #include "beamlayout.h" #include "chordlayout.h" @@ -1032,6 +1033,8 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) } } + std::vector possibleElementsToAlign; + //------------------------------------------------------------- // Dynamics and figured bass //------------------------------------------------------------- @@ -1044,6 +1047,7 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) if (e->autoplace()) { if (e->isDynamic()) { toDynamic(e)->manageBarlineCollisions(); + possibleElementsToAlign.push_back(e); } Autoplace::autoplaceSegmentElement(e, e->mutldata(), false); dynamicsAndFigBass.push_back(e); @@ -1078,6 +1082,7 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) if (e->isExpression()) { TLayout::layoutItem(e, ctx); if (e->addToSkyline()) { + possibleElementsToAlign.push_back(e); system->staff(e->staffIdx())->skyline().add(e->shape().translate(e->pos() + s->pos() + m->pos())); } } @@ -1127,6 +1132,15 @@ void SystemLayout::layoutSystemElements(System* system, LayoutContext& ctx) } } processLines(system, ctx, hairpins, false); + + for (SpannerSegment* spannerSegment : system->spannerSegments()) { + if (spannerSegment->isHairpinSegment()) { + possibleElementsToAlign.push_back(spannerSegment); + } + } + + AlignmentLayout::alignItems(possibleElementsToAlign, system); + processLines(system, ctx, spanner, false); processLines(system, ctx, ottavas, false); processLines(system, ctx, pedal, true); @@ -2714,11 +2728,25 @@ double SystemLayout::minDistance(const System* top, const System* bottom, const return dist; } +void SystemLayout::updateSkylineForElement(EngravingItem* element, const System* system, double yMove) +{ + Skyline& skyline = system->staff(element->staffIdx())->skyline(); + SkylineLine& skylineLine = element->placeAbove() ? skyline.north() : skyline.south(); + for (ShapeElement& shapeEl : skylineLine.elements()) { + if (shapeEl.item() == element) { + shapeEl.translate(0.0, yMove); + } + } +} + void SystemLayout::centerElementsBetweenStaves(const System* system) { + std::vector centeredItems; + for (SpannerSegment* spannerSeg : system->spannerSegments()) { - if (spannerSeg->isHairpinSegment() && elementNeedsCenterBetweenStaves(spannerSeg, system)) { + if (spannerSeg->isHairpinSegment() && spannerSeg->shouldBeCenteredBetweenStaves(system)) { centerElementBetweenStaves(spannerSeg, system); + centeredItems.push_back(spannerSeg); } } @@ -2728,61 +2756,15 @@ void SystemLayout::centerElementsBetweenStaves(const System* system) } for (const Segment& seg : toMeasure(mb)->segments()) { for (EngravingItem* item : seg.annotations()) { - if (item->isDynamic() && elementNeedsCenterBetweenStaves(item, system)) { + if ((item->isDynamic() || item->isExpression()) && item->shouldBeCenteredBetweenStaves(system)) { centerElementBetweenStaves(item, system); - Expression* snappedExpr = toDynamic(item)->snappedExpression(); - if (snappedExpr) { - snappedExpr->mutldata()->setPosY(item->ldata()->pos().y()); - } + centeredItems.push_back(item); } } } } -} - -bool SystemLayout::elementNeedsCenterBetweenStaves(const EngravingItem* element, const System* system) -{ - if (!element->isStyled(Pid::OFFSET)) { - // NOTE: because of current limitations of the offset system, we can't center an element that's been manually moved. - return false; - } - const Part* part = element->part(); - bool centerStyle = element->style().styleB(Sid::dynamicsHairpinsAutoCenterOnGrandStaff); - AutoOnOff centerProperty = element->getProperty(Pid::CENTER_BETWEEN_STAVES).value(); - if (part->nstaves() <= 1 || centerProperty == AutoOnOff::OFF || (!centerStyle && centerProperty != AutoOnOff::ON)) { - return false; - } - - if (centerProperty != AutoOnOff::ON && !part->instrument()->isNormallyMultiStaveInstrument()) { - return false; - } - - const Staff* thisStaff = element->staff(); - const std::vector& partStaves = part->staves(); - IF_ASSERT_FAILED(partStaves.size() > 0) { - return false; - } - - if ((thisStaff == partStaves.front() && element->placeAbove()) || (thisStaff == partStaves.back() && element->placeBelow())) { - return false; - } - - staff_idx_t thisIdx = thisStaff->idx(); - if (element->placeAbove()) { - IF_ASSERT_FAILED(thisIdx > 0) { - return false; - } - } - staff_idx_t nextIdx = element->placeAbove() ? thisIdx - 1 : thisIdx + 1; - - const SysStaff* thisSystemStaff = system->staff(thisIdx); - const SysStaff* nextSystemStaff = system->staff(nextIdx); - if (!thisSystemStaff->show() || !nextSystemStaff->show()) { - return false; - } - - return centerProperty == AutoOnOff::ON || element->appliesToAllVoicesInInstrument(); + AlignmentLayout::alignStaffCenteredItems(centeredItems, system); } void SystemLayout::centerElementBetweenStaves(EngravingItem* element, const System* system) @@ -2813,7 +2795,8 @@ void SystemLayout::centerElementBetweenStaves(EngravingItem* element, const Syst SkylineLine thisSkyline = isAbove ? thisStaff->skyline().north() : thisStaff->skyline().south(); thisSkyline.remove_if([element](ShapeElement& shEl) { const EngravingItem* shapeItem = shEl.item(); - return shapeItem && (shapeItem == element || shapeItem->isAccidental()); + return shapeItem && (shapeItem == element || shapeItem->isAccidental() + || shapeItem == element->ldata()->itemSnappedBefore() || shapeItem == element->ldata()->itemSnappedAfter()); }); double edgeOfThisStaff = isAbove ? thisSkyline.top(startX, endX) : thisSkyline.bottom(startX, endX); @@ -2823,8 +2806,18 @@ void SystemLayout::centerElementBetweenStaves(EngravingItem* element, const Syst edgeOfNextStaff += yStaffDiff; double yCenter = 0.5 * (edgeOfThisStaff + edgeOfNextStaff) + visualVerticalCenter(element); + double curY = element->pos().y(); + double yDiff = yCenter - curY; + + element->mutldata()->moveY(yDiff); + + elementBbox.translate(0.0, yDiff); + double elementMinDist = element->minDistance().toMM(element->spatium()); + double availSpaceAbove = (isAbove ? edgeOfNextStaff : edgeOfThisStaff) - (elementBbox.top() - elementMinDist); + double availSpaceBelow = (isAbove ? edgeOfThisStaff : edgeOfNextStaff) - (elementBbox.bottom() + elementMinDist); + element->mutldata()->setStaffCenteringInfo(availSpaceAbove, availSpaceBelow); - element->mutldata()->setPosY(yCenter - element->offset().y()); + updateSkylineForElement(element, system, yDiff); } double SystemLayout::visualVerticalCenter(const EngravingItem* element) diff --git a/src/engraving/rendering/dev/systemlayout.h b/src/engraving/rendering/dev/systemlayout.h index 4ef4be7428860..519a0edb3a259 100644 --- a/src/engraving/rendering/dev/systemlayout.h +++ b/src/engraving/rendering/dev/systemlayout.h @@ -61,6 +61,8 @@ class SystemLayout static void centerElementsBetweenStaves(const System* system); + static void updateSkylineForElement(EngravingItem* element, const System* system, double yMove); + private: static System* getNextSystem(LayoutContext& lc); static void processLines(System* system, LayoutContext& ctx, std::vector lines, bool align); @@ -82,7 +84,6 @@ class SystemLayout std::vector& bl, Measure* measure); static double minVertSpaceForCrossStaffBeams(System* system, staff_idx_t staffIdx1, staff_idx_t staffIdx2); - static bool elementNeedsCenterBetweenStaves(const EngravingItem* element, const System* system); static void centerElementBetweenStaves(EngravingItem* element, const System* system); static double visualVerticalCenter(const EngravingItem* element); }; diff --git a/src/engraving/rendering/dev/tlayout.cpp b/src/engraving/rendering/dev/tlayout.cpp index cd835f8baa113..85b5c3739b66c 100644 --- a/src/engraving/rendering/dev/tlayout.cpp +++ b/src/engraving/rendering/dev/tlayout.cpp @@ -1879,7 +1879,9 @@ void TLayout::layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData void TLayout::layoutDynamic(Dynamic* item, Dynamic::LayoutData* ldata, const LayoutConfiguration& conf) { LAYOUT_CALL_ITEM(item); - const_cast(item)->setSnappedExpression(nullptr); // Here we reset it. It will become known again when we layout expression + + ldata->disconnectItemSnappedAfter(); + ldata->disconnectItemSnappedBefore(); const StaffType* stType = item->staffType(); if (stType && stType->isHiddenElementOnTab(conf.style(), Sid::dynamicsShowTabCommon, Sid::dynamicsShowTabSimple)) { @@ -1950,6 +1952,9 @@ void TLayout::layoutExpression(const Expression* item, Expression::LayoutData* l return; } + const_cast(item)->setPlacementBasedOnVoiceApplication(item->style().styleV( + Sid::dynamicsHairpinVoiceBasedPlacement).value()); + TLayout::layoutBaseTextBase(item, ldata); const Segment* segment = item->segment(); @@ -1975,7 +1980,8 @@ void TLayout::layoutExpression(const Expression* item, Expression::LayoutData* l } } - const_cast(item)->setSnappedDynamic(nullptr); + ldata->disconnectItemSnappedAfter(); + ldata->disconnectItemSnappedBefore(); if (!item->autoplace()) { return; @@ -1987,41 +1993,31 @@ void TLayout::layoutExpression(const Expression* item, Expression::LayoutData* l LD_CONDITION(m->ldata()->isSetPos()); LD_CONDITION(s->ldata()->isSetPos()); - if (!item->snapToDynamics()) { - Autoplace::autoplaceSegmentElement(item, ldata); - return; - } - Dynamic* dynamic = toDynamic(segment->findAnnotation(ElementType::DYNAMIC, item->track(), item->track())); - if (!dynamic || dynamic->placeAbove() != item->placeAbove() || !dynamic->visible()) { + if (!dynamic) { + Segment* timeTickSeg = m->findSegmentR(SegmentType::TimeTick, s->rtick()); + dynamic = timeTickSeg ? toDynamic(timeTickSeg->findAnnotation(ElementType::DYNAMIC, item->track(), item->track())) : nullptr; + } + if (!dynamic || dynamic->placeAbove() != item->placeAbove() || dynamic->applyToVoice() != item->applyToVoice() || !dynamic->visible()) { Autoplace::autoplaceSegmentElement(item, ldata); return; } - const_cast(item)->setSnappedDynamic(dynamic); - dynamic->setSnappedExpression(const_cast(item)); - LD_CONDITION(dynamic->ldata()->isSetBbox()); // dynamic->shape() LD_CONDITION(dynamic->ldata()->isSetPos()); - // If there is a dynamic on same segment and track, lock this expression to it - double padding = item->computeDynamicExpressionDistance(); + // If there is a dynamic on same segment and track make space for it horizontally + double padding = item->computeDynamicExpressionDistance(dynamic); double dynamicRight = dynamic->shape().translate(dynamic->pos()).right(); double expressionLeft = ldata->bbox().translated(item->pos()).left(); double difference = expressionLeft - dynamicRight - padding; ldata->moveX(-difference); - // Keep expression and dynamic vertically aligned - Autoplace::autoplaceSegmentElement(item, ldata); - bool above = item->placeAbove(); - double yExpression = item->pos().y(); - double yDynamic = dynamic->pos().y(); - bool expressionIsOuter = above ? yExpression < yDynamic : yExpression > yDynamic; - if (expressionIsOuter) { - dynamic->mutldata()->moveY((yExpression - yDynamic)); - } else { - ldata->moveY((yDynamic - yExpression)); + if (item->snapToDynamics()) { + ldata->connectItemSnappedBefore(dynamic); } + + Autoplace::autoplaceSegmentElement(item, ldata); } void TLayout::layoutFermata(const Fermata* item, Fermata::LayoutData* ldata, const LayoutConfiguration& conf) @@ -3020,50 +3016,56 @@ void TLayout::layoutHairpinSegment(HairpinSegment* item, LayoutContext& ctx) ldata->setIsSkipDraw(false); const double _spatium = item->spatium(); - Dynamic* sd = item->findStartDynamic(); - Dynamic* ed = item->findEndDynamic(); - double dymax = item->hairpin()->placeBelow() ? -DBL_MAX : DBL_MAX; - if (item->autoplace() && !ctx.conf().isPaletteMode() - && item->explicitParent() // TODO: remove this line (this might happen when Ctrl+Shift+Dragging an item) - ) { - // Try to fit between adjacent dynamics - double minDynamicsDistance = ctx.conf().styleMM(Sid::autoplaceHairpinDynamicsDistance) * item->staff()->staffMag(item->tick()); - if (item->isSingleType() || item->isBeginType()) { - if (sd && sd->addToSkyline() && sd->placement() == item->hairpin()->placement() - && (item->hairpin()->lineVisible() || !item->text()->empty())) { - double segmentXPos = sd->segment()->pos().x() + sd->measure()->pos().x(); - double sdRight = sd->pos().x() + segmentXPos + sd->ldata()->bbox().right(); - if (sd->snappedExpression()) { - Expression* expression = sd->snappedExpression(); - double exprRight = expression->pos().x() + segmentXPos + expression->ldata()->bbox().right(); - sdRight = std::max(sdRight, exprRight); - } - const double dist = std::max(sdRight - item->pos().x() + minDynamicsDistance, 0.0); - ldata->moveX(dist); - item->rxpos2() -= dist; - // prepare to align vertically - dymax = sd->pos().y(); - } + + ldata->disconnectItemSnappedBefore(); + ldata->disconnectItemSnappedAfter(); + + EngravingItem* possibleSnapBeforeElement = nullptr; + EngravingItem* possibleSnapAfterElement = nullptr; + if (item->isSingleBeginType()) { + possibleSnapBeforeElement = item->findElementToSnapBefore(); + } + if (item->isSingleEndType()) { + possibleSnapAfterElement = item->findElementToSnapAfter(); + } + + // In case of dynamics/expressions before or after, make space for them horizontally + double hairpinDistToDynOrExpr = ctx.conf().style().styleMM(Sid::autoplaceHairpinDynamicsDistance); + if (possibleSnapBeforeElement && (possibleSnapBeforeElement->isDynamic() || possibleSnapBeforeElement->isExpression())) { + double xItemPos = possibleSnapBeforeElement->pageX() - item->system()->pageX(); + double itemRightEdge = xItemPos + possibleSnapBeforeElement->ldata()->bbox().right(); + double xMinHairpinStart = itemRightEdge + hairpinDistToDynOrExpr; + double xStartDiff = ldata->pos().x() - xMinHairpinStart; + if (xStartDiff < 0) { + ldata->setPosX(xMinHairpinStart); + item->rxpos2() += xStartDiff; } - if (item->isSingleType() || item->isEndType()) { - if (ed && ed->addToSkyline() && ed->placement() == item->hairpin()->placement() - && (item->hairpin()->lineVisible() || !item->endText()->empty())) { - const double edLeft = ed->ldata()->bbox().left() + ed->pos().x() - + ed->segment()->pos().x() + ed->measure()->pos().x(); - const double dist = edLeft - item->pos2().x() - item->pos().x() - minDynamicsDistance; - const double extendThreshold = 3.0 * _spatium; // TODO: style setting - if (dist < 0.0) { - item->rxpos2() += dist; // always shorten - } else if (dist >= extendThreshold && item->hairpin()->endText().isEmpty() && minDynamicsDistance > 0.0) { - item->rxpos2() += dist; // lengthen only if appropriate - } - // prepare to align vertically - if (item->hairpin()->placeBelow()) { - dymax = std::max(dymax, ed->pos().y()); - } else { - dymax = std::min(dymax, ed->pos().y()); - } - } + } + if (possibleSnapAfterElement && (possibleSnapAfterElement->isDynamic() || possibleSnapAfterElement->isExpression())) { + double xItemPos = possibleSnapAfterElement->pageX() - item->system()->pageX(); + double itemLeftEdge = xItemPos + possibleSnapAfterElement->ldata()->bbox().left(); + double xMaxHairpinEnd = itemLeftEdge - hairpinDistToDynOrExpr; + double xEndDiff = xMaxHairpinEnd - (item->pos().x() + item->pos2().x()); + const double EXTEND_THRESHOLD = 3.0 * _spatium; + if (xEndDiff < 0) { + item->rxpos2() += xEndDiff; + } else if (item->hairpin()->snapToItemAfter() && xEndDiff > EXTEND_THRESHOLD) { + item->rxpos2() += xEndDiff; + } + } + + if (item->hairpin()->snapToItemBefore() && possibleSnapBeforeElement) { + if (possibleSnapBeforeElement->isExpression() || possibleSnapBeforeElement->isDynamic() + || (possibleSnapBeforeElement->isHairpinSegment() + && toHairpinSegment(possibleSnapBeforeElement)->hairpin()->snapToItemAfter())) { + ldata->connectItemSnappedBefore(possibleSnapBeforeElement); + } + } + if (item->hairpin()->snapToItemAfter() && possibleSnapAfterElement) { + if (possibleSnapAfterElement->isExpression() || possibleSnapAfterElement->isDynamic() + || (possibleSnapAfterElement->isHairpinSegment() + && toHairpinSegment(possibleSnapAfterElement)->hairpin()->snapToItemBefore())) { + ldata->connectItemSnappedAfter(possibleSnapAfterElement); } } @@ -3201,103 +3203,9 @@ void TLayout::layoutHairpinSegment(HairpinSegment* item, LayoutContext& ctx) } if (item->autoplace()) { - double ymax = item->pos().y(); - double d; - double ddiff = item->hairpin()->isLineType() ? 0.0 : _spatium * 0.5; - - double sp = item->spatium(); - - // TODO: in the future, there should be a minDistance style setting for hairpinLines as well as hairpins. - double minDist = item->twoLines() ? item->minDistance().val() : ctx.conf().styleS(Sid::dynamicsMinDistance).val(); - double md = minDist * sp; - - bool above = item->spanner()->placeAbove(); - SkylineLine sl(!above); - Shape sh = item->shape(); - sl.add(sh.translate(item->pos())); - if (above) { - d = item->system()->topDistance(item->staffIdx(), sl); - if (d > -md) { - ymax -= d + md; - } - // align hairpin with dynamics - if (!item->hairpin()->diagonal()) { - ymax = std::min(ymax, dymax - ddiff); - } - } else { - d = item->system()->bottomDistance(item->staffIdx(), sl); - if (d > -md) { - ymax += d + md; - } - // align hairpin with dynamics - if (!item->hairpin()->diagonal()) { - ymax = std::max(ymax, dymax - ddiff); - } - } - double yd = ymax - item->pos().y(); - if (!RealIsNull(yd)) { - if (ldata->offsetChanged() != OffsetChange::NONE) { - // user moved element within the skyline - // we may need to adjust minDistance, yd, and/or offset - double adj = item->pos().y() + rebase; - bool inStaff = above ? sh.bottom() + adj > 0.0 : sh.top() + adj < item->staff()->staffHeight(); - Autoplace::rebaseMinDistance(item, ldata, md, yd, sp, rebase, above, inStaff); - } - ldata->moveY(yd); - } - - if (item->hairpin()->addToSkyline() && !item->hairpin()->diagonal()) { - // align dynamics with hairpin - if (sd && sd->autoplace() && sd->placement() == item->hairpin()->placement() - && (item->hairpin()->lineVisible() || !item->text()->empty())) { - double ny = item->y() + ddiff - sd->offset().y(); - if (sd->placeAbove()) { - ny = std::min(ny, sd->ldata()->pos().y()); - } else { - ny = std::max(ny, sd->ldata()->pos().y()); - } - if (sd->ldata()->pos().y() != ny) { - sd->mutldata()->setPosY(ny); - if (sd->snappedExpression()) { - sd->snappedExpression()->mutldata()->setPosY(ny); - } - if (sd->addToSkyline()) { - Segment* s = sd->segment(); - Measure* m = s->measure(); - RectF r = sd->ldata()->bbox().translated(sd->pos()); - s->staffShape(sd->staffIdx()).add(r); - r = sd->ldata()->bbox().translated(sd->pos() + s->pos() + m->pos()); - m->system()->staff(sd->staffIdx())->skyline().add(r, sd); - } - } - } - if (ed && ed->autoplace() && ed->placement() == item->hairpin()->placement() - && (item->hairpin()->lineVisible() || !item->endText()->empty())) { - double ny = item->y() + ddiff - ed->offset().y(); - if (ed->placeAbove()) { - ny = std::min(ny, ed->ldata()->pos().y()); - } else { - ny = std::max(ny, ed->ldata()->pos().y()); - } - if (ed->ldata()->pos().y() != ny) { - ed->mutldata()->setPosY(ny); - Expression* snappedExpression = ed->snappedExpression(); - if (snappedExpression) { - double yOffsetDiff = snappedExpression->offset().y() - ed->offset().y(); - snappedExpression->mutldata()->setPosY(ny - yOffsetDiff); - } - if (ed->addToSkyline()) { - Segment* s = ed->segment(); - Measure* m = s->measure(); - RectF r = ed->ldata()->bbox().translated(ed->pos()); - s->staffShape(ed->staffIdx()).add(r); - r = ed->ldata()->bbox().translated(ed->pos() + s->pos() + m->pos()); - m->system()->staff(ed->staffIdx())->skyline().add(r, ed); - } - } - } - } + Autoplace::autoplaceSpannerSegment(item, ldata, item->spatium()); } + Autoplace::setOffsetChanged(item, ldata, false); } diff --git a/src/engraving/rendering/stable/tlayout.cpp b/src/engraving/rendering/stable/tlayout.cpp index 060c93ec90b3e..c6eb37b0567b8 100644 --- a/src/engraving/rendering/stable/tlayout.cpp +++ b/src/engraving/rendering/stable/tlayout.cpp @@ -1820,7 +1820,8 @@ void TLayout::layoutDeadSlapped(const DeadSlapped* item, DeadSlapped::LayoutData void TLayout::layoutDynamic(const Dynamic* item, Dynamic::LayoutData* ldata, const LayoutConfiguration& conf) { - const_cast(item)->setSnappedExpression(nullptr); // Here we reset it. It will become known again when we layout expression + ldata->disconnectItemSnappedAfter(); + ldata->disconnectItemSnappedBefore(); const StaffType* stType = item->staffType(); if (stType && stType->isHiddenElementOnTab(conf.style(), Sid::dynamicsShowTabCommon, Sid::dynamicsShowTabSimple)) { @@ -1917,7 +1918,8 @@ void TLayout::layoutExpression(const Expression* item, Expression::LayoutData* l } } - const_cast(item)->setSnappedDynamic(nullptr); + ldata->disconnectItemSnappedAfter(); + ldata->disconnectItemSnappedBefore(); if (!item->autoplace()) { return; @@ -1940,14 +1942,13 @@ void TLayout::layoutExpression(const Expression* item, Expression::LayoutData* l return; } - const_cast(item)->setSnappedDynamic(dynamic); - dynamic->setSnappedExpression(const_cast(item)); + ldata->connectItemSnappedBefore(dynamic); LD_CONDITION(dynamic->ldata()->isSetBbox()); // dynamic->shape() LD_CONDITION(dynamic->ldata()->isSetPos()); // If there is a dynamic on same segment and track, lock this expression to it - double padding = item->computeDynamicExpressionDistance(); + double padding = item->computeDynamicExpressionDistance(dynamic); double dynamicRight = dynamic->shape().translate(dynamic->pos()).right(); double expressionLeft = ldata->bbox().translated(item->pos()).left(); double difference = expressionLeft - dynamicRight - padding; diff --git a/src/engraving/rw/read410/tread.cpp b/src/engraving/rw/read410/tread.cpp index 5c5ad7d75e9b9..7baa360fbe619 100644 --- a/src/engraving/rw/read410/tread.cpp +++ b/src/engraving/rw/read410/tread.cpp @@ -785,9 +785,6 @@ void TRead::read(Dynamic* d, XmlReader& e, ReadContext& ctx) } else if (readProperty(d, tag, e, ctx, Pid::DYNAMICS_SIZE)) { } else if (readProperty(d, tag, e, ctx, Pid::CENTER_ON_NOTEHEAD)) { } else if (readProperty(d, tag, e, ctx, Pid::ANCHOR_TO_END_OF_PREVIOUS)) { - } else if (readProperty(d, tag, e, ctx, Pid::APPLY_TO_VOICE)) { - } else if (readProperty(d, tag, e, ctx, Pid::DIRECTION)) { - } else if (readProperty(d, tag, e, ctx, Pid::CENTER_BETWEEN_STAVES)) { } else if (!readProperties(static_cast(d), e, ctx)) { e.unknown(); } @@ -3077,6 +3074,8 @@ void TRead::read(Hairpin* h, XmlReader& e, ReadContext& ctx) } else if (readProperty(h, tag, e, ctx, Pid::APPLY_TO_VOICE)) { } else if (readProperty(h, tag, e, ctx, Pid::DIRECTION)) { } else if (readProperty(h, tag, e, ctx, Pid::CENTER_BETWEEN_STAVES)) { + } else if (readProperty(h, tag, e, ctx, Pid::SNAP_BEFORE)) { + } else if (readProperty(h, tag, e, ctx, Pid::SNAP_AFTER)) { } else if (!readProperties(static_cast(h), e, ctx)) { e.unknown(); } @@ -4604,6 +4603,9 @@ bool TRead::readProperties(TextBase* t, XmlReader& e, ReadContext& ctx) t->setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED); } } else if (TRead::readProperty(t, tag, e, ctx, Pid::TEXT_LINKED_TO_MASTER)) { + } else if (readProperty(t, tag, e, ctx, Pid::APPLY_TO_VOICE)) { + } else if (readProperty(t, tag, e, ctx, Pid::DIRECTION)) { + } else if (readProperty(t, tag, e, ctx, Pid::CENTER_BETWEEN_STAVES)) { } else if (!readItemProperties(t, e, ctx)) { return false; } diff --git a/src/engraving/rw/write/twrite.cpp b/src/engraving/rw/write/twrite.cpp index a9d7e6f55e63b..6e387654dd4c1 100644 --- a/src/engraving/rw/write/twrite.cpp +++ b/src/engraving/rw/write/twrite.cpp @@ -1124,10 +1124,6 @@ void TWrite::write(const Dynamic* item, XmlWriter& xml, WriteContext& ctx) writeProperty(item, xml, Pid::PLAY); writeProperty(item, xml, Pid::ANCHOR_TO_END_OF_PREVIOUS); - writeProperty(item, xml, Pid::APPLY_TO_VOICE); - writeProperty(item, xml, Pid::DIRECTION); - writeProperty(item, xml, Pid::CENTER_BETWEEN_STAVES); - if (item->isVelocityChangeAvailable()) { writeProperty(item, xml, Pid::VELO_CHANGE); writeProperty(item, xml, Pid::VELO_CHANGE_SPEED); @@ -1149,6 +1145,12 @@ void TWrite::write(const Expression* item, XmlWriter& xml, WriteContext& ctx) void TWrite::writeProperties(const TextBase* item, XmlWriter& xml, WriteContext& ctx, bool writeText) { + if (item->hasVoiceApplicationProperties()) { + writeProperty(item, xml, Pid::APPLY_TO_VOICE); + writeProperty(item, xml, Pid::DIRECTION); + writeProperty(item, xml, Pid::CENTER_BETWEEN_STAVES); + } + writeItemProperties(item, xml, ctx); writeProperty(item, xml, Pid::TEXT_STYLE); @@ -1605,6 +1607,9 @@ void TWrite::write(const Hairpin* item, XmlWriter& xml, WriteContext& ctx) writeProperty(item, xml, Pid::DIRECTION); writeProperty(item, xml, Pid::CENTER_BETWEEN_STAVES); + writeProperty(item, xml, Pid::SNAP_BEFORE); + writeProperty(item, xml, Pid::SNAP_AFTER); + for (const StyledProperty& spp : *item->styledProperties()) { if (!item->isStyled(spp.pid)) { writeProperty(item, xml, spp.pid); diff --git a/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp b/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp index 36c4730fed13a..78dc4560a1bd3 100644 --- a/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp +++ b/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp @@ -5,7 +5,7 @@ using namespace mu::inspector; ExpressionSettingsModel::ExpressionSettingsModel(QObject* parent, IElementRepositoryService* repository) - : AbstractInspectorModel(parent, repository) + : InspectorModelWithVoiceAndPositionOptions(parent, repository) { setModelType(InspectorModelType::TYPE_EXPRESSION); setTitle(muse::qtrc("inspector ", "Expression")); @@ -15,6 +15,8 @@ ExpressionSettingsModel::ExpressionSettingsModel(QObject* parent, IElementReposi void ExpressionSettingsModel::createProperties() { + InspectorModelWithVoiceAndPositionOptions::createProperties(); + m_snapExpression = buildPropertyItem(mu::engraving::Pid::SNAP_TO_DYNAMICS); } @@ -25,11 +27,15 @@ void ExpressionSettingsModel::requestElements() void ExpressionSettingsModel::loadProperties() { + InspectorModelWithVoiceAndPositionOptions::loadProperties(); + loadPropertyItem(m_snapExpression); } void ExpressionSettingsModel::resetProperties() { + InspectorModelWithVoiceAndPositionOptions::resetProperties(); + m_snapExpression->resetToDefault(); } diff --git a/src/inspector/models/notation/expressions/expressionsettingsmodel.h b/src/inspector/models/notation/expressions/expressionsettingsmodel.h index 72088750329aa..cb57f093e287c 100644 --- a/src/inspector/models/notation/expressions/expressionsettingsmodel.h +++ b/src/inspector/models/notation/expressions/expressionsettingsmodel.h @@ -1,10 +1,10 @@ #ifndef EXPRESSIONSETTINGSMODEL_H #define EXPRESSIONSETTINGSMODEL_H -#include "models/abstractinspectormodel.h" +#include "models/inspectormodelwithvoiceandpositionoptions.h" namespace mu::inspector { -class ExpressionSettingsModel : public AbstractInspectorModel +class ExpressionSettingsModel : public InspectorModelWithVoiceAndPositionOptions { Q_OBJECT diff --git a/src/inspector/models/notation/lines/hairpinlinesettingsmodel.cpp b/src/inspector/models/notation/lines/hairpinlinesettingsmodel.cpp index 2d1737a5ae2b9..deacfbb1d7fe5 100644 --- a/src/inspector/models/notation/lines/hairpinlinesettingsmodel.cpp +++ b/src/inspector/models/notation/lines/hairpinlinesettingsmodel.cpp @@ -50,15 +50,44 @@ HairpinLineSettingsModel::HairpinLineSettingsModel(QObject* parent, IElementRepo createProperties(); } +PropertyItem* HairpinLineSettingsModel::snapBefore() const +{ + return m_snapBefore; +} + +PropertyItem* HairpinLineSettingsModel::snapAfter() const +{ + return m_snapAfter; +} + void HairpinLineSettingsModel::createProperties() { TextLineSettingsModel::createProperties(); + m_snapBefore = buildPropertyItem(mu::engraving::Pid::SNAP_BEFORE); + m_snapAfter = buildPropertyItem(mu::engraving::Pid::SNAP_AFTER); + isLineVisible()->setIsVisible(true); allowDiagonal()->setIsVisible(false); placement()->setIsVisible(true); } +void HairpinLineSettingsModel::loadProperties() +{ + TextLineSettingsModel::loadProperties(); + + loadPropertyItem(m_snapBefore); + loadPropertyItem(m_snapAfter); +} + +void HairpinLineSettingsModel::resetProperties() +{ + TextLineSettingsModel::resetProperties(); + + m_snapBefore->resetToDefault(); + m_snapAfter->resetToDefault(); +} + void HairpinLineSettingsModel::requestElements() { m_elementList = m_repository->findElementsByType(mu::engraving::ElementType::HAIRPIN, [this](const mu::engraving::EngravingItem* element) -> bool { diff --git a/src/inspector/models/notation/lines/hairpinlinesettingsmodel.h b/src/inspector/models/notation/lines/hairpinlinesettingsmodel.h index f260d3ada66eb..de478f40e4cef 100644 --- a/src/inspector/models/notation/lines/hairpinlinesettingsmodel.h +++ b/src/inspector/models/notation/lines/hairpinlinesettingsmodel.h @@ -29,6 +29,9 @@ class HairpinLineSettingsModel : public TextLineSettingsModel { Q_OBJECT + Q_PROPERTY(PropertyItem * snapBefore READ snapBefore CONSTANT) + Q_PROPERTY(PropertyItem * snapAfter READ snapAfter CONSTANT) + public: enum HairpinLineType { Crescendo, @@ -37,11 +40,19 @@ class HairpinLineSettingsModel : public TextLineSettingsModel explicit HairpinLineSettingsModel(QObject* parent, IElementRepositoryService* repository, HairpinLineType lineType); + PropertyItem* snapBefore() const; + PropertyItem* snapAfter() const; + private: engraving::HairpinType m_hairpinType = engraving::HairpinType::CRESC_LINE; void createProperties() override; + void loadProperties() override; + void resetProperties() override; void requestElements() override; + + PropertyItem* m_snapBefore = nullptr; + PropertyItem* m_snapAfter = nullptr; }; } diff --git a/src/inspector/models/notation/lines/hairpinsettingsmodel.cpp b/src/inspector/models/notation/lines/hairpinsettingsmodel.cpp index f9cf3640888e1..48d4ee4107274 100644 --- a/src/inspector/models/notation/lines/hairpinsettingsmodel.cpp +++ b/src/inspector/models/notation/lines/hairpinsettingsmodel.cpp @@ -56,6 +56,16 @@ PropertyItem* HairpinSettingsModel::continuousHeight() const return m_continuousHeight; } +PropertyItem* HairpinSettingsModel::snapBefore() const +{ + return m_snapBefore; +} + +PropertyItem* HairpinSettingsModel::snapAfter() const +{ + return m_snapAfter; +} + void HairpinSettingsModel::createProperties() { TextLineSettingsModel::createProperties(); @@ -64,6 +74,9 @@ void HairpinSettingsModel::createProperties() m_height = buildPropertyItem(mu::engraving::Pid::HAIRPIN_HEIGHT); m_continuousHeight = buildPropertyItem(mu::engraving::Pid::HAIRPIN_CONT_HEIGHT); + m_snapBefore = buildPropertyItem(mu::engraving::Pid::SNAP_BEFORE); + m_snapAfter = buildPropertyItem(mu::engraving::Pid::SNAP_AFTER); + isLineVisible()->setIsVisible(false); allowDiagonal()->setIsVisible(true); placement()->setIsVisible(true); @@ -77,6 +90,8 @@ void HairpinSettingsModel::loadProperties() Pid::HAIRPIN_CIRCLEDTIP, Pid::HAIRPIN_HEIGHT, Pid::HAIRPIN_CONT_HEIGHT, + Pid::SNAP_BEFORE, + Pid::SNAP_AFTER }; loadProperties(propertyIdSet); @@ -89,6 +104,8 @@ void HairpinSettingsModel::resetProperties() m_isNienteCircleVisible->resetToDefault(); m_height->resetToDefault(); m_continuousHeight->resetToDefault(); + m_snapBefore->resetToDefault(); + m_snapAfter->resetToDefault(); } void HairpinSettingsModel::requestElements() @@ -124,4 +141,12 @@ void HairpinSettingsModel::loadProperties(const PropertyIdSet& propertyIdSet) if (muse::contains(propertyIdSet, Pid::HAIRPIN_CONT_HEIGHT)) { loadPropertyItem(m_continuousHeight, formatDoubleFunc); } + + if (muse::contains(propertyIdSet, Pid::SNAP_BEFORE)) { + loadPropertyItem(m_snapBefore); + } + + if (muse::contains(propertyIdSet, Pid::SNAP_AFTER)) { + loadPropertyItem(m_snapAfter); + } } diff --git a/src/inspector/models/notation/lines/hairpinsettingsmodel.h b/src/inspector/models/notation/lines/hairpinsettingsmodel.h index 85043c5946a49..0654b67f6a6b2 100644 --- a/src/inspector/models/notation/lines/hairpinsettingsmodel.h +++ b/src/inspector/models/notation/lines/hairpinsettingsmodel.h @@ -33,6 +33,8 @@ class HairpinSettingsModel : public TextLineSettingsModel Q_PROPERTY(PropertyItem * height READ height CONSTANT) Q_PROPERTY(PropertyItem * continuousHeight READ continuousHeight CONSTANT) + Q_PROPERTY(PropertyItem * snapBefore READ snapBefore CONSTANT) + Q_PROPERTY(PropertyItem * snapAfter READ snapAfter CONSTANT) public: explicit HairpinSettingsModel(QObject* parent, IElementRepositoryService* repository); @@ -42,6 +44,9 @@ class HairpinSettingsModel : public TextLineSettingsModel PropertyItem* height() const; PropertyItem* continuousHeight() const; + PropertyItem* snapBefore() const; + PropertyItem* snapAfter() const; + private: void createProperties() override; void loadProperties() override; @@ -56,6 +61,9 @@ class HairpinSettingsModel : public TextLineSettingsModel PropertyItem* m_height = nullptr; PropertyItem* m_continuousHeight = nullptr; + + PropertyItem* m_snapBefore = nullptr; + PropertyItem* m_snapAfter = nullptr; }; } diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml index 84e4f777f7893..6d95369396651 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml @@ -79,10 +79,18 @@ Column { navigation.panel: root.navigationPanel navigation.row: root.navigationRowStart - text: qsTrc("inspector", "Snap to dynamic") + text: qsTrc("inspector", "Align with preceding dynamic") propertyItem: root.model ? root.model.snapExpression : null } } + } + + VoicesAndPositionSection { + id: voicesAndPositionSection + + navigationPanel: root.navigationPanel + navigationRowStart: layoutSection.navigationRowEnd + 1 + model: root.model } } diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/CrescDimLineStyleSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/CrescDimLineStyleSettings.qml index 38ec3354b7fd2..6ab1b0734de58 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/CrescDimLineStyleSettings.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/CrescDimLineStyleSettings.qml @@ -40,6 +40,20 @@ Column { spacing: 12 + PropertyCheckBox { + text: qsTrc("inspector", "Align with preceding hairpin or dynamics") + propertyItem: root.model ? root.model.snapBefore : null + + // TODO: navigation + } + + PropertyCheckBox { + text: qsTrc("inspector", "Align with following hairpin or dynamics") + propertyItem: root.model ? root.model.snapAfter : null + + // TODO: navigation + } + Column { width: parent.width diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/HairpinStyleSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/HairpinStyleSettings.qml index 038e58e09bcbe..2b368b6d8c0b1 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/HairpinStyleSettings.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/lines/internal/HairpinStyleSettings.qml @@ -46,6 +46,21 @@ FocusableItem { spacing: 12 + + PropertyCheckBox { + text: qsTrc("inspector", "Align with preceding hairpin or dynamics") + propertyItem: root.model ? root.model.snapBefore : null + + // TODO: navigation + } + + PropertyCheckBox { + text: qsTrc("inspector", "Align with following hairpin or dynamics") + propertyItem: root.model ? root.model.snapAfter : null + + // TODO: navigation + } + Item { height: childrenRect.height width: parent.width diff --git a/src/notation/internal/notationinteraction.cpp b/src/notation/internal/notationinteraction.cpp index eb986bfd6bb15..a013a0de0e4a9 100644 --- a/src/notation/internal/notationinteraction.cpp +++ b/src/notation/internal/notationinteraction.cpp @@ -1059,6 +1059,9 @@ void NotationInteraction::drag(const PointF& fromPos, const PointF& toPos, DragM } else if (m_editData.element && !m_editData.element->hasGrips()) { m_dragData.ed.delta = evtDelta; m_editData.element->editDrag(m_dragData.ed); + for (auto& group : m_dragData.dragGroups) { + score()->addRefresh(group->drag(m_dragData.ed)); + } } else { for (auto& group : m_dragData.dragGroups) { score()->addRefresh(group->drag(m_dragData.ed));