diff --git a/src/engraving/dom/dom.cmake b/src/engraving/dom/dom.cmake index 2b0e6475c06b7..a1ecb4c5572d3 100644 --- a/src/engraving/dom/dom.cmake +++ b/src/engraving/dom/dom.cmake @@ -343,6 +343,8 @@ set(DOM_SRC ${CMAKE_CURRENT_LIST_DIR}/textlinebase.h ${CMAKE_CURRENT_LIST_DIR}/tie.cpp ${CMAKE_CURRENT_LIST_DIR}/tie.h + ${CMAKE_CURRENT_LIST_DIR}/tiejumppointlist.cpp + ${CMAKE_CURRENT_LIST_DIR}/tiejumppointlist.h ${CMAKE_CURRENT_LIST_DIR}/tiemap.h ${CMAKE_CURRENT_LIST_DIR}/timesig.cpp ${CMAKE_CURRENT_LIST_DIR}/timesig.h diff --git a/src/engraving/dom/edit.cpp b/src/engraving/dom/edit.cpp index 6519e366eb54a..ce23219f5d88e 100644 --- a/src/engraving/dom/edit.cpp +++ b/src/engraving/dom/edit.cpp @@ -1900,6 +1900,9 @@ static Tie* createAndAddTie(Note* startNote, Note* endNote) tie->setTrack(startNote->track()); tie->setTick(startNote->chord()->segment()->tick()); if (endNote) { + if (endNote->tieBack()) { + score->undoRemoveElement(endNote->tieBack()); + } tie->setEndNote(endNote); tie->setTicks(endNote->chord()->segment()->tick() - startNote->chord()->segment()->tick()); } diff --git a/src/engraving/dom/note.cpp b/src/engraving/dom/note.cpp index ba0fefdc9ed23..cbe605adac293 100644 --- a/src/engraving/dom/note.cpp +++ b/src/engraving/dom/note.cpp @@ -2509,13 +2509,13 @@ PartialTie* Note::outgoingPartialTie() const void Note::setTieFor(Tie* t) { m_tieFor = t; - m_jumpPoints.setStartTie(m_tieFor); } void Note::setTieBack(Tie* t) { if (m_tieBack && t && m_tieBack->jumpPoint()) { t->setJumpPoint(m_tieBack->jumpPoint()); + m_tieBack->setJumpPoint(nullptr); } m_tieBack = t; } diff --git a/src/engraving/dom/note.h b/src/engraving/dom/note.h index dd105f1656d60..6ee51d9424356 100644 --- a/src/engraving/dom/note.h +++ b/src/engraving/dom/note.h @@ -36,6 +36,7 @@ #include "pitchspelling.h" #include "symbol.h" #include "tie.h" +#include "tiejumppointlist.h" #include "types.h" namespace mu::engraving { @@ -574,6 +575,6 @@ class Note final : public EngravingItem String m_fretString; std::vector m_lineAttachPoints; - TieJumpPointList m_jumpPoints; + TieJumpPointList m_jumpPoints { this }; }; } // namespace mu::engraving diff --git a/src/engraving/dom/tie.cpp b/src/engraving/dom/tie.cpp index a43809274d19a..db58fb76205ec 100644 --- a/src/engraving/dom/tie.cpp +++ b/src/engraving/dom/tie.cpp @@ -31,7 +31,6 @@ #include "factory.h" #include "hook.h" #include "ledgerline.h" -#include "marker.h" #include "masterscore.h" #include "measure.h" #include "mscoreview.h" @@ -45,7 +44,7 @@ #include "stafftype.h" #include "stem.h" #include "system.h" -#include "types/typesconv.h" +#include "tiejumppointlist.h" #include "undo.h" #include "utils.h" #include "volta.h" @@ -215,17 +214,23 @@ Tie::Tie(const ElementType& type, EngravingItem* parent) setAnchor(Anchor::NOTE); } +TieJumpPointList* Tie::startTieJumpPoints() const +{ + return m_jumpPoint ? m_jumpPoint->jumpPointList() : nullptr; +} + void Tie::updatePossibleJumpPoints() { - const Note* note = toNote(parentItem()); - const Chord* chord = note->chord(); - const Measure* measure = chord->measure(); if (!tieJumpPoints()) { return; } tieJumpPoints()->clear(); + const Note* note = toNote(parentItem()); + const Chord* chord = note->chord(); + const Measure* measure = chord->measure(); + if (!chord->hasFollowingJumpItem()) { return; } @@ -276,7 +281,7 @@ void Tie::addTiesToJumpPoints() jumpPoint->undoSetActive(true); continue; } - jumpPoints->addTieToScore(jumpPoint); + jumpPoints->undoAddTieToScore(jumpPoint); } } @@ -451,254 +456,18 @@ bool Tie::isCrossStaff() const || (endChord && (endChord->staffMove() != 0 || endChord->vStaffIdx() != staff)); } -//--------------------------------------------------------- -// PartialTieJumpPoint -//--------------------------------------------------------- - -TieJumpPoint::TieJumpPoint(Note* note, bool active, int idx, bool followingNote) - : m_note(note), m_active(active), m_followingNote(followingNote) -{ - m_id = u"jumpPoint" + String::fromStdString(std::to_string(idx)); - if (active && endTie()) { - endTie()->setJumpPoint(this); - } -} - -Tie* TieJumpPoint::endTie() const -{ - return m_note ? m_note->tieBack() : nullptr; -} - -void TieJumpPoint::undoSetActive(bool v) -{ - Score* score = m_note ? m_note->score() : nullptr; - if (!score || m_active == v) { - return; - } - score->undo(new ChangeTieJumpPointActive(m_jumpPointList, m_id, v)); -} - -const String TieJumpPoint::menuTitle() const -{ - const Measure* measure = m_note->findMeasure(); - const int measureNo = measure ? measure->no() + 1 : 0; - const TranslatableString tieTo("engraving", "Tie to "); - const String title = tieTo.str + precedingJumpItemName() + u" " + muse::mtrc("engraving", "(m. %1)").arg(measureNo); - - return title; -} - -String TieJumpPoint::precedingJumpItemName() const -{ - const Chord* startChord = m_note->chord(); - const Segment* seg = startChord->segment(); - const Measure* measure = seg->measure(); - - if (seg->score()->firstSegment(SegmentType::ChordRest) == seg) { - return muse::mtrc("engraving", "start of score"); - } - - // Markers - for (const EngravingItem* e : measure->el()) { - if (!e->isMarker()) { - continue; - } - - const Marker* marker = toMarker(e); - if (muse::contains(Marker::RIGHT_MARKERS, marker->markerType())) { - continue; - } - - if (marker->markerType() == MarkerType::CODA || marker->markerType() == MarkerType::VARCODA) { - return muse::mtrc("engraving", "coda"); - } else { - return muse::mtrc("engraving", "segno"); - } - } - - // Voltas - auto spanners = m_note->score()->spannerMap().findOverlapping(measure->tick().ticks(), measure->tick().ticks()); - for (auto& spanner : spanners) { - if (!spanner.value->isVolta() || Fraction::fromTicks(spanner.start) != startChord->tick()) { - continue; - } - - Volta* volta = toVolta(spanner.value); - - return muse::mtrc("engraving", "“%1” volta").arg(volta->beginText()); - } - - // Repeat barlines - if (measure->repeatStart()) { - return muse::mtrc("engraving", "start repeat"); - } - - for (Segment* prevSeg = seg->prev(SegmentType::BarLineType); prevSeg && prevSeg->tick() == seg->tick(); - prevSeg = prevSeg->prev(SegmentType::BarLineType)) { - EngravingItem* el = prevSeg->element(startChord->track()); - if (!el || !el->isBarLine()) { - continue; - } - - BarLine* bl = toBarLine(el); - if (bl->barLineType() & (BarLineType::START_REPEAT | BarLineType::END_START_REPEAT)) { - return muse::mtrc("engraving", "start repeat"); - } - } - - if (m_note->tieBack() && m_note->tieBack()->startNote()) { - return muse::mtrc("engraving", "next note"); - } - - return muse::mtrc("engraving", "invalid"); -} - -//--------------------------------------------------------- -// PartialTieJumpPointList -//--------------------------------------------------------- - -TieJumpPointList::~TieJumpPointList() -{ - muse::DeleteAll(m_jumpPoints); - m_jumpPoints.clear(); -} - -void TieJumpPointList::add(TieJumpPoint* item) -{ - item->setJumpPointList(this); - m_jumpPoints.push_back(item); -} - -void TieJumpPointList::clear() -{ - for (const TieJumpPoint* jumpPoint : m_jumpPoints) { - Tie* endTie = jumpPoint->endTie(); - if (!endTie) { - continue; - } - endTie->setJumpPoint(nullptr); - } - muse::DeleteAll(m_jumpPoints); - m_jumpPoints.clear(); -} - -TieJumpPoint* TieJumpPointList::findJumpPoint(const String& id) -{ - for (TieJumpPoint* jumpPoint : m_jumpPoints) { - if (jumpPoint->id() != id) { - continue; - } - - return jumpPoint; - } - return nullptr; -} - -void TieJumpPointList::toggleJumpPoint(const String& id) -{ - TieJumpPoint* end = findJumpPoint(id); - - if (!end) { - LOGE() << "No partial tie end point found with id: " << id; - return; - } - - Score* score = end->note() ? end->note()->score() : nullptr; - if (!score) { - return; - } - - score->startCmd(TranslatableString("engraving", "Toggle partial tie")); - const bool checked = end->active(); - if (checked) { - undoRemoveTieFromScore(end); - } else { - addTieToScore(end); - } - score->endCmd(); -} - -void TieJumpPointList::addTieToScore(TieJumpPoint* jumpPoint) -{ - Note* note = jumpPoint->note(); - Score* score = note ? note->score() : nullptr; - if (!m_startTie || !score) { - return; - } - - if (jumpPoint->followingNote()) { - // Remove partial tie and add full tie - if (!m_startTie->isPartialTie() || !toPartialTie(m_startTie)->isOutgoing()) { - return; - } - jumpPoint->undoSetActive(true); - m_startTie = Tie::changeTieType(m_startTie, note); - return; - } - - jumpPoint->undoSetActive(true); - - // Check if there is already a tie. If so, add partial tie info to it - Tie* tieBack = note->tieBack(); - if (tieBack && !tieBack->isPartialTie()) { - tieBack->setJumpPoint(jumpPoint); - return; - } - // Otherwise create incoming partial tie on note - PartialTie* pt = Factory::createPartialTie(note); - pt->setParent(note); - pt->setEndNote(note); - pt->setJumpPoint(jumpPoint); - score->undoAddElement(pt); -} - -void TieJumpPointList::undoRemoveTieFromScore(TieJumpPoint* jumpPoint) -{ - Note* note = jumpPoint->note(); - Score* score = note ? note->score() : nullptr; - if (!m_startTie || !score) { - return; - } - - if (jumpPoint->followingNote()) { - // Remove full tie and add partial tie - if (m_startTie->isPartialTie()) { - return; - } - jumpPoint->undoSetActive(false); - - m_startTie = Tie::changeTieType(m_startTie); - return; - } - - jumpPoint->undoSetActive(false); - - // Check if there is a full tie. If so, remove partial tie info from it - Tie* tieBack = note->tieBack(); - if (tieBack && !tieBack->isPartialTie()) { - tieBack->setJumpPoint(nullptr); - return; - } - // Otherwise remove incoming partial tie on note - PartialTie* pt = note->incomingPartialTie(); - if (!pt) { - return; - } - score->undoRemoveElement(pt); -} - -Tie* Tie::changeTieType(Tie* oldTie, Note* endNote) +void Tie::changeTieType(Tie* oldTie, Note* endNote) { // Replaces oldTie with an outgoing partial tie if no endNote is specified. Otherwise replaces oldTie with a regular tie Note* startNote = oldTie->startNote(); bool addPartialTie = !endNote; Score* score = startNote ? startNote->score() : nullptr; if (!score) { - return nullptr; + return; } - TranslatableString undoCmd = addPartialTie ? TranslatableString("engraving", "Replace full tie with partial tie") : TranslatableString( - "engraving", "Replace partial tie with full tie"); + TranslatableString undoCmd = addPartialTie ? TranslatableString("engraving", "Replace full tie with partial tie") + : TranslatableString("engraving", "Replace partial tie with full tie"); Tie* newTie = addPartialTie ? Factory::createPartialTie(score->dummy()->note()) : Factory::createTie(score->dummy()->note()); score->undoRemoveElement(oldTie); @@ -724,8 +493,6 @@ Tie* Tie::changeTieType(Tie* oldTie, Note* endNote) score->undoAddElement(newTie); score->endCmd(); - - return newTie; } void Tie::updateStartTieOnRemoval() @@ -739,4 +506,9 @@ void Tie::updateStartTieOnRemoval() score()->undoRemoveElement(_startTie); } } + +Tie* Tie::startTie() const +{ + return startTieJumpPoints() ? startTieJumpPoints()->startTie() : nullptr; +} } diff --git a/src/engraving/dom/tie.h b/src/engraving/dom/tie.h index d2ca9e4c3c81c..b4c8c60b0eb24 100644 --- a/src/engraving/dom/tie.h +++ b/src/engraving/dom/tie.h @@ -26,63 +26,7 @@ namespace mu::engraving { class TieJumpPointList; -class TieJumpPoint -{ -public: - TieJumpPoint(Note* note, bool active, int idx, bool followingNote); - TieJumpPoint() {} - - Note* note() const { return m_note; } - Tie* endTie() const; - bool followingNote() const { return m_followingNote; } - const String& id() const { return m_id; } - bool active() const { return m_active; } - void setActive(bool v) { m_active = v; } - void undoSetActive(bool v); - void setJumpPointList(TieJumpPointList* jumpPointList) { m_jumpPointList = jumpPointList; } - TieJumpPointList* jumpPointList() { return m_jumpPointList; } - - const String menuTitle() const; - -private: - String precedingJumpItemName() const; - Note* m_note = nullptr; - bool m_active = false; - String m_id; - TieJumpPointList* m_jumpPointList = nullptr; - bool m_followingNote = false; -}; - -class TieJumpPointList -{ -public: - TieJumpPointList() = default; - ~TieJumpPointList(); - - void add(TieJumpPoint* item); - void clear(); - size_t size() const { return m_jumpPoints.size(); } - bool empty() const { return m_jumpPoints.empty(); } - - void setStartTie(Tie* startTie) { m_startTie = startTie; } - Tie* startTie() const { return m_startTie; } - - TieJumpPoint* findJumpPoint(const String& id); - void toggleJumpPoint(const String& id); - - void addTieToScore(TieJumpPoint* jumpPoint); - void undoRemoveTieFromScore(TieJumpPoint* jumpPoint); - - std::vector::iterator begin() { return m_jumpPoints.begin(); } - std::vector::const_iterator begin() const { return m_jumpPoints.begin(); } - std::vector::iterator end() { return m_jumpPoints.end(); } - std::vector::const_iterator end() const { return m_jumpPoints.end(); } - -private: - std::vector m_jumpPoints; - Tie* m_startTie = nullptr; -}; - +class TieJumpPoint; //--------------------------------------------------------- // @@ TieSegment /// a single segment of a tie @@ -189,9 +133,9 @@ class Tie : public SlurTie void setJumpPoint(TieJumpPoint* jumpPoint) { m_jumpPoint = jumpPoint; } void updateStartTieOnRemoval(); TieJumpPoint* jumpPoint() const { return m_jumpPoint; } - Tie* startTie() const { return startTieJumpPoints() ? startTieJumpPoints()->startTie() : nullptr; } + Tie* startTie() const; - static Tie* changeTieType(Tie* oldTie, Note* endNote = nullptr); + static void changeTieType(Tie* oldTie, Note* endNote = nullptr); protected: Tie(const ElementType& type, EngravingItem* parent = nullptr); @@ -201,6 +145,6 @@ class Tie : public SlurTie // Jump point information for incoming ties after repeats TieJumpPoint* m_jumpPoint = nullptr; - TieJumpPointList* startTieJumpPoints() const { return m_jumpPoint ? m_jumpPoint->jumpPointList() : nullptr; } + TieJumpPointList* startTieJumpPoints() const; }; } // namespace mu::engraving diff --git a/src/engraving/dom/tiejumppointlist.cpp b/src/engraving/dom/tiejumppointlist.cpp new file mode 100644 index 0000000000000..7ee4372d00cae --- /dev/null +++ b/src/engraving/dom/tiejumppointlist.cpp @@ -0,0 +1,288 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2025 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 "tiejumppointlist.h" + +#include "barline.h" +#include "chord.h" +#include "factory.h" +#include "marker.h" +#include "measure.h" +#include "note.h" +#include "partialtie.h" +#include "score.h" +#include "segment.h" +#include "tie.h" +#include "undo.h" +#include "volta.h" + +using namespace mu::engraving; + +//--------------------------------------------------------- +// TieJumpPoint +//--------------------------------------------------------- + +TieJumpPoint::TieJumpPoint(Note* note, bool active, int idx, bool followingNote) + : m_note(note), m_active(active), m_followingNote(followingNote) +{ + m_id = u"jumpPoint" + String::fromStdString(std::to_string(idx)); + if (active && endTie()) { + endTie()->setJumpPoint(this); + } +} + +Tie* TieJumpPoint::endTie() const +{ + return m_note ? m_note->tieBack() : nullptr; +} + +void TieJumpPoint::undoSetActive(bool v) +{ + Score* score = m_note ? m_note->score() : nullptr; + if (!score || m_active == v) { + return; + } + score->undo(new ChangeTieJumpPointActive(m_jumpPointList, m_id, v)); +} + +const String TieJumpPoint::menuTitle() const +{ + const Measure* measure = m_note->findMeasure(); + const int measureNo = measure ? measure->no() + 1 : 0; + + //: %1 represents the preceding jump item eg. coda. %2 represents the measure number + return muse::mtrc("engraving", "Tie to %1 (m. %2)").arg(precedingJumpItemName(), String::number(measureNo)); +} + +String TieJumpPoint::precedingJumpItemName() const +{ + const Chord* startChord = m_note->chord(); + const Segment* seg = startChord->segment(); + const Measure* measure = seg->measure(); + + if (seg->score()->firstSegment(SegmentType::ChordRest) == seg) { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "start of score", "partial tie menu"); + } + + // Markers + for (const EngravingItem* e : measure->el()) { + if (!e->isMarker()) { + continue; + } + + const Marker* marker = toMarker(e); + if (muse::contains(Marker::RIGHT_MARKERS, marker->markerType())) { + continue; + } + + if (marker->markerType() == MarkerType::CODA || marker->markerType() == MarkerType::VARCODA) { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "coda", "partial tie menu"); + } else { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "segno", "partial tie menu"); + } + } + + // Voltas + auto spanners = m_note->score()->spannerMap().findOverlapping(measure->tick().ticks(), measure->tick().ticks()); + for (auto& spanner : spanners) { + if (!spanner.value->isVolta() || Fraction::fromTicks(spanner.start) != startChord->tick()) { + continue; + } + + Volta* volta = toVolta(spanner.value); + + //: Used at %1 in the string "Tie to %1 (m. %2)". %1 in this string represents the volta's text set by the user + return muse::mtrc("engraving", "“%1” volta", "partial tie menu").arg(volta->beginText()); + } + + // Repeat barlines + if (measure->repeatStart()) { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "start repeat", "partial tie menu"); + } + + for (Segment* prevSeg = seg->prev(SegmentType::BarLineType); prevSeg && prevSeg->tick() == seg->tick(); + prevSeg = prevSeg->prev(SegmentType::BarLineType)) { + EngravingItem* el = prevSeg->element(startChord->track()); + if (!el || !el->isBarLine()) { + continue; + } + + BarLine* bl = toBarLine(el); + if (bl->barLineType() & (BarLineType::START_REPEAT | BarLineType::END_START_REPEAT)) { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "start repeat", "partial tie menu"); + } + } + + if (m_followingNote) { + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "next note", "partial tie menu"); + } + + //: Used at %1 in the string "Tie to %1 (m. %2)" + return muse::mtrc("engraving", "invalid", "partial tie menu"); +} + +//--------------------------------------------------------- +// TieJumpPointList +//--------------------------------------------------------- + +TieJumpPointList::~TieJumpPointList() +{ + muse::DeleteAll(m_jumpPoints); + m_jumpPoints.clear(); +} + +void TieJumpPointList::add(TieJumpPoint* item) +{ + item->setJumpPointList(this); + m_jumpPoints.push_back(item); +} + +void TieJumpPointList::clear() +{ + for (const TieJumpPoint* jumpPoint : m_jumpPoints) { + Tie* endTie = jumpPoint->endTie(); + if (!endTie) { + continue; + } + endTie->setJumpPoint(nullptr); + } + muse::DeleteAll(m_jumpPoints); + m_jumpPoints.clear(); +} + +Tie* TieJumpPointList::startTie() const +{ + return m_note ? m_note->tieFor() : nullptr; +} + +TieJumpPoint* TieJumpPointList::findJumpPoint(const String& id) +{ + for (TieJumpPoint* jumpPoint : m_jumpPoints) { + if (jumpPoint->id() != id) { + continue; + } + + return jumpPoint; + } + return nullptr; +} + +void TieJumpPointList::toggleJumpPoint(const String& id) +{ + TieJumpPoint* end = findJumpPoint(id); + + if (!end) { + LOGE() << "No partial tie end point found with id: " << id; + return; + } + + Score* score = end->note() ? end->note()->score() : nullptr; + if (!score) { + return; + } + + score->startCmd(TranslatableString("engraving", "Toggle partial tie")); + const bool checked = end->active(); + if (checked) { + undoRemoveTieFromScore(end); + } else { + undoAddTieToScore(end); + } + score->endCmd(); +} + +void TieJumpPointList::undoAddTieToScore(TieJumpPoint* jumpPoint) +{ + Note* note = jumpPoint->note(); + Score* score = note ? note->score() : nullptr; + Tie* tie = startTie(); + if (!tie || !score) { + return; + } + + if (jumpPoint->followingNote()) { + // Remove partial tie and add full tie + if (!tie->isPartialTie() || !toPartialTie(tie)->isOutgoing()) { + return; + } + jumpPoint->undoSetActive(true); + Tie::changeTieType(tie, note); + return; + } + + jumpPoint->undoSetActive(true); + + // Check if there is already a tie. If so, add partial tie info to it + Tie* tieBack = note->tieBack(); + if (tieBack && !tieBack->isPartialTie()) { + tieBack->setJumpPoint(jumpPoint); + return; + } + // Otherwise create incoming partial tie on note + PartialTie* pt = Factory::createPartialTie(note); + pt->setParent(note); + pt->setEndNote(note); + pt->setJumpPoint(jumpPoint); + score->undoAddElement(pt); +} + +void TieJumpPointList::undoRemoveTieFromScore(TieJumpPoint* jumpPoint) +{ + Note* note = jumpPoint->note(); + Score* score = note ? note->score() : nullptr; + Tie* tie = startTie(); + if (!tie || !score) { + return; + } + + if (jumpPoint->followingNote()) { + // Remove full tie and add partial tie + if (tie->isPartialTie()) { + return; + } + jumpPoint->undoSetActive(false); + + Tie::changeTieType(tie); + return; + } + + jumpPoint->undoSetActive(false); + + // Check if there is a full tie. If so, remove partial tie info from it + Tie* tieBack = note->tieBack(); + if (tieBack && !tieBack->isPartialTie()) { + tieBack->setJumpPoint(nullptr); + return; + } + // Otherwise remove incoming partial tie on note + PartialTie* pt = note->incomingPartialTie(); + if (!pt) { + return; + } + score->undoRemoveElement(pt); +} diff --git a/src/engraving/dom/tiejumppointlist.h b/src/engraving/dom/tiejumppointlist.h new file mode 100644 index 0000000000000..8b6147674f789 --- /dev/null +++ b/src/engraving/dom/tiejumppointlist.h @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2025 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 . + */ + +#pragma once +#include "global/types/string.h" + +namespace mu::engraving { +class Note; +class Tie; +class TieJumpPointList; + +// Items of TieJumpPointList +// Stores information about an endpoint +class TieJumpPoint +{ +public: + TieJumpPoint(Note* note, bool active, int idx, bool followingNote); + TieJumpPoint() {} + + Note* note() const { return m_note; } + Tie* endTie() const; + bool followingNote() const { return m_followingNote; } + const muse::String& id() const { return m_id; } + bool active() const { return m_active; } + void setActive(bool v) { m_active = v; } + void undoSetActive(bool v); + + // Stores the list that this end point belongs to. This is needed to find the start tie from an endpoint + void setJumpPointList(TieJumpPointList* jumpPointList) { m_jumpPointList = jumpPointList; } + TieJumpPointList* jumpPointList() { return m_jumpPointList; } + + const muse::String menuTitle() const; + +private: + muse::String precedingJumpItemName() const; + Note* m_note = nullptr; + bool m_active = false; + muse::String m_id; + TieJumpPointList* m_jumpPointList = nullptr; + bool m_followingNote = false; +}; + +// Created & deleted by Note +// Manages adding and removing ties from endpoints +class TieJumpPointList +{ +public: + TieJumpPointList(Note* note) + : m_note(note) {} + ~TieJumpPointList(); + + void add(TieJumpPoint* item); + void clear(); + size_t size() const { return m_jumpPoints.size(); } + bool empty() const { return m_jumpPoints.empty(); } + + Tie* startTie() const; + + TieJumpPoint* findJumpPoint(const muse::String& id); + void toggleJumpPoint(const muse::String& id); + + void undoAddTieToScore(TieJumpPoint* jumpPoint); + void undoRemoveTieFromScore(TieJumpPoint* jumpPoint); + + std::vector::iterator begin() { return m_jumpPoints.begin(); } + std::vector::const_iterator begin() const { return m_jumpPoints.begin(); } + std::vector::iterator end() { return m_jumpPoints.end(); } + std::vector::const_iterator end() const { return m_jumpPoints.end(); } + +private: + std::vector m_jumpPoints; + Note* m_note = nullptr; +}; +} diff --git a/src/engraving/rendering/score/horizontalspacing.cpp b/src/engraving/rendering/score/horizontalspacing.cpp index a8337935a7db6..c61ee9eeae588 100644 --- a/src/engraving/rendering/score/horizontalspacing.cpp +++ b/src/engraving/rendering/score/horizontalspacing.cpp @@ -1507,6 +1507,8 @@ void HorizontalSpacing::computeHangingLineWidth(const Segment* firstSeg, const S const Measure* crMeasure = crSeg->measure(); const size_t ntracks = crSeg->score()->ntracks(); + std::vector tieLengths; + for (track_idx_t track = 0; track < ntracks; track++) { // Hanging lines only occur at start or end of a measure const ChordRest* cr = incoming ? crMeasure->firstChordRest(track) : crMeasure->lastChordRest(track); @@ -1542,9 +1544,7 @@ void HorizontalSpacing::computeHangingLineWidth(const Segment* firstSeg, const S } double minLength = 0.0; - if (attachedLine->isTie()) { - minLength = style.styleMM(Sid::minHangingTieLength); - } else if (attachedLine->isGlissando()) { + if (attachedLine->isGlissando()) { bool straight = toGlissando(attachedLine)->glissandoType() == GlissandoType::STRAIGHT; minLength = straight ? style.styleMM(Sid::minStraightGlissandoLength) : style.styleMM(Sid::minWigglyGlissandoLength); @@ -1561,10 +1561,32 @@ void HorizontalSpacing::computeHangingLineWidth(const Segment* firstSeg, const S const double lineEndPointX = incoming ? lineNoteEndPos : lineSegEndPos; const double lineLength = lineEndPointX - lineStartPointX; + if (attachedLine->isTie()) { + tieLengths.push_back(lineLength); + continue; + } + if (lineLength < minLength) { width += minLength - lineLength; } } } } + + if (tieLengths.empty()) { + return; + } + + const double maxLength = *std::max_element(tieLengths.begin(), tieLengths.end()); + const double oldWidth = width; + for (double& tieLength : tieLengths) { + // Adjust for new width + tieLength += width - oldWidth; + const double minLength = muse::RealIsEqual(tieLength, maxLength) ? style.styleMM(Sid::minHangingTieLength) : style.styleMM( + Sid::minTieLength); + + if (tieLength < minLength) { + width += minLength - tieLength; + } + } } diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index 317ff8929d198..933fc5607fe07 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -538,7 +538,7 @@ const std::array StyleDef::styleValue styleDef(tieMidWidth, Spatium(.21)), styleDef(tieDottedWidth, Spatium(.10)), styleDef(minTieLength, Spatium(1.0)), - styleDef(minHangingTieLength, Spatium(1.0)), + styleDef(minHangingTieLength, Spatium(1.5)), styleDef(minStraightGlissandoLength, Spatium(1.2)), styleDef(minWigglyGlissandoLength, Spatium(2.0)), styleDef(slurMinDistance, Spatium(0.5)), diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/ContextMenuLoader.qml b/src/framework/uicomponents/qml/Muse/UiComponents/ContextMenuLoader.qml index d5a9b891abfd6..6633c1b116c60 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/ContextMenuLoader.qml +++ b/src/framework/uicomponents/qml/Muse/UiComponents/ContextMenuLoader.qml @@ -21,23 +21,12 @@ */ import QtQuick 2.15 -import Muse.Ui 1.0 - Item { id: container // Useful for static context menus property var items: [] - - property NavigationSection notationViewNavigationSection: null - property int navigationOrderStart: 0 - - property alias closeMenuOnSelection: contextMenuLoader.closeMenuOnSelection - property alias opensUpward: contextMenuLoader.opensUpward - property alias focusOnOpened: contextMenuLoader.focusOnOpened - property alias item: contextMenuLoader.item - signal handleMenuItem(string itemId) signal opened() signal closed() @@ -47,13 +36,6 @@ Item { width: 0 height: 0 - onNotationViewNavigationSectionChanged: function() { - contextMenuLoader.item.navigationSectionOverride = container.notationViewNavigationSection - } - onNavigationOrderStartChanged: function() { - contextMenuLoader.item.navigationOrderStart = container.navigationOrderStart - } - function show(position: point, items) { if (!items) { items = container.items diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml b/src/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml index 2b0d0fcf8ee20..1622e320e80bf 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml +++ b/src/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml @@ -21,7 +21,6 @@ */ import QtQuick 2.15 -import Muse.Ui 1.0 import Muse.UiComponents 1.0 import "internal" @@ -38,13 +37,6 @@ Loader { property StyledMenu menu: loader.item as StyledMenu property Item menuAnchorItem: null property bool hasSiblingMenus: false - property bool closeMenuOnSelection: true - property bool focusOnOpened: true - - property NavigationSection notationViewNavigationSection: null - property int navigationOrderStart: 0 - - property bool opensUpward: false property alias isMenuOpened: loader.active @@ -73,9 +65,7 @@ Loader { accessibleName: loader.accessibleName onHandleMenuItem: function(itemId) { - if (loader.closeMenuOnSelection) { - itemMenu.close() - } + itemMenu.close() Qt.callLater(loader.handleMenuItem, itemId) } @@ -92,9 +82,7 @@ Loader { } onOpened: { - if (focusOnOpened) { - focusOnOpenedMenuTimer.start() - } + focusOnOpenedMenuTimer.start() } } @@ -145,8 +133,6 @@ Loader { menu.closeSubMenu() - menu.setOpensUpward(loader.opensUpward) - if (x !== -1) { menu.x = x } diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml b/src/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml index cb2b4c8fb5bdc..6262704ad7365 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml +++ b/src/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml @@ -32,9 +32,6 @@ MenuView { property alias model: view.model - property NavigationSection navigationSectionOverride: null - property int navigationOrderStart: 0 - property int preferredAlign: Qt.AlignRight // Left, HCenter, Right property bool hasSiblingMenus: loader.hasSiblingMenus @@ -137,9 +134,9 @@ MenuView { property NavigationPanel navigationPanel: NavigationPanel { name: "StyledMenu" + section: content.navigationSection direction: NavigationPanel.Vertical - section: root.navigationSectionOverride ?? content.navigationSection - order: root.navigationOrderStart + order: 1 accessible.name: root.accessibleName diff --git a/src/notation/notationscene.qrc b/src/notation/notationscene.qrc index cf0e035766a58..4f7b108f17775 100644 --- a/src/notation/notationscene.qrc +++ b/src/notation/notationscene.qrc @@ -76,5 +76,6 @@ qml/MuseScore/NotationScene/internal/EditStyle/timeSigImages/timesig-on_all_staves.png qml/MuseScore/NotationScene/internal/EditStyle/timeSigImages/timesig-courtesy-create_space.png qml/MuseScore/NotationScene/internal/EditStyle/StyledImage.qml + qml/MuseScore/NotationScene/internal/PartialTieMenuRowItem.qml diff --git a/src/notation/qml/MuseScore/NotationScene/internal/CapoPopup.qml b/src/notation/qml/MuseScore/NotationScene/internal/CapoPopup.qml index b8bcea7a0b52e..ee9d27f11fdfc 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/CapoPopup.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/CapoPopup.qml @@ -33,8 +33,6 @@ StyledPopupView { property alias navigationOrderStart: capoSettingsNavPanel.order readonly property alias navigationOrderEnd: capoSettingsNavPanel.order - property QtObject model: capoModel - contentWidth: content.width contentHeight: content.height diff --git a/src/notation/qml/MuseScore/NotationScene/internal/HarpPedalPopup.qml b/src/notation/qml/MuseScore/NotationScene/internal/HarpPedalPopup.qml index dd8d398606fa7..1660a59fa69fe 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/HarpPedalPopup.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/HarpPedalPopup.qml @@ -29,9 +29,6 @@ import MuseScore.NotationScene 1.0 StyledPopupView { id: root - - property QtObject model: harpModel - property variant pedalState: harpModel.pedalState property alias notationViewNavigationSection: pedalSettingsNavPanel.section @@ -279,7 +276,7 @@ StyledPopupView { Layout.leftMargin: 30 Layout.fillWidth: true - checked: model.isDiagram + checked: harpModel.isDiagram text: qsTrc("notation", "Diagram") navigation.name: "diagramButton" @@ -288,7 +285,7 @@ StyledPopupView { navigation.accessible.name: qsTrc("notation", "Diagram") onToggled: { - model.setIsDiagram(true) + harpModel.setIsDiagram(true) } } @@ -302,7 +299,7 @@ StyledPopupView { Layout.bottomMargin: 15 Layout.fillWidth: true - checked: !model.isDiagram + checked: !harpModel.isDiagram text: qsTrc("notation", "Text") navigation.name: "textButton" @@ -311,7 +308,7 @@ StyledPopupView { navigation.accessible.name: qsTrc("notation", "Text") onToggled: { - model.setIsDiagram(false) + harpModel.setIsDiagram(false) } } } diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PartialTieMenuRowItem.qml b/src/notation/qml/MuseScore/NotationScene/internal/PartialTieMenuRowItem.qml new file mode 100644 index 0000000000000..fd92b3044a7cb --- /dev/null +++ b/src/notation/qml/MuseScore/NotationScene/internal/PartialTieMenuRowItem.qml @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2025 MuseScore BVBA and others + * + * 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 . + */ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 + +import Muse.Ui 1.0 +import Muse.UiComponents 1.0 +import MuseScore.NotationScene 1.0 + +ListItemBlank { + function calculateWidth() { + let result = 0 + + result += rowLayout.anchors.leftMargin + + result += Math.ceil(checkIcon.Layout.preferredWidth) + result += rowLayout.spacing + + result += Math.ceil(titleLabel.implicitWidth) + result += rowLayout.spacing + result += rowLayout.anchors.rightMargin + + return result + } + + id: rowDelegate + + required property var modelData + required property var model + + RowLayout { + id: rowLayout + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + + spacing: 12 + + StyledIconLabel { + id: checkIcon + Layout.alignment: Qt.AlignLeft + Layout.preferredWidth: 16 + iconCode: rowDelegate.modelData.checked ? IconCode.TICK_RIGHT_ANGLE : IconCode.NONE + } + + StyledTextLabel { + id: titleLabel + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + + text: rowDelegate.modelData.title + + textFormat: Text.RichText + } + } +} diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PartialTiePopup.qml b/src/notation/qml/MuseScore/NotationScene/internal/PartialTiePopup.qml index bc9dc8944e6bd..23623f2cade40 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/PartialTiePopup.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/PartialTiePopup.qml @@ -5,7 +5,7 @@ * MuseScore * Music Composition & Notation * - * Copyright (C) 2022 MuseScore BVBA and others + * Copyright (C) 2025 MuseScore BVBA and others * * 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 @@ -32,20 +32,16 @@ StyledPopupView { id: root margins: 0 - property alias notationViewNavigationSection: partialTieNavPanel.section - property alias navigationOrderStart: partialTieNavPanel.order - readonly property alias navigationOrderEnd: partialTieNavPanel.order + contentWidth: content.width + contentHeight: content.height - property QtObject model: partialTiePopupModel + property NavigationSection notationViewNavigationSection: null + property int navigationOrderStart: 0 + property int navigationOrderEnd: tieMenuList.navigation.order showArrow: false - onOpened: { - tieMenuLoader.show(Qt.point(0, 0)) - } - onClosed: { - tieMenuLoader.close() partialTiePopupModel.onClosed() } @@ -53,26 +49,23 @@ StyledPopupView { function updatePosition() { const opensUp = partialTiePopupModel.tieDirection - const popupHeight = tieMenuLoader.item.height + root.margins * 2 + root.padding * 2 + const popupHeight = content.height + root.margins * 2 + root.padding * 2 root.x = partialTiePopupModel.dialogPosition.x - root.parent.x root.y = partialTiePopupModel.dialogPosition.y - root.parent.y - (opensUp ? popupHeight : 0) root.setOpensUpward(opensUp) - } - - contentWidth: tieMenuLoader.width - contentHeight: tieMenuLoader.childrenRect.height - ContextMenuLoader { - id: tieMenuLoader - closeMenuOnSelection: false - focusOnOpened: false - opensUpward: root.opensUpward + tieMenuList.calculateWidth() + } - items: partialTiePopupModel.items + Component.onCompleted: { + partialTiePopupModel.init() + } - onHandleMenuItem: function(itemId) { - partialTiePopupModel.toggleItemChecked(itemId) - } + Column { + id: content + width: tieMenuList.width + height: tieMenuList.height + spacing: 0 PartialTiePopupModel { id: partialTiePopupModel @@ -82,26 +75,53 @@ StyledPopupView { } onItemsChanged: function() { - tieMenuLoader.show(Qt.point(0, 0)) + tieMenuList.model = partialTiePopupModel.items } } - Component.onCompleted: { - partialTiePopupModel.init() - } + StyledListView { + id: tieMenuList - NavigationPanel { - id: partialTieNavPanel - name: "PartialTieMenu" - direction: NavigationPanel.Vertical - accessible.name: qsTrc("notation", "Partial tie menu items") + property int itemHeight: 32 - onSectionChanged: function() { - tieMenuLoader.notationViewNavigationSection = section + implicitWidth: contentItem.childrenRect.width + height: itemHeight * count + + spacing: 0 + arrowControlsAvailable: false + + model: partialTiePopupModel.items + + navigation.section: notationViewNavigationSection + navigation.order: navigationOrderStart + + visible: true + + function calculateWidth() { + var result = 0 + for (var item in tieMenuList.contentItem.children) { + var row = tieMenuList.contentItem.children[item]; + if (!(row instanceof ListItemBlank)) { + continue + } + result = Math.max(result, tieMenuList.contentItem.children[item].calculateWidth()) + } + + tieMenuList.width = result } - onOrderChanged: function() { - tieMenuLoader.navigationOrderStart = order + delegate: PartialTieMenuRowItem { + implicitHeight: tieMenuList.itemHeight + hoverHitColor: ui.theme.accentColor + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + + navigation.panel: tieMenuList.navigation + navigation.row: model.index + + onClicked: { + partialTiePopupModel.toggleItemChecked(modelData.id) + } } } } diff --git a/src/notation/qml/MuseScore/NotationScene/internal/StringTuningsPopup.qml b/src/notation/qml/MuseScore/NotationScene/internal/StringTuningsPopup.qml index eb7689150d103..f04b2b30eefeb 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/StringTuningsPopup.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/StringTuningsPopup.qml @@ -33,9 +33,6 @@ StyledPopupView { property alias navigationOrderStart: navPanel.order readonly property alias navigationOrderEnd: navPanel.order - property QtObject model: stringTuningsModel - - contentWidth: content.width contentHeight: content.height diff --git a/src/playback/qml/MuseScore/Playback/SoundFlagPopup.qml b/src/playback/qml/MuseScore/Playback/SoundFlagPopup.qml index 6fee516b39f90..502edeb8b175c 100644 --- a/src/playback/qml/MuseScore/Playback/SoundFlagPopup.qml +++ b/src/playback/qml/MuseScore/Playback/SoundFlagPopup.qml @@ -36,8 +36,6 @@ StyledPopupView { property alias navigationOrderStart: navPanel.order readonly property alias navigationOrderEnd: museSoundsParams.navigationPanelOrderEnd - property QtObject model: soundFlagModel - contentWidth: content.width contentHeight: content.childrenRect.height onContentHeightChanged: {