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"));