diff --git a/src/engraving/api/v1/score.cpp b/src/engraving/api/v1/score.cpp index 506e365f4ef57..92086ae775847 100644 --- a/src/engraving/api/v1/score.cpp +++ b/src/engraving/api/v1/score.cpp @@ -262,6 +262,16 @@ QQmlListProperty Score::systems() return wrapContainerProperty(this, score()->systems()); } +//--------------------------------------------------------- +// Score::spanners +//--------------------------------------------------------- + +QQmlListProperty Score::spanners() +{ + m_cachedSpannerList = score()->spannerList(); + return wrapContainerProperty(this, m_cachedSpannerList); +} + //--------------------------------------------------------- // Score::startCmd //--------------------------------------------------------- diff --git a/src/engraving/api/v1/score.h b/src/engraving/api/v1/score.h index 486d686e0925b..e16599417479a 100644 --- a/src/engraving/api/v1/score.h +++ b/src/engraving/api/v1/score.h @@ -165,6 +165,9 @@ class Score : public apiv1::ScoreElement, public muse::Injectable /// List of systems in this score. /// \since MuseScore 4.6 Q_PROPERTY(QQmlListProperty systems READ systems) + /// List of spanners (hairpins, slurs, etc.) in this score. + /// \since MuseScore 4.7 + Q_PROPERTY(QQmlListProperty spanners READ spanners) muse::Inject context = { this }; @@ -345,6 +348,7 @@ class Score : public apiv1::ScoreElement, public muse::Injectable QQmlListProperty staves(); QQmlListProperty pages(); QQmlListProperty systems(); + QQmlListProperty spanners(); static const mu::engraving::InstrumentTemplate* instrTemplateFromName(const QString& name); // used by PluginAPI::newScore() /// \endcond @@ -352,5 +356,8 @@ class Score : public apiv1::ScoreElement, public muse::Injectable private: mu::notation::INotationPtr notation() const; mu::notation::INotationUndoStackPtr undoStack() const; + + // Cache for spanner list to provide stable reference for QML property + mutable std::vector m_cachedSpannerList; }; } diff --git a/src/engraving/dom/score.cpp b/src/engraving/dom/score.cpp index a2d496ce698d8..6ea43ef81f299 100644 --- a/src/engraving/dom/score.cpp +++ b/src/engraving/dom/score.cpp @@ -4496,6 +4496,20 @@ void Score::addSpanner(Spanner* s, bool computeStartEnd) } } +//--------------------------------------------------------- +// spannerList +//--------------------------------------------------------- + +std::vector Score::spannerList() const +{ + std::vector result; + const std::multimap& spannerMap = m_spanner.map(); + for (auto it = spannerMap.begin(); it != spannerMap.end(); ++it) { + result.push_back(it->second); + } + return result; +} + //--------------------------------------------------------- // removeSpanner //--------------------------------------------------------- diff --git a/src/engraving/dom/score.h b/src/engraving/dom/score.h index 105c6c205c0df..94e06f745aaf1 100644 --- a/src/engraving/dom/score.h +++ b/src/engraving/dom/score.h @@ -964,6 +964,7 @@ class Score : public EngravingObject, public muse::Injectable const std::multimap& spanner() const { return m_spanner.map(); } SpannerMap& spannerMap() { return m_spanner; } const SpannerMap& spannerMap() const { return m_spanner; } + std::vector spannerList() const; // Return all spanners as a vector for Plugin API bool isSpannerStartEnd(const Fraction& tick, track_idx_t track) const; void removeSpanner(Spanner*); void addSpanner(Spanner*, bool computeStartEnd = true); diff --git a/src/engraving/tests/spanners_tests.cpp b/src/engraving/tests/spanners_tests.cpp index c00a1eca85e89..29fffab2735c0 100644 --- a/src/engraving/tests/spanners_tests.cpp +++ b/src/engraving/tests/spanners_tests.cpp @@ -36,6 +36,9 @@ #include "engraving/dom/system.h" #include "engraving/editing/editexcerpt.h" +#include "engraving/api/v1/score.h" +#include "engraving/api/v1/elements.h" + #include "utils/scorerw.h" #include "utils/scorecomp.h" @@ -593,3 +596,55 @@ TEST_F(Engraving_SpannersTests, spanners16) EXPECT_TRUE(ScoreComp::saveCompareScore(score, u"smallstaff01.mscx", SPANNERS_DATA_DIR + u"smallstaff01-ref.mscx")); delete score; } + +//--------------------------------------------------------- +/// spanners17 +/// Test Plugin API score.spanners property +/// Verify that score.spanners exposes all spanners in the score +//--------------------------------------------------------- + +TEST_F(Engraving_SpannersTests, spanners17_pluginAPI_scoreSpanners) +{ + // Load a score file + MasterScore* score = ScoreRW::readScore(SPANNERS_DATA_DIR + u"glissando01.mscx"); + EXPECT_TRUE(score); + + // Create Plugin API wrapper for the score + apiv1::Score apiScore(score); + + // Get spanners using Plugin API + QQmlListProperty scoreSpanners = apiScore.spanners(); + + // Basic sanity checks: the property should return a valid list + EXPECT_NE(scoreSpanners.count, nullptr); + EXPECT_NE(scoreSpanners.at, nullptr); + + int spannerCount = scoreSpanners.count(&scoreSpanners); + EXPECT_GE(spannerCount, 0) << "Count should be non-negative"; + + // Get spanners directly from the score for comparison + auto domSpanners = score->spannerList(); + + // The Plugin API should expose the same number of spanners + EXPECT_EQ(spannerCount, (int)domSpanners.size()) + << "Plugin API should expose all spanners from the score"; + + // Verify each spanner can be accessed and has valid properties + for (int i = 0; i < spannerCount; i++) { + auto* item = scoreSpanners.at(&scoreSpanners, i); + apiv1::EngravingItem* apiItem = qobject_cast(item); + EXPECT_TRUE(apiItem != nullptr) << "Spanner " << i << " should be a valid EngravingItem"; + + if (apiItem && apiItem->element()) { + // Verify it's actually a spanner + mu::engraving::EngravingItem* elem = apiItem->element(); + EXPECT_TRUE(elem->isSpanner()) << "Item " << i << " should be a Spanner"; + + // Verify we can access the track property (spanners have tracks) + track_idx_t track = elem->track(); + EXPECT_GE(track, 0) << "Spanner " << i << " should have a valid track"; + } + } + + delete score; +}