diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml index f988adc02d9ea..664b5c5e0181f 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPad.qml @@ -121,10 +121,6 @@ DropArea { accessible.visualItem: footerFocusBorder accessible.enabled: footerNavCtrl.enabled - - onTriggered: { - // TODO: trigger context menu (not yet implemented) - } } Rectangle { @@ -188,6 +184,8 @@ DropArea { id: padContentComponent PercussionPanelPadContent { + id: padContent + padModel: root.padModel panelMode: root.panelMode useNotationPreview: root.useNotationPreview @@ -195,6 +193,13 @@ DropArea { footerHeight: prv.footerHeight padSwapActive: dragHandler.active + + Connections { + target: footerNavCtrl + function onTriggered() { + padContent.openFooterContextMenu() + } + } } } diff --git a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml index cc5cf174b1dec..5f64099c6ff8c 100644 --- a/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml +++ b/src/notation/qml/MuseScore/NotationScene/internal/PercussionPanelPadContent.qml @@ -37,6 +37,13 @@ Column { property bool padSwapActive: false + function openFooterContextMenu() { + if (!root.padModel) { + return + } + menuLoader.toggleOpened(root.padModel.footerContextMenuItems) + } + Item { id: mainContentArea @@ -154,6 +161,17 @@ Column { color: Utils.colorWithAlpha(ui.theme.buttonColor, ui.theme.buttonOpacityNormal) + MouseArea { + id: footerMouseArea + + anchors.fill: parent + enabled: root.panelMode !== PanelMode.EDIT_LAYOUT + + onClicked: { + root.openFooterContextMenu() + } + } + StyledTextLabel { id: shortcutLabel @@ -190,5 +208,13 @@ Column { text: Boolean(root.padModel) ? root.padModel.midiNote : "" } + + StyledMenuLoader { + id: menuLoader + + onHandleMenuItem: function(itemId) { + root.padModel.handleMenuItem(itemId) + } + } } } diff --git a/src/notation/view/percussionpanel/percussionpanelmodel.cpp b/src/notation/view/percussionpanel/percussionpanelmodel.cpp index 96409efb3fb03..fc4671c2cd206 100644 --- a/src/notation/view/percussionpanel/percussionpanelmodel.cpp +++ b/src/notation/view/percussionpanel/percussionpanelmodel.cpp @@ -125,36 +125,27 @@ void PercussionPanelModel::init() QList PercussionPanelModel::layoutMenuItems() const { - const TranslatableString padNamesTitle("notation/percussion", "Pad names"); - // Using IconCode for this instead of "checked" because we want the tick to display on the left - const int padNamesIcon = static_cast(m_useNotationPreview ? IconCode::Code::NONE : IconCode::Code::TICK_RIGHT_ANGLE); + const auto editLayoutTitle = m_currentPanelMode == PanelMode::Mode::EDIT_LAYOUT + ? muse::qtrc("notation/percussion", "Finish editing") + : muse::qtrc("notation/percussion", "Edit layout"); - const TranslatableString notationPreviewTitle("notation/percussion", "Notation preview"); - // Using IconCode for this instead of "checked" because we want the tick to display on the left - const int notationPreviewIcon = static_cast(m_useNotationPreview ? IconCode::Code::TICK_RIGHT_ANGLE : IconCode::Code::NONE); - - const TranslatableString editLayoutTitle = m_currentPanelMode == PanelMode::Mode::EDIT_LAYOUT - ? TranslatableString("notation/percussion", "Finish editing") - : TranslatableString("notation/percussion", "Edit layout"); - const int editLayoutIcon = static_cast(IconCode::Code::CONFIGURE); - - const TranslatableString resetLayoutTitle("notation/percussion", "Reset layout"); - const int resetLayoutIcon = static_cast(IconCode::Code::UNDO); + static constexpr int editLayoutIcon = static_cast(IconCode::Code::CONFIGURE); + static constexpr int resetLayoutIcon = static_cast(IconCode::Code::UNDO); QList menuItems = { - { { "id", PAD_NAMES_CODE }, - { "title", padNamesTitle.qTranslated() }, { "icon", padNamesIcon }, { "enabled", true } }, + { { "id", PAD_NAMES_CODE }, { "title", muse::qtrc("notation/percussion", "Pad names") }, + { "checkable", true }, { "checked", !m_useNotationPreview }, { "enabled", true } }, - { { "id", NOTATION_PREVIEW_CODE }, - { "title", notationPreviewTitle.qTranslated() }, { "icon", notationPreviewIcon }, { "enabled", true } }, + { { "id", NOTATION_PREVIEW_CODE }, { "title", muse::qtrc("notation/percussion", "Notation preview") }, + { "checkable", true }, { "checked", m_useNotationPreview }, { "enabled", true } }, { }, // separator { { "id", EDIT_LAYOUT_CODE }, - { "title", editLayoutTitle.qTranslated() }, { "icon", editLayoutIcon }, { "enabled", true } }, + { "title", editLayoutTitle }, { "icon", editLayoutIcon }, { "enabled", true } }, { { "id", RESET_LAYOUT_CODE }, - { "title", resetLayoutTitle.qTranslated() }, { "icon", resetLayoutIcon }, { "enabled", true } }, + { "title", muse::qtrc("notation/percussion", "Reset layout") }, { "icon", resetLayoutIcon }, { "enabled", true } }, }; return menuItems; @@ -185,16 +176,11 @@ void PercussionPanelModel::finishEditing(bool discardChanges) m_padListModel->removeEmptyRows(); - NoteInputState inputState = interaction()->noteInput()->state(); - const Staff* staff = inputState.staff; - - IF_ASSERT_FAILED(staff && staff->part()) { - return; - } - - Instrument* inst = staff->part()->instrument(inputState.segment->tick()); + const std::pair instAndPart = getCurrentInstrumentAndPart(); + Instrument* inst = instAndPart.first; + Part* part = instAndPart.second; - IF_ASSERT_FAILED(inst && inst->drumset()) { + IF_ASSERT_FAILED(inst && inst->drumset() && part) { return; } @@ -234,7 +220,7 @@ void PercussionPanelModel::finishEditing(bool discardChanges) INotationUndoStackPtr undoStack = notation()->undoStack(); undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Edit percussion panel layout")); - score()->undo(new engraving::ChangeDrumset(inst, &updatedDrumset, staff->part())); + score()->undo(new engraving::ChangeDrumset(inst, &updatedDrumset, part)); undoStack->commitChanges(); setCurrentPanelMode(m_panelModeToRestore); @@ -283,11 +269,22 @@ void PercussionPanelModel::setUpConnections() setEnabled(m_padListModel->hasActivePads()); }); - m_padListModel->padTriggered().onReceive(this, [this](int pitch) { - switch (currentPanelMode()) { - case PanelMode::Mode::EDIT_LAYOUT: return; - case PanelMode::Mode::WRITE: writePitch(pitch); // fall through - case PanelMode::Mode::SOUND_PREVIEW: playPitch(pitch); + m_padListModel->padActionRequested().onReceive(this, [this](PercussionPanelPadModel::PadAction action, int pitch) { + switch (action) { + case PercussionPanelPadModel::PadAction::TRIGGER: + onPadTriggered(pitch); + break; + case PercussionPanelPadModel::PadAction::DUPLICATE: + onDuplicatePadRequested(pitch); + break; + case PercussionPanelPadModel::PadAction::DELETE: + onDeletePadRequested(pitch); + break; + case PercussionPanelPadModel::PadAction::DEFINE_SHORTCUT: + onDefinePadShortcutRequested(pitch); + break; + default: + break; } }); @@ -338,6 +335,76 @@ bool PercussionPanelModel::eventFilter(QObject* watched, QEvent* event) return true; } +void PercussionPanelModel::onPadTriggered(int pitch) +{ + switch (currentPanelMode()) { + case PanelMode::Mode::EDIT_LAYOUT: return; + case PanelMode::Mode::WRITE: writePitch(pitch); // fall through + case PanelMode::Mode::SOUND_PREVIEW: playPitch(pitch); + default: break; + } +} + +void PercussionPanelModel::onDuplicatePadRequested(int pitch) +{ + const std::pair instAndPart = getCurrentInstrumentAndPart(); + Instrument* inst = instAndPart.first; + Part* part = instAndPart.second; + + IF_ASSERT_FAILED(inst && part) { + return; + } + + const int nextAvailablePitch = m_padListModel->nextAvailablePitch(pitch); + if (nextAvailablePitch < 0) { + // TODO: Show some sort of warning dialog? + LOGE() << "No space to duplicate drum pad"; + return; + } + const int nextAvailableIndex = m_padListModel->nextAvailableIndex(pitch); + + Drumset updatedDrumset = *m_padListModel->drumset(); + + engraving::DrumInstrument duplicateDrum = updatedDrumset.drum(pitch); + + duplicateDrum.shortcut = '\0'; // Don't steal the shortcut + duplicateDrum.panelRow = nextAvailableIndex / m_padListModel->numColumns(); + duplicateDrum.panelColumn = nextAvailableIndex % m_padListModel->numColumns(); + + 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(); +} + +void PercussionPanelModel::onDeletePadRequested(int pitch) +{ + const std::pair instAndPart = getCurrentInstrumentAndPart(); + Instrument* inst = instAndPart.first; + Part* part = instAndPart.second; + + IF_ASSERT_FAILED(inst && part) { + return; + } + + 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(); +} + +void PercussionPanelModel::onDefinePadShortcutRequested(int) +{ + // TODO: Design in progress... +} + void PercussionPanelModel::writePitch(int pitch) { INotationUndoStackPtr undoStack = notation()->undoStack(); @@ -388,16 +455,11 @@ void PercussionPanelModel::resetLayout() finishEditing(/*discardChanges*/ true); } - NoteInputState inputState = interaction()->noteInput()->state(); - const Staff* staff = inputState.staff; + const std::pair instAndPart = getCurrentInstrumentAndPart(); + Instrument* inst = instAndPart.first; + Part* part = instAndPart.second; - IF_ASSERT_FAILED(staff && staff->part()) { - return; - } - - Instrument* inst = staff->part()->instrument(inputState.segment->tick()); - - IF_ASSERT_FAILED(inst) { + IF_ASSERT_FAILED(inst && inst->drumset() && part) { return; } @@ -417,7 +479,7 @@ void PercussionPanelModel::resetLayout() INotationUndoStackPtr undoStack = notation()->undoStack(); undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Reset percussion panel layout")); - score()->undo(new engraving::ChangeDrumset(inst, &defaultLayout, staff->part())); + score()->undo(new engraving::ChangeDrumset(inst, &defaultLayout, part)); undoStack->commitChanges(); } @@ -437,6 +499,23 @@ InstrumentTrackId PercussionPanelModel::currentTrackId() const return { staff->part()->id(), staff->part()->instrumentId(inputState.segment->tick()) }; } +std::pair PercussionPanelModel::getCurrentInstrumentAndPart() const +{ + if (!interaction()) { + return { nullptr, nullptr }; + } + + NoteInputState inputState = interaction()->noteInput()->state(); + + const Staff* staff = inputState.staff; + + Part* part = staff ? staff->part() : nullptr; + + Instrument* inst = part ? part->instrument(inputState.segment->tick()) : nullptr; + + return { inst, part }; +} + const project::IProjectAudioSettingsPtr PercussionPanelModel::audioSettings() const { return globalContext()->currentProject() ? globalContext()->currentProject()->audioSettings() : nullptr; diff --git a/src/notation/view/percussionpanel/percussionpanelmodel.h b/src/notation/view/percussionpanel/percussionpanelmodel.h index 0eec3c27d9cc3..2b612dfcf00ee 100644 --- a/src/notation/view/percussionpanel/percussionpanelmodel.h +++ b/src/notation/view/percussionpanel/percussionpanelmodel.h @@ -113,6 +113,11 @@ class PercussionPanelModel : public QObject, public muse::Injectable, public mus bool eventFilter(QObject* watched, QEvent* event) override; + void onPadTriggered(int pitch); + void onDuplicatePadRequested(int pitch); + void onDeletePadRequested(int pitch); + void onDefinePadShortcutRequested(int pitch); + void writePitch(int pitch); void playPitch(int pitch); @@ -121,6 +126,9 @@ class PercussionPanelModel : public QObject, public muse::Injectable, public mus mu::engraving::InstrumentTrackId currentTrackId() const; const project::IProjectAudioSettingsPtr audioSettings() const; + + std::pair getCurrentInstrumentAndPart() const; + const mu::notation::INotationPtr notation() const; const mu::notation::INotationInteractionPtr interaction() const; diff --git a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp index 39625d0d7b8f5..e5e15c465c69b 100644 --- a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp +++ b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.cpp @@ -235,6 +235,35 @@ void PercussionPanelPadListModel::focusLastActivePad() } } +int PercussionPanelPadListModel::nextAvailableIndex(int pitch) const +{ + const int currentModelIndex = getModelIndexForPitch(pitch); + for (int candidateIndex = currentModelIndex + 1; candidateIndex != currentModelIndex; ++candidateIndex) { + if (candidateIndex == m_padModels.size()) { + // Wrap around + candidateIndex = 0; + } + if (!m_padModels.at(candidateIndex)) { + return candidateIndex; + } + } + return m_padModels.size(); +} + +int PercussionPanelPadListModel::nextAvailablePitch(int pitch) const +{ + for (int candidatePitch = pitch + 1; candidatePitch != pitch; ++candidatePitch) { + if (candidatePitch == mu::engraving::DRUM_INSTRUMENTS) { + // Wrap around + candidatePitch = 0; + } + if (!m_drumset->isValid(candidatePitch)) { + return candidatePitch; + } + } + return -1; +} + void PercussionPanelPadListModel::load() { beginResetModel(); @@ -317,8 +346,8 @@ PercussionPanelPadModel* PercussionPanelPadListModel::createPadModelForPitch(int model->setPitch(pitch); - model->padTriggered().onNotify(this, [this, pitch]() { - m_triggeredChannel.send(pitch); + model->padActionTriggered().onReceive(this, [this, pitch](PercussionPanelPadModel::PadAction action) { + m_padActionRequestChannel.send(action, pitch); }); model->setNotationPreviewItem(PercussionUtilities::getDrumNoteForPreview(m_drumset, pitch)); @@ -395,6 +424,22 @@ void PercussionPanelPadListModel::swapMidiNotesAndShortcuts(int fromIndex, int t toModel->setKeyboardShortcut(tempShortcut); } +int PercussionPanelPadListModel::getModelIndexForPitch(int pitch) const +{ + IF_ASSERT_FAILED(m_drumset && m_drumset->isValid(pitch)) { + return -1; + } + + for (int i = 0; i < m_padModels.size(); ++i) { + const PercussionPanelPadModel* model = m_padModels.at(i); + if (model && model->pitch() == pitch) { + return i; + } + } + + return -1; +} + void PercussionPanelPadListModel::movePad(int fromIndex, int toIndex) { const int fromRow = fromIndex / NUM_COLUMNS; diff --git a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h index 82653d6c717ec..764041b00bee8 100644 --- a/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h +++ b/src/notation/view/percussionpanel/percussionpanelpadlistmodel.h @@ -78,11 +78,13 @@ class PercussionPanelPadListModel : public QAbstractListModel, public muse::Inje QList padList() const { return m_padModels; } mu::engraving::Drumset constructDefaultLayout(const engraving::Drumset* templateDrumset) const; + int nextAvailableIndex(int pitch) const; //! NOTE: This may be equal to m_padModels.size() + int nextAvailablePitch(int pitch) const; void focusLastActivePad(); muse::async::Notification hasActivePadsChanged() const { return m_hasActivePadsChanged; } - muse::async::Channel padTriggered() const { return m_triggeredChannel; } + muse::async::Channel padActionRequested() const { return m_padActionRequestChannel; } signals: void numPadsChanged(); @@ -103,6 +105,8 @@ class PercussionPanelPadListModel : public QAbstractListModel, public muse::Inje PercussionPanelPadModel* createPadModelForPitch(int pitch); int createModelIndexForPitch(int pitch) const; + int getModelIndexForPitch(int pitch) const; + void movePad(int fromIndex, int toIndex); muse::RetVal openPadSwapDialog(); @@ -116,6 +120,6 @@ class PercussionPanelPadListModel : public QAbstractListModel, public muse::Inje int m_padSwapStartIndex = -1; muse::async::Notification m_hasActivePadsChanged; - muse::async::Channel m_triggeredChannel; + muse::async::Channel m_padActionRequestChannel; }; } diff --git a/src/notation/view/percussionpanel/percussionpanelpadmodel.cpp b/src/notation/view/percussionpanel/percussionpanelpadmodel.cpp index 7a3ea0e4601c7..d9f2b94a90d4a 100644 --- a/src/notation/view/percussionpanel/percussionpanelpadmodel.cpp +++ b/src/notation/view/percussionpanel/percussionpanelpadmodel.cpp @@ -22,7 +22,14 @@ #include "percussionpanelpadmodel.h" +#include "ui/view/iconcodes.h" + +static const QString DUPLICATE_PAD_CODE("duplicate-pad"); +static const QString DELETE_PAD_CODE("delete-pad"); +static const QString DEFINE_PAD_SHORTCUT_CODE("define-pad-shortcut"); + using namespace mu::notation; +using namespace muse::ui; PercussionPanelPadModel::PercussionPanelPadModel(QObject* parent) : QObject(parent) @@ -74,7 +81,40 @@ const QVariant PercussionPanelPadModel::notationPreviewItemVariant() const return QVariant::fromValue(m_notationPreviewItem); } +QList PercussionPanelPadModel::footerContextMenuItems() const +{ + static constexpr int duplicatePadIcon = static_cast(IconCode::Code::COPY); + static constexpr int deletePadIcon = static_cast(IconCode::Code::DELETE_TANK); + static constexpr int definePadShortcutIcon = static_cast(IconCode::Code::SHORTCUTS); + + QList menuItems = { + { { "id", DUPLICATE_PAD_CODE }, { "title", muse::qtrc("global", "Duplicate") }, + { "icon", duplicatePadIcon }, { "enabled", true } }, + + { { "id", DELETE_PAD_CODE }, { "title", muse::qtrc("global", "Delete") }, + { "icon", deletePadIcon }, { "enabled", true } }, + + { }, // separator + + { { "id", DEFINE_PAD_SHORTCUT_CODE }, { "title", muse::qtrc("shortcuts", "Define keyboard shortcut") }, + { "icon", definePadShortcutIcon }, { "enabled", true } }, + }; + + return menuItems; +} + +void PercussionPanelPadModel::handleMenuItem(const QString& itemId) +{ + if (itemId == DUPLICATE_PAD_CODE) { + m_padActionTriggered.send(PadAction::DUPLICATE); + } else if (itemId == DELETE_PAD_CODE) { + m_padActionTriggered.send(PadAction::DELETE); + } else if (itemId == DEFINE_PAD_SHORTCUT_CODE) { + m_padActionTriggered.send(PadAction::DEFINE_SHORTCUT); + } +} + void PercussionPanelPadModel::triggerPad() { - m_triggeredNotification.notify(); + m_padActionTriggered.send(PadAction::TRIGGER); } diff --git a/src/notation/view/percussionpanel/percussionpanelpadmodel.h b/src/notation/view/percussionpanel/percussionpanelpadmodel.h index 1a6c0d7c44026..9ed63728fc363 100644 --- a/src/notation/view/percussionpanel/percussionpanelpadmodel.h +++ b/src/notation/view/percussionpanel/percussionpanelpadmodel.h @@ -43,6 +43,8 @@ class PercussionPanelPadModel : public QObject, public muse::async::Asyncable Q_PROPERTY(QVariant notationPreviewItem READ notationPreviewItemVariant NOTIFY notationPreviewItemChanged) + Q_PROPERTY(QList footerContextMenuItems READ footerContextMenuItems CONSTANT) + public: explicit PercussionPanelPadModel(QObject* parent = nullptr); @@ -62,8 +64,19 @@ class PercussionPanelPadModel : public QObject, public muse::async::Asyncable const QVariant notationPreviewItemVariant() const; + QList footerContextMenuItems() const; + Q_INVOKABLE void handleMenuItem(const QString& itemId); + Q_INVOKABLE void triggerPad(); - muse::async::Notification padTriggered() const { return m_triggeredNotification; } + + enum class PadAction { + TRIGGER, + DUPLICATE, + DELETE, + DEFINE_SHORTCUT, + }; + + muse::async::Channel padActionTriggered() const { return m_padActionTriggered; } signals: void padNameChanged(); @@ -81,6 +94,6 @@ class PercussionPanelPadModel : public QObject, public muse::async::Asyncable mu::engraving::ElementPtr m_notationPreviewItem; - muse::async::Notification m_triggeredNotification; + muse::async::Channel m_padActionTriggered; }; }