From ac9e61afcf8ec62152d5a8725f19b46d2e657298 Mon Sep 17 00:00:00 2001 From: mnorrisvuw Date: Sun, 13 Sep 2026 19:50:37 +1200 Subject: [PATCH] Recognize pasted metronome markings and follow their tempo (#28667) --- src/engraving/dom/tempotext.cpp | 47 +++++++++++++++++++++++------- src/engraving/dom/tempotext.h | 2 ++ src/engraving/editing/textedit.cpp | 12 ++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/engraving/dom/tempotext.cpp b/src/engraving/dom/tempotext.cpp index 9301b54c0b0a8..c16f3a8470088 100644 --- a/src/engraving/dom/tempotext.cpp +++ b/src/engraving/dom/tempotext.cpp @@ -248,12 +248,12 @@ void TempoText::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags // updateTempo //--------------------------------------------------------- -void TempoText::updateTempo() +/** Normalizes supported spacing and equivalence notation before matching tempo text. */ +static String normalizedTempoText(String s) { - // cache regexp, they are costly to create - static std::unordered_map regexps; - static std::unordered_map regexps2; - String s = plainText(); + // UTF-8 regex whitespace does not match these spaces used in metronome markings. + s.replace(u"\u00a0", u" "); + s.replace(u"\u2009", u" "); s.replace(u",", u"."); s.replace(u"space", u" "); s.replace(u"≒", u"="); @@ -262,15 +262,40 @@ void TempoText::updateTempo() s.replace(u"ca.", u""); s.replace(u"c.", u""); s.replace(u"approx.", u""); + return s; +} + +/** Returns the cached numeric metronome expression for a note-duration pattern. */ +static const std::regex& metronomeRegex(const String& pattern) +{ + static std::unordered_map regexps; + auto it = regexps.find(pattern); + if (it == regexps.end()) { + it = regexps.emplace(pattern, std::regex(String(u"%1\\s*=\\s*(\\d+[.]{0,1}\\d*)\\s*").arg(pattern).toStdString())).first; + } + return it->second; +} + +/** Checks plain text for an explicit numeric metronome marking without changing score state. */ +bool TempoText::isMetronomeMark(const String& text) +{ + const std::string normalized = normalizedTempoText(text).toStdString(); + for (const TempoPattern& pattern : tp) { + if (std::regex_search(normalized, metronomeRegex(String::fromUtf8(pattern.pattern)))) { + return true; + } + } + return false; +} + +void TempoText::updateTempo() +{ + static std::unordered_map regexps2; + const String s = normalizedTempoText(plainText()); std::string su8 = s.toStdString(); for (const TempoPattern& pa : tp) { String pattern = String::fromUtf8(pa.pattern); - std::regex re; - if (!muse::contains(regexps, String::fromUtf8(pa.pattern))) { - re = std::regex(String(u"%1\\s*=\\s*(\\d+[.]{0,1}\\d*)\\s*").arg(pattern).toStdString()); - regexps[pattern] = re; - } - re = muse::value(regexps, pattern); + const std::regex& re = metronomeRegex(pattern); std::smatch match; std::regex_search(su8, match, re); if (!match.empty()) { diff --git a/src/engraving/dom/tempotext.h b/src/engraving/dom/tempotext.h index b6711db04eb94..650ca983686ae 100644 --- a/src/engraving/dom/tempotext.h +++ b/src/engraving/dom/tempotext.h @@ -81,6 +81,8 @@ class TempoText final : public TextBase void setFollowText(bool v) { m_followText = v; } void updateTempo(); + /** Checks plain text for an explicit numeric metronome marking. */ + static bool isMetronomeMark(const String& text); TDuration duration() const; diff --git a/src/engraving/editing/textedit.cpp b/src/engraving/editing/textedit.cpp index c8e95c6eb38a7..42352a84c8473 100644 --- a/src/engraving/editing/textedit.cpp +++ b/src/engraving/editing/textedit.cpp @@ -33,6 +33,7 @@ #include "../dom/lyrics.h" #include "../dom/score.h" #include "../dom/symbol.h" +#include "../dom/tempotext.h" #include "../dom/utils.h" #include "../rendering/iscorerenderer.h" @@ -993,6 +994,17 @@ void TextBase::paste(const String& txt) insertText(u"&"); insertText(token); } + // Update playback in the paste transaction rather than waiting for text editing to end. + if (isTempoText()) { + TempoText* tempo = toTempoText(this); + TempoText pastedTempo(tempo->segment()); + pastedTempo.setXmlText(txt); + if (!tempo->followText() && TempoText::isMetronomeMark(pastedTempo.plainText())) { + undoChangeProperty(Pid::TEMPO_FOLLOW_TEXT, true); + } else if (tempo->followText()) { + tempo->updateTempo(); + } + } score()->endCmd(); }