diff --git a/src/app/configs/data/shortcuts.xml b/src/app/configs/data/shortcuts.xml
index 468b5626f2b4b..bbd92a0bb82d6 100644
--- a/src/app/configs/data/shortcuts.xml
+++ b/src/app/configs/data/shortcuts.xml
@@ -898,6 +898,10 @@
toggle-piano-keyboard
P
+
+ toggle-percussion-panel
+ O
+
next-score
20
diff --git a/src/app/configs/data/shortcuts_azerty.xml b/src/app/configs/data/shortcuts_azerty.xml
index 584e89952811d..ceb50391e2d32 100644
--- a/src/app/configs/data/shortcuts_azerty.xml
+++ b/src/app/configs/data/shortcuts_azerty.xml
@@ -924,6 +924,10 @@
toggle-piano-keyboard
P
+
+ toggle-percussion-panel
+ O
+
next-score
20
diff --git a/src/app/configs/data/shortcuts_mac.xml b/src/app/configs/data/shortcuts_mac.xml
index f532e63b10270..d0d4b28a6fe44 100644
--- a/src/app/configs/data/shortcuts_mac.xml
+++ b/src/app/configs/data/shortcuts_mac.xml
@@ -899,6 +899,10 @@
toggle-piano-keyboard
P
+
+ toggle-percussion-panel
+ O
+
next-score
20
diff --git a/src/appshell/internal/applicationuiactions.cpp b/src/appshell/internal/applicationuiactions.cpp
index 32ce297765d76..bdfd79693a537 100644
--- a/src/appshell/internal/applicationuiactions.cpp
+++ b/src/appshell/internal/applicationuiactions.cpp
@@ -39,6 +39,7 @@ using namespace muse::dock;
static const ActionCode FULL_SCREEN_CODE("fullscreen");
static const ActionCode TOGGLE_NAVIGATOR_ACTION_CODE("toggle-navigator");
static const ActionCode TOGGLE_BRAILLE_ACTION_CODE("toggle-braille-panel");
+static const ActionCode TOGGLE_PERCUSSION_PANEL_ACTION_CODE("toggle-percussion-panel");
const UiActionList ApplicationUiActions::m_actions = {
UiAction("quit",
@@ -194,14 +195,13 @@ const UiActionList ApplicationUiActions::m_actions = {
TranslatableString("action", "Show/hide piano keyboard"),
Checkable::Yes
),
- // still in development
- // UiAction("toggle-percussion-panel",
- // mu::context::UiCtxNotationOpened,
- // mu::context::CTX_ANY,
- // TranslatableString("action", "Percussion"),
- // TranslatableString("action", "Show/hide percussion panel"),
- // Checkable::Yes
- // ),
+ UiAction(TOGGLE_PERCUSSION_PANEL_ACTION_CODE,
+ mu::context::UiCtxProjectOpened,
+ mu::context::CTX_NOTATION_OPENED,
+ TranslatableString("action", "Percussion"),
+ TranslatableString("action", "Show/hide percussion panel"),
+ Checkable::Yes
+ ),
UiAction("toggle-scorecmp-tool",
mu::context::UiCtxProjectOpened,
mu::context::CTX_NOTATION_OPENED,
@@ -248,6 +248,10 @@ void ApplicationUiActions::init()
dockWindowProvider()->windowChanged().onNotify(this, [this]() {
listenOpenedDocksChanged(dockWindowProvider()->window());
});
+
+ notationConfiguration()->useNewPercussionPanelChanged().onNotify(this, [this]() {
+ m_actionEnabledChanged.send({ TOGGLE_PERCUSSION_PANEL_ACTION_CODE });
+ });
}
void ApplicationUiActions::listenOpenedDocksChanged(IDockWindow* window)
@@ -280,11 +284,11 @@ const muse::ui::UiActionList& ApplicationUiActions::actionsList() const
bool ApplicationUiActions::actionEnabled(const UiAction& act) const
{
- if (!m_controller->canReceiveAction(act.code)) {
- return false;
+ if (act.code == TOGGLE_PERCUSSION_PANEL_ACTION_CODE) {
+ return notationConfiguration()->useNewPercussionPanel();
}
- return true;
+ return m_controller->canReceiveAction(act.code);
}
bool ApplicationUiActions::actionChecked(const UiAction& act) const
@@ -340,7 +344,7 @@ const QMap& ApplicationUiActions::toggleDockActions()
{ "toggle-timeline", TIMELINE_PANEL_NAME },
{ "toggle-mixer", MIXER_PANEL_NAME },
{ "toggle-piano-keyboard", PIANO_KEYBOARD_PANEL_NAME },
- { "toggle-percussion-panel", PERCUSSION_PANEL_NAME },
+ { TOGGLE_PERCUSSION_PANEL_ACTION_CODE, PERCUSSION_PANEL_NAME },
{ "toggle-statusbar", NOTATION_STATUSBAR_NAME },
};
diff --git a/src/appshell/internal/applicationuiactions.h b/src/appshell/internal/applicationuiactions.h
index 396c47638bb5c..2fb747d6aa491 100644
--- a/src/appshell/internal/applicationuiactions.h
+++ b/src/appshell/internal/applicationuiactions.h
@@ -29,6 +29,7 @@
#include "async/asyncable.h"
#include "ui/imainwindow.h"
#include "view/preferences/braillepreferencesmodel.h"
+#include "notation/inotationconfiguration.h"
#include "dockwindow/idockwindowprovider.h"
@@ -39,6 +40,7 @@ class ApplicationUiActions : public muse::ui::IUiActionsModule, public muse::Inj
muse::Inject dockWindowProvider = { this };
muse::Inject configuration = { this };
muse::Inject brailleConfiguration = { this };
+ muse::Inject notationConfiguration = { this };
public:
ApplicationUiActions(std::shared_ptr controller, const muse::modularity::ContextPtr& iocCtx);
diff --git a/src/appshell/view/appmenumodel.cpp b/src/appshell/view/appmenumodel.cpp
index 79f3501d371fa..b8ab70ebd2383 100644
--- a/src/appshell/view/appmenumodel.cpp
+++ b/src/appshell/view/appmenumodel.cpp
@@ -260,7 +260,7 @@ MenuItem* AppMenuModel::makeViewMenu()
makeMenuItem("toggle-timeline"),
makeMenuItem("toggle-mixer"),
makeMenuItem("toggle-piano-keyboard"),
- // makeMenuItem("toggle-percussion-panel"), // still in development
+ makeMenuItem("toggle-percussion-panel"),
makeMenuItem("playback-setup"),
//makeMenuItem("toggle-scorecmp-tool"), // not implemented
makeSeparator(),
diff --git a/src/engraving/dom/instrchange.cpp b/src/engraving/dom/instrchange.cpp
index 2f908eaf4ef0c..f324a47fb91d7 100644
--- a/src/engraving/dom/instrchange.cpp
+++ b/src/engraving/dom/instrchange.cpp
@@ -86,70 +86,72 @@ void InstrumentChange::setInstrument(const Instrument& i)
void InstrumentChange::setupInstrument(const Instrument* instrument)
{
- if (m_init) {
- Fraction tickStart = segment()->tick();
- Part* part = staff()->part();
- Interval oldV = part->instrument(tickStart)->transpose();
- Interval oldKv = staff()->transpose(tickStart);
- Interval v = instrument->transpose();
- bool concPitch = style().styleB(Sid::concertPitch);
-
- // change the clef for each staff
- for (size_t i = 0; i < part->nstaves(); i++) {
- ClefType oldClefType = concPitch ? part->instrument(tickStart)->clefType(i).concertClef
- : part->instrument(tickStart)->clefType(i).transposingClef;
- ClefType newClefType = concPitch ? instrument->clefType(i).concertClef
- : instrument->clefType(i).transposingClef;
- // Introduce cleff change only if the new clef *symbol* is different from the old one
- if (ClefInfo::symId(oldClefType) != ClefInfo::symId(newClefType)) {
- // If instrument change is at the start of a measure, use the measure as the element, as this will place the instrument change before the barline.
- EngravingItem* element = rtick().isZero() ? toEngravingItem(findMeasure()) : toEngravingItem(this);
- score()->undoChangeClef(part->staff(i), element, newClefType, true);
- }
+ if (!m_init) {
+ return;
+ }
+
+ Fraction tickStart = segment()->tick();
+ Part* part = staff()->part();
+ Interval oldV = part->instrument(tickStart)->transpose();
+ Interval oldKv = staff()->transpose(tickStart);
+ Interval v = instrument->transpose();
+ bool concPitch = style().styleB(Sid::concertPitch);
+
+ // change the clef for each staff
+ for (size_t i = 0; i < part->nstaves(); i++) {
+ ClefType oldClefType = concPitch ? part->instrument(tickStart)->clefType(i).concertClef
+ : part->instrument(tickStart)->clefType(i).transposingClef;
+ ClefType newClefType = concPitch ? instrument->clefType(i).concertClef
+ : instrument->clefType(i).transposingClef;
+ // Introduce cleff change only if the new clef *symbol* is different from the old one
+ if (ClefInfo::symId(oldClefType) != ClefInfo::symId(newClefType)) {
+ // If instrument change is at the start of a measure, use the measure as the element, as this will place the instrument change before the barline.
+ EngravingItem* element = rtick().isZero() ? toEngravingItem(findMeasure()) : toEngravingItem(this);
+ score()->undoChangeClef(part->staff(i), element, newClefType, true);
}
+ }
- // Change key signature if necessary. CAUTION: not necessary in case of octave-transposing!
- if ((v.chromatic - oldV.chromatic) % 12) {
- for (size_t i = 0; i < part->nstaves(); i++) {
- if (!part->staff(i)->keySigEvent(tickStart).isAtonal()) {
- KeySigEvent ks;
- // Check, if some key signature is already there, if no, mark new one "for instrument change"
- Segment* seg = segment()->prev1(SegmentType::KeySig);
- voice_idx_t voice = part->staff(i)->idx() * VOICES;
- KeySig* ksig = seg ? toKeySig(seg->element(voice)) : nullptr;
- bool forInstChange = !(ksig && ksig->tick() == tickStart && !ksig->generated());
- ks.setForInstrumentChange(forInstChange);
- Key cKey = part->staff(i)->concertKey(tickStart);
- ks.setConcertKey(cKey);
- score()->undoChangeKeySig(part->staff(i), tickStart, ks);
- }
+ // Change key signature if necessary. CAUTION: not necessary in case of octave-transposing!
+ if ((v.chromatic - oldV.chromatic) % 12) {
+ for (size_t i = 0; i < part->nstaves(); i++) {
+ if (!part->staff(i)->keySigEvent(tickStart).isAtonal()) {
+ KeySigEvent ks;
+ // Check, if some key signature is already there, if no, mark new one "for instrument change"
+ Segment* seg = segment()->prev1(SegmentType::KeySig);
+ voice_idx_t voice = part->staff(i)->idx() * VOICES;
+ KeySig* ksig = seg ? toKeySig(seg->element(voice)) : nullptr;
+ bool forInstChange = !(ksig && ksig->tick() == tickStart && !ksig->generated());
+ ks.setForInstrumentChange(forInstChange);
+ Key cKey = part->staff(i)->concertKey(tickStart);
+ ks.setConcertKey(cKey);
+ score()->undoChangeKeySig(part->staff(i), tickStart, ks);
}
}
+ }
- // change instrument in all linked scores
- for (EngravingObject* se : linkList()) {
- InstrumentChange* lic = static_cast(se);
- Instrument* newInstrument = new Instrument(*instrument);
- lic->score()->undo(new ChangeInstrument(lic, newInstrument));
- }
+ // change instrument in all linked scores
+ for (EngravingObject* se : linkList()) {
+ InstrumentChange* lic = static_cast(se);
+ Instrument* newInstrument = new Instrument(*instrument);
+ lic->score()->undo(new ChangeInstrument(lic, newInstrument));
+ }
- // transpose for current score only
- // this automatically propagates to linked scores
- if (part->instrument(tickStart)->transpose() != oldV) {
- auto i = part->instruments().upper_bound(tickStart.ticks()); // find(), ++i
- Fraction tickEnd;
- if (i == part->instruments().end()) {
- tickEnd = Fraction(-1, 1);
- } else {
- tickEnd = Fraction::fromTicks(i->first);
- }
- score()->transpositionChanged(part, oldKv, tickStart, tickEnd);
+ // transpose for current score only
+ // this automatically propagates to linked scores
+ if (part->instrument(tickStart)->transpose() != oldV) {
+ auto i = part->instruments().upper_bound(tickStart.ticks()); // find(), ++i
+ Fraction tickEnd;
+ if (i == part->instruments().end()) {
+ tickEnd = Fraction(-1, 1);
+ } else {
+ tickEnd = Fraction::fromTicks(i->first);
}
-
- //: The text of an "instrument change" marking. It is an instruction to the player to switch to another instrument.
- const String newInstrChangeText = muse::mtrc("engraving", "To %1").arg(instrument->trackName());
- undoChangeProperty(Pid::TEXT, TextBase::plainToXmlText(newInstrChangeText));
+ score()->transpositionChanged(part, oldKv, tickStart, tickEnd);
}
+
+ //: The text of an "instrument change" marking. It is an instruction to the player to switch to another instrument.
+ const String newInstrChangeText = muse::mtrc("engraving", "To %1").arg(instrument->trackName());
+ undoChangeProperty(Pid::TEXT, TextBase::plainToXmlText(newInstrChangeText));
}
//---------------------------------------------------------
diff --git a/src/framework/accessibility/iaccessible.h b/src/framework/accessibility/iaccessible.h
index 237edf5f942a0..eb80503b30a87 100644
--- a/src/framework/accessibility/iaccessible.h
+++ b/src/framework/accessibility/iaccessible.h
@@ -48,6 +48,7 @@ class IAccessible
Panel,
StaticText,
EditableText,
+ SilentRole, // avoids reading "button", "text", etc. after item name
Button,
CheckBox,
RadioButton,
diff --git a/src/framework/accessibility/internal/accessibleiteminterface.cpp b/src/framework/accessibility/internal/accessibleiteminterface.cpp
index 9e9343538cbda..3ded1115684f5 100644
--- a/src/framework/accessibility/internal/accessibleiteminterface.cpp
+++ b/src/framework/accessibility/internal/accessibleiteminterface.cpp
@@ -156,6 +156,10 @@ QAccessible::State AccessibleItemInterface::state() const
state.focusable = true;
state.focused = item->accessibleState(IAccessible::State::Focused);
} break;
+ case IAccessible::Role::SilentRole: {
+ state.focusable = true;
+ state.focused = item->accessibleState(IAccessible::State::Focused);
+ } break;
case IAccessible::Role::List: {
state.active = item->accessibleState(IAccessible::State::Active);
} break;
@@ -217,6 +221,23 @@ QAccessible::Role AccessibleItemInterface::role() const
case IAccessible::Role::Dialog: return QAccessible::Dialog;
case IAccessible::Role::Panel: return QAccessible::Pane;
case IAccessible::Role::StaticText: return QAccessible::StaticText;
+ case IAccessible::Role::SilentRole: {
+ // See https://doc.qt.io/qt-5/qaccessible.html#Role-enum
+ // We want the screen reader to say the name of the current item and
+ // nothing else (i.e. not the name followed by "button" or "text").
+#if defined(Q_OS_MACOS)
+ // Good on macOS with VoiceOver.
+ return QAccessible::StaticText;
+ // VoiceOver gives unwanted additional output if ListItem is used, and it
+ // doesn't work at all if the role is TreeItem or Cell.
+#else
+ // Good on Windows with Narrator, NVDA, or JAWS; and on Linux with Orca.
+ return QAccessible::ListItem;
+ // Orca is equally happy with the roles TreeItem or Cell, but these cause
+ // unwanted additional ouput on Windows. StaticText causes unwanted
+ // additional output on both Linux and Windows.
+#endif
+ }
case IAccessible::Role::EditableText: return QAccessible::EditableText;
case IAccessible::Role::Button: return QAccessible::Button;
case IAccessible::Role::CheckBox: return QAccessible::CheckBox;
diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/EditShortcutDialogContent.qml b/src/framework/shortcuts/qml/Muse/Shortcuts/EditShortcutDialogContent.qml
index b383e77d5ea3e..c50b3dbd77b8d 100644
--- a/src/framework/shortcuts/qml/Muse/Shortcuts/EditShortcutDialogContent.qml
+++ b/src/framework/shortcuts/qml/Muse/Shortcuts/EditShortcutDialogContent.qml
@@ -44,6 +44,7 @@ Item {
signal saveRequested()
signal cancelRequested()
+ signal clearRequested()
signal keyPressed(var event)
anchors.fill: parent
@@ -138,6 +139,17 @@ Item {
navigationPanel.section: root.navigationSection
navigationPanel.order: 2
+ FlatButton {
+ text: qsTrc("global", "Clear")
+ buttonRole: ButtonBoxModel.CustomRole
+ buttonId: ButtonBoxModel.Clear
+ isLeftSide: true
+
+ onClicked: {
+ root.clearRequested()
+ }
+ }
+
onStandardButtonClicked: function(buttonId) {
if (buttonId === ButtonBoxModel.Cancel) {
root.cancelRequested()
diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml b/src/framework/shortcuts/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml
index 7505bfbb9a3dc..8941901309d11 100644
--- a/src/framework/shortcuts/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml
+++ b/src/framework/shortcuts/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml
@@ -63,7 +63,8 @@ StyledDialogView {
headerText: qsTrc("shortcuts", "Define keyboard shortcut")
- originShortcutText: editShortcutModel.originSequence
+ //! NOTE: There's no need to actually clear the origin shortcut, we can simply hide it for aesthetic purposes...
+ originShortcutText: !editShortcutModel.cleared ? editShortcutModel.originSequence : ""
newShortcutText: editShortcutModel.newSequence
informationText: editShortcutModel.conflictWarning
@@ -76,6 +77,10 @@ StyledDialogView {
root.reject()
}
+ onClearRequested: {
+ editShortcutModel.clear()
+ }
+
onKeyPressed: function(event) {
editShortcutModel.inputKey(event.key, event.modifiers)
}
diff --git a/src/framework/shortcuts/view/editshortcutmodel.cpp b/src/framework/shortcuts/view/editshortcutmodel.cpp
index 2d06428f5fe2b..d803a16f91fd3 100644
--- a/src/framework/shortcuts/view/editshortcutmodel.cpp
+++ b/src/framework/shortcuts/view/editshortcutmodel.cpp
@@ -63,7 +63,10 @@ void EditShortcutModel::load(const QVariant& originShortcut, const QVariantList&
m_originSequence = originShortcutMap.value("sequence").toString();
m_originShortcutTitle = originShortcutMap.value("title").toString();
+ m_cleared = false;
+
emit originSequenceChanged();
+ emit clearedChanged();
}
void EditShortcutModel::clearNewSequence()
@@ -110,6 +113,14 @@ void EditShortcutModel::inputKey(Qt::Key key, Qt::KeyboardModifiers modifiers)
emit newSequenceChanged();
}
+void EditShortcutModel::clear()
+{
+ clearNewSequence();
+ m_cleared = true;
+ emit originSequenceChanged();
+ emit clearedChanged();
+}
+
bool EditShortcutModel::isShiftAllowed(Qt::Key key)
{
if (key >= Qt::Key_A && key <= Qt::Key_Z) {
@@ -215,8 +226,8 @@ QString EditShortcutModel::conflictWarning() const
void EditShortcutModel::trySave()
{
QString newSequence = this->newSequence();
-
- if (m_originSequence == newSequence) {
+ const bool alreadyEmpty = originSequenceInNativeFormat().isEmpty() && m_cleared;
+ if (alreadyEmpty || m_originSequence == newSequence) {
return;
}
diff --git a/src/framework/shortcuts/view/editshortcutmodel.h b/src/framework/shortcuts/view/editshortcutmodel.h
index 8ee05fa787117..3040ed03391ac 100644
--- a/src/framework/shortcuts/view/editshortcutmodel.h
+++ b/src/framework/shortcuts/view/editshortcutmodel.h
@@ -39,6 +39,8 @@ class EditShortcutModel : public QObject, public Injectable
Q_PROPERTY(QString newSequence READ newSequenceInNativeFormat NOTIFY newSequenceChanged)
Q_PROPERTY(QString conflictWarning READ conflictWarning NOTIFY newSequenceChanged)
+ Q_PROPERTY(bool cleared READ cleared NOTIFY clearedChanged)
+
Inject interactive = { this };
public:
@@ -47,15 +49,18 @@ class EditShortcutModel : public QObject, public Injectable
QString originSequenceInNativeFormat() const;
QString newSequenceInNativeFormat() const;
QString conflictWarning() const;
+ bool cleared() const { return m_cleared; }
bool isShiftAllowed(Qt::Key key);
Q_INVOKABLE void load(const QVariant& shortcut, const QVariantList& allShortcuts);
Q_INVOKABLE void inputKey(Qt::Key key, Qt::KeyboardModifiers modifiers);
+ Q_INVOKABLE void clear();
Q_INVOKABLE void trySave();
signals:
void originSequenceChanged();
void newSequenceChanged();
+ void clearedChanged();
void applyNewSequenceRequested(const QString& newSequence, int conflictShortcutIndex = -1);
@@ -74,6 +79,8 @@ class EditShortcutModel : public QObject, public Injectable
QVariantMap m_conflictShortcut;
QKeySequence m_newSequence;
+
+ bool m_cleared = false;
};
}
diff --git a/src/framework/ui/view/qmlaccessible.h b/src/framework/ui/view/qmlaccessible.h
index a0eb3a09e4fbb..fb7ca93ebec29 100644
--- a/src/framework/ui/view/qmlaccessible.h
+++ b/src/framework/ui/view/qmlaccessible.h
@@ -52,6 +52,7 @@ class MUAccessible
Panel,
StaticText,
EditableText,
+ SilentRole,
Button,
CheckBox,
RadioButton,
diff --git a/src/notation/internal/notationinteraction.cpp b/src/notation/internal/notationinteraction.cpp
index 229798e02ecb4..1eea8a939dfb3 100644
--- a/src/notation/internal/notationinteraction.cpp
+++ b/src/notation/internal/notationinteraction.cpp
@@ -2027,9 +2027,38 @@ bool NotationInteraction::selectInstrument(mu::engraving::InstrumentChange* inst
instrumentChange->setInit(true);
instrumentChange->setupInstrument(&newInstrument);
+ if (newInstrument.useDrumset()) {
+ cleanupDrumsetChanges(instrumentChange);
+ }
+
return true;
}
+void NotationInteraction::cleanupDrumsetChanges(mu::engraving::InstrumentChange* instrumentChange) const
+{
+ Part* part = instrumentChange ? instrumentChange->part() : nullptr;
+ Instrument* newInstrument = instrumentChange ? instrumentChange->instrument() : nullptr;
+ if (!part || !newInstrument) {
+ return;
+ }
+
+ for (auto pair : part->instruments()) {
+ const Instrument* otherInst = pair.second;
+ if (!otherInst || otherInst == newInstrument) {
+ continue;
+ }
+
+ // If the following conditional is true, it means that we're trying to change to a drumset that already exists for this part. Due to the fact
+ // that we don't create new tracks for identical instruments in a given part, the knock-on effect is that the new drumset won't have a chance
+ // to load a MuseSampler patch (see usage of shouldLoadDrumset in PlaybackController). The following logic resolves this by copying the patch
+ // from the existing drumset into the new one...
+ if (otherInst->drumset() && newInstrument->id() == otherInst->id()) {
+ score()->undo(new engraving::ChangeDrumset(newInstrument, *otherInst->drumset(), part));
+ return;
+ }
+ }
+}
+
//! NOTE Copied from Palette::applyPaletteElement
bool NotationInteraction::applyPaletteElement(mu::engraving::EngravingItem* element, Qt::KeyboardModifiers modifiers)
{
diff --git a/src/notation/internal/notationinteraction.h b/src/notation/internal/notationinteraction.h
index 1c46cc5f40957..0b38d6855ab59 100644
--- a/src/notation/internal/notationinteraction.h
+++ b/src/notation/internal/notationinteraction.h
@@ -413,6 +413,7 @@ class NotationInteraction : public INotationInteraction, public muse::Injectable
void resetDropData();
bool selectInstrument(mu::engraving::InstrumentChange* instrumentChange);
+ void cleanupDrumsetChanges(mu::engraving::InstrumentChange* instrumentChange) const;
void applyDropPaletteElement(mu::engraving::Score* score, mu::engraving::EngravingItem* target, mu::engraving::EngravingItem* e,
Qt::KeyboardModifiers modifiers, muse::PointF pt = muse::PointF(), bool pasteMode = false);
diff --git a/src/notation/internal/notationparts.cpp b/src/notation/internal/notationparts.cpp
index 2ca23a3a8d4e0..d53c8b8391672 100644
--- a/src/notation/internal/notationparts.cpp
+++ b/src/notation/internal/notationparts.cpp
@@ -690,21 +690,26 @@ void NotationParts::replaceDrumset(const InstrumentKey& instrumentKey, const Dru
return;
}
- mu::engraving::Instrument* instrument = part->instrument(instrumentKey.tick);
- if (!instrument) {
- return;
- }
-
+ // Update all identical drumsets in the part...
if (undoable) {
startEdit(TranslatableString("undoableAction", "Edit drumset"));
- score()->undo(new mu::engraving::ChangeDrumset(instrument, newDrumset, part));
+ for (auto pair : part->instruments()) {
+ Instrument* instrument = pair.second;
+ if (instrument && instrument->drumset() && instrument->id() == instrumentKey.instrumentId) {
+ score()->undo(new mu::engraving::ChangeDrumset(instrument, newDrumset, part));
+ }
+ }
apply();
} else {
- instrument->setDrumset(&newDrumset);
+ for (auto pair : part->instruments()) {
+ Instrument* instrument = pair.second;
+ if (instrument && instrument->drumset() && instrument->id() == instrumentKey.instrumentId) {
+ instrument->setDrumset(&newDrumset);
+ }
+ }
}
notifyAboutPartChanged(part);
-
m_interaction->noteInput()->stateChanged().notify();
}
diff --git a/src/notation/qml/MuseScore/NotationScene/EditPercussionShortcutDialog.qml b/src/notation/qml/MuseScore/NotationScene/EditPercussionShortcutDialog.qml
index 0830ebaefbc16..c4c4617fdbc0d 100644
--- a/src/notation/qml/MuseScore/NotationScene/EditPercussionShortcutDialog.qml
+++ b/src/notation/qml/MuseScore/NotationScene/EditPercussionShortcutDialog.qml
@@ -70,7 +70,8 @@ StyledDialogView {
headerText: qsTrc("shortcuts", "Define percussion keyboard shortcut")
- originShortcutText: model.originShortcutText
+ //! NOTE: There's no need to actually clear the origin shortcut, we can simply hide it for aesthetic purposes...
+ originShortcutText: !model.cleared ? model.originShortcutText : ""
newShortcutText: model.newShortcutText
informationText: model.informationText
@@ -86,6 +87,10 @@ StyledDialogView {
root.reject()
}
+ onClearRequested: {
+ model.clear()
+ }
+
onKeyPressed: function(event) {
model.inputKey(event.key)
}
diff --git a/src/notation/qml/MuseScore/NotationScene/PercussionPanel.qml b/src/notation/qml/MuseScore/NotationScene/PercussionPanel.qml
index 7e1124cbea37d..556652d4d8976 100644
--- a/src/notation/qml/MuseScore/NotationScene/PercussionPanel.qml
+++ b/src/notation/qml/MuseScore/NotationScene/PercussionPanel.qml
@@ -168,7 +168,7 @@ Item {
onNavigationEvent: {
// Use the last known "pad navigation row" and tab to the associated delete button if it exists
var padNavigationRow = navigationPrv.currentPadNavigationIndex[0]
- if (padGrid.model.rowIsEmpty(padNavigationRow)) {
+ if (padGrid.numRows > 1) {
event.setData("controlIndex", [padNavigationRow, 0])
}
}
@@ -202,7 +202,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
- visible: padGrid.numRows > 1 && padGrid.model.rowIsEmpty(model.index)
+ visible: padGrid.numRows > 1
icon: IconCode.DELETE_TANK
backgroundRadius: deleteButton.width / 2
@@ -216,17 +216,6 @@ Item {
onClicked: {
padGrid.model.deleteRow(model.index)
}
-
- Connections {
- target: padGrid.model
-
- function onRowIsEmptyChanged(row, isEmpty) {
- if (row !== model.index) {
- return
- }
- deleteButton.visible = padGrid.numRows > 1 && isEmpty
- }
- }
}
}
}
diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml
index 31fac3472afd5..4ee59e504751c 100644
--- a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml
+++ b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml
@@ -65,15 +65,38 @@ DropArea {
readonly property color enabledBackgroundColor: Utils.colorWithAlpha(ui.theme.buttonColor, ui.theme.buttonOpacityNormal)
readonly property color disabledBackgroundColor: Utils.colorWithAlpha(ui.theme.buttonColor, ui.theme.itemOpacityDisabled)
readonly property real footerHeight: 24
- readonly property string accessibleDescription: {
+
+ readonly property string accessibleDetailsString: {
+ if (!Boolean(root.padModel)) {
+ return ""
+ }
+
+ //: %1 will be the MIDI note for a drum (displayed in the percussion panel)
+ let line1 = qsTrc("notation/percussion", "MIDI %1").arg(root.padModel.midiNote)
+
+ let shortcut = root.padModel.keyboardShortcut
+ if (shortcut === "") {
+ return line1
+ }
+
+
+ //: %1 will be the shortcut for a drum (displayed in the percussion panel)
+ let line2 = qsTrc("notation/percussion", "Shortcut %1").arg(shortcut)
+
+ return line2 + ", " + line1
+ }
+
+ readonly property string accessibleRowColumnString: {
//: %1 will be the row number of a percussion panel pad
- let line1 = qsTrc("notation/percussion", "Row: %1").arg(root.navigationRow + 1)
+ let line1 = qsTrc("notation/percussion", "Row %1").arg(root.navigationRow + 1)
//: %1 will be the column number of a percussion panel pad
- let line2 = qsTrc("notation/percussion", "Column: %1").arg(root.navigationColumn + 1)
+ let line2 = qsTrc("notation/percussion", "Column %1").arg(root.navigationColumn + 1)
- return line1 + ", " + line2
+ return line1 + " " + line2
}
+
+ readonly property string fullAccessibleString: prv.accessibleDetailsString + ", " + prv.accessibleRowColumnString
}
NavigationControl {
@@ -87,10 +110,10 @@ DropArea {
// Only navigate to empty slots when we're in edit mode
enabled: Boolean(root.padModel) || root.panelMode === PanelMode.EDIT_LAYOUT
- accessible.role: MUAccessible.Button
+ accessible.role: MUAccessible.SilentRole
accessible.name: Boolean(root.padModel) ? root.padModel.padName : qsTrc("notation/percussion", "Empty pad")
- accessible.description: prv.accessibleDescription
+ accessible.description: Boolean(root.padModel) ? prv.fullAccessibleString : prv.accessibleRowColumnString
accessible.visualItem: padFocusBorder
accessible.enabled: padNavCtrl.enabled
@@ -114,10 +137,8 @@ DropArea {
enabled: Boolean(root.padModel)
- accessible.role: MUAccessible.Button
- accessible.name: Boolean(root.padModel) ? root.padModel.padName + " " + qsTrc("notation/percussion", "footer") : ""
-
- accessible.description: prv.accessibleDescription
+ accessible.role: MUAccessible.SilentRole
+ accessible.name: Boolean(root.padModel) ? root.padModel.padName + " " + qsTrc("notation/percussion", "options") : ""
accessible.visualItem: footerFocusBorder
accessible.enabled: footerNavCtrl.enabled
diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml
index 038ecc551329d..f05d41a08b67f 100644
--- a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml
+++ b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml
@@ -180,6 +180,7 @@ Column {
anchors.fill: parent
enabled: root.panelMode !== PanelMode.EDIT_LAYOUT
+ hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
@@ -233,5 +234,24 @@ Column {
onHandleMenuItem: function(itemId) {
root.padModel.handleMenuItem(itemId)
}
+
+ states: [
+ State {
+ name: "MOUSE_HOVERED"
+ when: footerMouseArea.containsMouse && !footerMouseArea.pressed
+ PropertyChanges {
+ target: footerArea
+ color: Utils.colorWithAlpha(ui.theme.buttonColor, ui.theme.buttonOpacityHover)
+ }
+ },
+ State {
+ name: "MOUSE_HIT"
+ when: footerMouseArea.pressed
+ PropertyChanges {
+ target: footerArea
+ color: Utils.colorWithAlpha(ui.theme.buttonColor, ui.theme.buttonOpacityHit)
+ }
+ }
+ ]
}
}
diff --git a/src/notation/utilities/percussionutilities.cpp b/src/notation/utilities/percussionutilities.cpp
index 6c87384b99e98..b9800439111e6 100644
--- a/src/notation/utilities/percussionutilities.cpp
+++ b/src/notation/utilities/percussionutilities.cpp
@@ -105,15 +105,15 @@ std::shared_ptr PercussionUtilities::getDrumNoteForPreview(const Drumset*
}
/// Opens the percussion shortcut dialog, modifies drumset with user input
-void PercussionUtilities::editPercussionShortcut(Drumset& drumset, int originPitch)
+bool PercussionUtilities::editPercussionShortcut(Drumset& drumset, int originPitch)
{
IF_ASSERT_FAILED(drumset.isValid(originPitch)) {
- return;
+ return false;
}
const muse::RetVal rv = openPercussionShortcutDialog(drumset, originPitch);
if (!rv.ret) {
- return;
+ return false;
}
const QVariantMap vals = rv.val.toQVariant().toMap();
@@ -124,6 +124,8 @@ void PercussionUtilities::editPercussionShortcut(Drumset& drumset, int originPit
if (drumset.isValid(conflictShortcutPitch)) {
drumset.drum(conflictShortcutPitch).shortcut.clear();
}
+
+ return true;
}
muse::RetVal PercussionUtilities::openPercussionShortcutDialog(const Drumset& drumset, int originPitch)
diff --git a/src/notation/utilities/percussionutilities.h b/src/notation/utilities/percussionutilities.h
index bfceb2c06d1af..69c41749844bb 100644
--- a/src/notation/utilities/percussionutilities.h
+++ b/src/notation/utilities/percussionutilities.h
@@ -48,7 +48,7 @@ class PercussionUtilities
public:
static void readDrumset(const muse::ByteArray& drumMapping, mu::engraving::Drumset& drumset);
static std::shared_ptr getDrumNoteForPreview(const mu::engraving::Drumset* drumset, int pitch);
- static void editPercussionShortcut(mu::engraving::Drumset& drumset, int originPitch);
+ static bool editPercussionShortcut(mu::engraving::Drumset& drumset, int originPitch);
private:
static muse::RetVal openPercussionShortcutDialog(const mu::engraving::Drumset& drumset, int originPitch);
diff --git a/src/notation/view/editpercussionshortcutmodel.cpp b/src/notation/view/editpercussionshortcutmodel.cpp
index b884f15fea128..84631cec5a4f7 100644
--- a/src/notation/view/editpercussionshortcutmodel.cpp
+++ b/src/notation/view/editpercussionshortcutmodel.cpp
@@ -44,7 +44,10 @@ void EditPercussionShortcutModel::load(const QVariant& originDrum, const QVarian
m_drumsWithShortcut << drum;
}
+ m_cleared = false;
+
emit originShortcutTextChanged();
+ emit clearedChanged();
}
void EditPercussionShortcutModel::inputKey(Qt::Key key)
@@ -70,9 +73,22 @@ void EditPercussionShortcutModel::inputKey(Qt::Key key)
emit newShortcutTextChanged();
}
+void EditPercussionShortcutModel::clear()
+{
+ m_newShortcut = QKeySequence();
+ m_conflictShortcut.clear();
+
+ m_cleared = true;
+
+ emit newShortcutTextChanged();
+ emit clearedChanged();
+}
+
bool EditPercussionShortcutModel::trySave()
{
- if (originShortcutText() == m_newShortcut.toString()) {
+ const QString newShortcut = m_newShortcut.toString();
+ const bool alreadyEmpty = originShortcutText().isEmpty() && m_cleared;
+ if (alreadyEmpty || originShortcutText() == newShortcut) {
return false;
}
diff --git a/src/notation/view/editpercussionshortcutmodel.h b/src/notation/view/editpercussionshortcutmodel.h
index f2004264ebc42..28007dd8dd4f8 100644
--- a/src/notation/view/editpercussionshortcutmodel.h
+++ b/src/notation/view/editpercussionshortcutmodel.h
@@ -37,6 +37,8 @@ class EditPercussionShortcutModel : public QObject, public muse::Injectable
Q_PROPERTY(QString newShortcutText READ newShortcutText NOTIFY newShortcutTextChanged)
Q_PROPERTY(QString informationText READ informationText NOTIFY newShortcutTextChanged)
+ Q_PROPERTY(bool cleared READ cleared NOTIFY clearedChanged)
+
Inject interactive = { this };
public:
@@ -44,6 +46,7 @@ class EditPercussionShortcutModel : public QObject, public muse::Injectable
Q_INVOKABLE void load(const QVariant& originDrum, const QVariantList& drumsWithShortcut, const QVariantList& applicationShortcuts);
Q_INVOKABLE void inputKey(Qt::Key key);
+ Q_INVOKABLE void clear();
Q_INVOKABLE bool trySave();
Q_INVOKABLE int conflictDrumPitch() const;
@@ -52,9 +55,12 @@ class EditPercussionShortcutModel : public QObject, public muse::Injectable
QString newShortcutText() const;
QString informationText() const;
+ bool cleared() const { return m_cleared; }
+
signals:
void originShortcutTextChanged();
void newShortcutTextChanged();
+ void clearedChanged();
private:
bool checkDrumShortcutsForConflict();
@@ -71,5 +77,6 @@ class EditPercussionShortcutModel : public QObject, public muse::Injectable
QVariantList m_applicationShortcuts;
bool m_conflictInAppShortcuts = false;
+ bool m_cleared = false;
};
}
diff --git a/src/notation/view/percussionpanel/percussionpanelmodel.cpp b/src/notation/view/percussionpanel/percussionpanelmodel.cpp
index 8dd594e965f7d..b82882e27e807 100644
--- a/src/notation/view/percussionpanel/percussionpanelmodel.cpp
+++ b/src/notation/view/percussionpanel/percussionpanelmodel.cpp
@@ -231,11 +231,8 @@ void PercussionPanelModel::finishEditing(bool discardChanges)
return;
}
- INotationUndoStackPtr undoStack = notation()->undoStack();
-
- undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Edit percussion panel layout"));
- score()->undo(new engraving::ChangeDrumset(inst, updatedDrumset, part));
- undoStack->commitChanges();
+ const InstrumentKey key = { inst->id(), part->id() };
+ notation()->parts()->replaceDrumset(key, updatedDrumset);
m_padListModel->focusLastActivePad();
}
@@ -411,11 +408,8 @@ void PercussionPanelModel::onDuplicatePadRequested(int pitch)
updatedDrumset.setDrum(nextAvailablePitch, duplicateDrum);
- INotationUndoStackPtr undoStack = notation()->undoStack();
-
- undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Duplicate percussion panel pad"));
- score()->undo(new engraving::ChangeDrumset(inst, updatedDrumset, part));
- undoStack->commitChanges();
+ const InstrumentKey key = { inst->id(), part->id() };
+ notation()->parts()->replaceDrumset(key, updatedDrumset);
}
void PercussionPanelModel::onDeletePadRequested(int pitch)
@@ -431,11 +425,8 @@ void PercussionPanelModel::onDeletePadRequested(int pitch)
Drumset updatedDrumset = *m_padListModel->drumset();
updatedDrumset.setDrum(pitch, engraving::DrumInstrument());
- INotationUndoStackPtr undoStack = notation()->undoStack();
-
- undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Delete percussion panel pad"));
- score()->undo(new engraving::ChangeDrumset(inst, updatedDrumset, part));
- undoStack->commitChanges();
+ const InstrumentKey key = { inst->id(), part->id() };
+ notation()->parts()->replaceDrumset(key, updatedDrumset);
}
void PercussionPanelModel::onDefinePadShortcutRequested(int pitch)
@@ -448,13 +439,12 @@ void PercussionPanelModel::onDefinePadShortcutRequested(int pitch)
}
Drumset updatedDrumset = *m_padListModel->drumset();
- PercussionUtilities::editPercussionShortcut(updatedDrumset, pitch);
-
- INotationUndoStackPtr undoStack = notation()->undoStack();
+ if (!PercussionUtilities::editPercussionShortcut(updatedDrumset, pitch)) {
+ return;
+ }
- undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Edit percussion shortcut"));
- score()->undo(new engraving::ChangeDrumset(inst, updatedDrumset, part));
- undoStack->commitChanges();
+ const InstrumentKey key = { inst->id(), part->id() };
+ notation()->parts()->replaceDrumset(key, updatedDrumset);
}
void PercussionPanelModel::writePitch(int pitch, const NoteAddingMode& addingMode)
@@ -513,11 +503,8 @@ void PercussionPanelModel::resetLayout()
return;
}
- INotationUndoStackPtr undoStack = notation()->undoStack();
-
- undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Reset percussion panel layout"));
- score()->undo(new engraving::ChangeDrumset(inst, defaultLayout, part));
- undoStack->commitChanges();
+ const InstrumentKey key = { inst->id(), part->id() };
+ notation()->parts()->replaceDrumset(key, defaultLayout);
}
Drumset PercussionPanelModel::standardDefaultDrumset() const
diff --git a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp
index 98e08f09f7588..e3172d700f895 100644
--- a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp
+++ b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp
@@ -84,7 +84,17 @@ void PercussionPanelPadListModel::addEmptyRow(bool focusFirstInNewRow)
void PercussionPanelPadListModel::deleteRow(int row)
{
+ // Update the drumset...
+ const int startIdx = row * NUM_COLUMNS;
+ for (int i = startIdx; i < startIdx + NUM_COLUMNS; ++i) {
+ if (const PercussionPanelPadModel* model = m_padModels.at(i)) {
+ m_drumset->setDrum(model->pitch(), mu::engraving::DrumInstrument());
+ }
+ }
+
+ // Then remove the row...
m_padModels.remove(row * NUM_COLUMNS, NUM_COLUMNS);
+
emit layoutChanged();
emit numPadsChanged();
}
@@ -95,7 +105,8 @@ void PercussionPanelPadListModel::removeEmptyRows()
const int lastRowIndex = numPads() / NUM_COLUMNS - 1;
for (int i = lastRowIndex; i >= 0; --i) {
const int numRows = numPads() / NUM_COLUMNS;
- if (rowIsEmpty(i) && numRows > 1) { // never delete the first row
+ const bool rowIsEmpty = numEmptySlotsAtRow(i) == NUM_COLUMNS;
+ if (rowIsEmpty && numRows > 1) { // never delete the first row
m_padModels.remove(i * NUM_COLUMNS, NUM_COLUMNS);
rowsRemoved = true;
}
@@ -106,11 +117,6 @@ void PercussionPanelPadListModel::removeEmptyRows()
}
}
-bool PercussionPanelPadListModel::rowIsEmpty(int row) const
-{
- return numEmptySlotsAtRow(row) == NUM_COLUMNS;
-}
-
void PercussionPanelPadListModel::startPadSwap(int startIndex)
{
m_padSwapStartIndex = startIndex;
@@ -424,23 +430,8 @@ int PercussionPanelPadListModel::getModelIndexForPitch(int pitch) const
void PercussionPanelPadListModel::movePad(int fromIndex, int toIndex)
{
- const int fromRow = fromIndex / NUM_COLUMNS;
- const int toRow = toIndex / NUM_COLUMNS;
-
- // fromRow will become empty if there's only 1 "occupied" slot, toRow will no longer be empty if it was previously...
- const bool fromRowEmptyChanged = numEmptySlotsAtRow(fromRow) == NUM_COLUMNS - 1;
- const bool toRowEmptyChanged = rowIsEmpty(toRow);
-
m_padModels.swapItemsAt(fromIndex, toIndex);
emit layoutChanged();
-
- if (fromRowEmptyChanged) {
- emit rowIsEmptyChanged(fromRow, /*isEmpty*/ true);
- }
-
- if (toRowEmptyChanged) {
- emit rowIsEmptyChanged(toRow, /*isEmpty*/ false);
- }
}
int PercussionPanelPadListModel::numEmptySlotsAtRow(int row) const
diff --git a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h
index ec049f60bc277..8c9b2b8d4b740 100644
--- a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h
+++ b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h
@@ -61,8 +61,6 @@ class PercussionPanelPadListModel : public QAbstractListModel, public muse::Inje
void removeEmptyRows();
- Q_INVOKABLE bool rowIsEmpty(int row) const;
-
Q_INVOKABLE void startPadSwap(int startIndex);
Q_INVOKABLE void endPadSwap(int endIndex);
bool swapInProgress() const { return indexIsValid(m_padSwapStartIndex); }
@@ -89,7 +87,6 @@ class PercussionPanelPadListModel : public QAbstractListModel, public muse::Inje
signals:
void numPadsChanged();
- void rowIsEmptyChanged(int row, bool empty);
void padFocusRequested(int padIndex); //! NOTE: This won't work if it is called immediately before a layoutChange
private:
diff --git a/src/palette/view/widgets/customizekitdialog.cpp b/src/palette/view/widgets/customizekitdialog.cpp
index 5c326cd6a01cd..c93bb0a1e91b2 100644
--- a/src/palette/view/widgets/customizekitdialog.cpp
+++ b/src/palette/view/widgets/customizekitdialog.cpp
@@ -666,7 +666,9 @@ void CustomizeKitDialog::defineShortcut()
}
const int originPitch = item->data(Column::PITCH, Qt::UserRole).toInt();
- PercussionUtilities::editPercussionShortcut(m_editedDrumset, originPitch);
+ if (!PercussionUtilities::editPercussionShortcut(m_editedDrumset, originPitch)) {
+ return;
+ }
const QString editedShortcutText = m_editedDrumset.shortcut(originPitch);
shortcut->setText(!editedShortcutText.isEmpty() ? editedShortcutText : muse::qtrc("shortcuts", "None"));