Merge pull request #10926 from cbjeukendrup/onscreen_piano

Implemented On-screen Piano Keyboard
This commit is contained in:
RomanPudashkin 2022-03-31 16:28:27 +02:00 committed by GitHub
commit 064eb06ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1400 additions and 58 deletions

View file

@ -32,11 +32,14 @@ using DockName = QString;
static const DockName PALETTES_PANEL_NAME("palettesPanel");
static const DockName INSTRUMENTS_PANEL_NAME("instrumentsPanel");
static const DockName INSPECTOR_PANEL_NAME("inspectorPanel");
static const DockName NOTATION_NAVIGATOR_PANEL_NAME("notationNavigatorPanel");
static const DockName TIMELINE_PANEL_NAME("timelinePanel");
static const DockName MIXER_PANEL_NAME("mixerPanel");
static const DockName SELECTION_FILTERS_PANEL_NAME("selectionFiltersPanel");
static const DockName PIANO_PANEL_NAME("pianoPanel");
static const DockName NOTATION_NAVIGATOR_PANEL_NAME("notationNavigatorPanel");
static const DockName MIXER_PANEL_NAME("mixerPanel");
static const DockName PIANO_ROLL_PANEL_NAME("pianoRollPanel");
static const DockName PIANO_KEYBOARD_PANEL_NAME("pianoKeyboardPanel");
static const DockName TIMELINE_PANEL_NAME("timelinePanel");
static const DockName DRUMSET_PANEL_NAME("drumsetPanel");
// Toolbars:

View file

@ -81,24 +81,29 @@ const UiActionList ApplicationUiActions::m_actions = {
QT_TRANSLATE_NOOP("action", "Revert to factory settings"),
QT_TRANSLATE_NOOP("action", "Revert to factory settings")
),
// Docking
UiAction("dock-restore-default-layout",
mu::context::UiCtxAny,
QT_TRANSLATE_NOOP("action", "Restore the default layout"),
QT_TRANSLATE_NOOP("action", "Restore the default layout")
),
UiAction("toggle-mixer",
// Toolbars
UiAction("toggle-transport",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Mixer"),
QT_TRANSLATE_NOOP("action", "Toggle mixer"),
IconCode::Code::MIXER,
QT_TRANSLATE_NOOP("action", "Playback Controls"),
QT_TRANSLATE_NOOP("action", "Toggle Playback Controls toolbar"),
Checkable::Yes
),
UiAction("toggle-navigator",
UiAction("toggle-noteinput",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Navigator"),
QT_TRANSLATE_NOOP("action", "Toggle 'Navigator'"),
QT_TRANSLATE_NOOP("action", "Note Input"),
QT_TRANSLATE_NOOP("action", "Toggle 'Note Input' toolbar"),
Checkable::Yes
),
// Vertical panels
UiAction("toggle-palettes",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Palettes"),
@ -123,40 +128,39 @@ const UiActionList ApplicationUiActions::m_actions = {
QT_TRANSLATE_NOOP("action", "Toggle 'Selection filter'"),
Checkable::Yes
),
UiAction("toggle-statusbar",
// Navigator
UiAction("toggle-navigator",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Status bar"),
QT_TRANSLATE_NOOP("action", "Toggle 'Status bar'"),
Checkable::Yes
),
UiAction("toggle-noteinput",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Note Input"),
QT_TRANSLATE_NOOP("action", "Toggle 'Note Input' toolbar"),
Checkable::Yes
),
UiAction("toggle-transport",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Playback Controls"),
QT_TRANSLATE_NOOP("action", "Toggle Playback Controls toolbar"),
QT_TRANSLATE_NOOP("action", "Navigator"),
QT_TRANSLATE_NOOP("action", "Toggle 'Navigator'"),
Checkable::Yes
),
// Horizontal panels
UiAction("toggle-timeline",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Timeline"),
QT_TRANSLATE_NOOP("action", "Toggle timeline"),
Checkable::Yes
),
UiAction("synth-control",
UiAction("toggle-mixer",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Synthesizer"),
QT_TRANSLATE_NOOP("action", "Toggle synthesizer"),
QT_TRANSLATE_NOOP("action", "Mixer"),
QT_TRANSLATE_NOOP("action", "Toggle mixer"),
IconCode::Code::MIXER,
Checkable::Yes
),
UiAction("toggle-piano",
UiAction("toggle-piano-roll",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Piano"),
QT_TRANSLATE_NOOP("action", "Toggle piano"),
QT_TRANSLATE_NOOP("action", "Piano roll"),
QT_TRANSLATE_NOOP("action", "Toggle piano roll"),
Checkable::Yes
),
UiAction("toggle-piano-keyboard",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Piano keyboard"),
QT_TRANSLATE_NOOP("action", "Toggle piano keyboard"),
Checkable::Yes
),
UiAction("toggle-scorecmp-tool",
@ -165,6 +169,15 @@ const UiActionList ApplicationUiActions::m_actions = {
QT_TRANSLATE_NOOP("action", "Toggle score comparison tool"),
Checkable::Yes
),
// Status bar
UiAction("toggle-statusbar",
mu::context::UiCtxNotationOpened,
QT_TRANSLATE_NOOP("action", "Status bar"),
QT_TRANSLATE_NOOP("action", "Toggle 'Status bar'"),
Checkable::Yes
),
UiAction("preference-dialog",
mu::context::UiCtxAny,
QT_TRANSLATE_NOOP("action", "&Preferences"),
@ -260,16 +273,22 @@ mu::async::Channel<mu::actions::ActionCodeList> ApplicationUiActions::actionChec
const QMap<mu::actions::ActionCode, DockName>& ApplicationUiActions::toggleDockActions()
{
static const QMap<mu::actions::ActionCode, DockName> actionsMap {
{ TOGGLE_NAVIGATOR_ACTION_CODE, NOTATION_NAVIGATOR_PANEL_NAME },
{ "toggle-mixer", MIXER_PANEL_NAME },
{ "toggle-timeline", TIMELINE_PANEL_NAME },
{ "toggle-transport", PLAYBACK_TOOLBAR_NAME },
{ "toggle-noteinput", NOTE_INPUT_BAR_NAME },
{ "toggle-palettes", PALETTES_PANEL_NAME },
{ "toggle-instruments", INSTRUMENTS_PANEL_NAME },
{ "inspector", INSPECTOR_PANEL_NAME },
{ "toggle-selection-filter", SELECTION_FILTERS_PANEL_NAME },
{ TOGGLE_NAVIGATOR_ACTION_CODE, NOTATION_NAVIGATOR_PANEL_NAME },
{ "toggle-timeline", TIMELINE_PANEL_NAME },
{ "toggle-mixer", MIXER_PANEL_NAME },
{ "toggle-piano-roll", PIANO_ROLL_PANEL_NAME },
{ "toggle-piano-keyboard", PIANO_KEYBOARD_PANEL_NAME },
{ "toggle-statusbar", NOTATION_STATUSBAR_NAME },
{ "toggle-noteinput", NOTE_INPUT_BAR_NAME },
{ "toggle-transport", PLAYBACK_TOOLBAR_NAME }
};
return actionsMap;

View file

@ -335,8 +335,33 @@ DockPage {
DockPanel {
id: pianoRollPanel
objectName: pageModel.pianoPanelName()
title: qsTrc("appshell", "Piano Roll")
objectName: pageModel.pianoRollPanelName()
title: qsTrc("appshell", "Piano roll")
height: 200
minimumHeight: root.horizontalPanelMinHeight
maximumHeight: root.horizontalPanelMaxHeight
tabifyPanel: pianoKeyboardPanel
//! NOTE: hidden by default
visible: false
location: Location.Bottom
dropDestinations: root.horizontalPanelDropDestinations
StyledTextLabel {
anchors.centerIn: parent
text: pianoRollPanel.title
}
},
DockPanel {
id: pianoKeyboardPanel
objectName: pageModel.pianoKeyboardPanelName()
title: qsTrc("appshell", "Piano keyboard")
height: 200
minimumHeight: root.horizontalPanelMinHeight
@ -351,9 +376,10 @@ DockPage {
dropDestinations: root.horizontalPanelDropDestinations
StyledTextLabel {
anchors.centerIn: parent
text: pianoRollPanel.title
PianoKeyboardPanel {
Component.onCompleted: {
pianoKeyboardPanel.contextMenuModel = contextMenuModel
}
}
},

View file

@ -194,8 +194,9 @@ MenuItem* AppMenuModel::makeViewMenu()
makeMenuItem("toggle-navigator"),
makeMenuItem("toggle-timeline"),
makeMenuItem("toggle-mixer"),
// TODO: https://github.com/musescore/MuseScore/issues/9168
// makeMenuItem("toggle-piano"), // need implement
makeMenuItem("toggle-piano-roll"),
makeMenuItem("toggle-piano-keyboard"),
//makeMenuItem("toggle-scorecmp-tool"), // not implemented
makeSeparator(),
makeMenu(qtrc("appshell", "&Toolbars"), makeToolbarsItems(), "menu-toolbars"),
makeMenu(qtrc("appshell", "W&orkspaces"), makeWorkspacesItems(), "menu-workspaces"),

View file

@ -97,16 +97,21 @@ QString NotationPageModel::selectionFiltersPanelName() const
return SELECTION_FILTERS_PANEL_NAME;
}
QString NotationPageModel::pianoPanelName() const
{
return PIANO_PANEL_NAME;
}
QString NotationPageModel::mixerPanelName() const
{
return MIXER_PANEL_NAME;
}
QString NotationPageModel::pianoRollPanelName() const
{
return PIANO_ROLL_PANEL_NAME;
}
QString NotationPageModel::pianoKeyboardPanelName() const
{
return PIANO_KEYBOARD_PANEL_NAME;
}
QString NotationPageModel::timelinePanelName() const
{
return TIMELINE_PANEL_NAME;

View file

@ -61,8 +61,9 @@ public:
Q_INVOKABLE QString inspectorPanelName() const;
Q_INVOKABLE QString selectionFiltersPanelName() const;
Q_INVOKABLE QString pianoPanelName() const;
Q_INVOKABLE QString mixerPanelName() const;
Q_INVOKABLE QString pianoRollPanelName() const;
Q_INVOKABLE QString pianoKeyboardPanelName() const;
Q_INVOKABLE QString timelinePanelName() const;
Q_INVOKABLE QString drumsetPanelName() const;

View file

@ -109,7 +109,7 @@ inline auto take(Map& m, const K& k) -> typename Map::mapped_type
m.erase(it);
return v;
}
typename Map::mapped_type def;
typename Map::mapped_type def {};
return def;
}
}

View file

@ -66,7 +66,7 @@ public:
std::string fontFamily() const override;
void setFontFamily(const std::string& family) override;
int fontSize(FontSizeType type) const override;
int fontSize(FontSizeType type = FontSizeType::BODY) const override;
void setBodyFontSize(int size) override;
async::Notification fontChanged() const override;

View file

@ -65,7 +65,7 @@ public:
virtual std::string fontFamily() const = 0;
virtual void setFontFamily(const std::string& family) = 0;
virtual int fontSize(FontSizeType type) const = 0;
virtual int fontSize(FontSizeType type = FontSizeType::BODY) const = 0;
virtual void setBodyFontSize(int size) = 0;
virtual async::Notification fontChanged() const = 0;

View file

@ -31,7 +31,7 @@ ScrollBar {
property real thickness: 8
property real minimumSizeInPixels: 30
readonly property bool isVertical: orientation === Qt.Vertical && root.policy !== ScrollBar.AlwaysOff
readonly property bool isVertical: orientation === Qt.Vertical
readonly property bool isScrollbarNeeded: size > 0.0 && size < 1.0
visible: isScrollbarNeeded
@ -83,8 +83,7 @@ ScrollBar {
}
}
function setPosition(position) {
root.position = position
function activate() {
root.active = true
Qt.callLater(function() {
root.active = Qt.binding( function() { return root.hovered || root.pressed } )

View file

@ -139,6 +139,14 @@ set(MODULE_SRC
${CMAKE_CURRENT_LIST_DIR}/view/selectionfiltermodel.h
${CMAKE_CURRENT_LIST_DIR}/view/editgridsizedialogmodel.cpp
${CMAKE_CURRENT_LIST_DIR}/view/editgridsizedialogmodel.h
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardtypes.h
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardcontroller.cpp
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardcontroller.h
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardview.cpp
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardview.h
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardpanelcontextmenumodel.cpp
${CMAKE_CURRENT_LIST_DIR}/view/pianokeyboard/pianokeyboardpanelcontextmenumodel.h
${WIDGETS_SRC}
${STYLEDIALOG_SRC}

View file

@ -170,6 +170,9 @@ public:
virtual bool needToShowAddBoxesErrorMessage() const = 0;
virtual void setNeedToShowAddBoxesErrorMessage(bool show) = 0;
virtual ValCh<int> pianoKeyboardNumberOfKeys() const = 0;
virtual void setPianoKeyboardNumberOfKeys(int number) = 0;
};
}

View file

@ -92,6 +92,8 @@ static const Settings::Key NEED_TO_SHOW_ADD_TEXT_ERROR_MESSAGE_KEY(module_name,
static const Settings::Key NEED_TO_SHOW_ADD_FIGURED_BASS_ERROR_MESSAGE_KEY(module_name, "ui/dialogs/needToShowAddFiguredBassErrorMessage");
static const Settings::Key NEED_TO_SHOW_ADD_BOXES_ERROR_MESSAGE_KEY(module_name, "ui/dialogs/needToShowAddBoxesErrorMessage");
static const Settings::Key PIANO_KEYBOARD_NUMBER_OF_KEYS(module_name, "pianoKeyboard/numberOfKeys");
static constexpr int DEFAULT_GRID_SIZE_SPATIUM = 2;
void NotationConfiguration::init()
@ -207,6 +209,12 @@ void NotationConfiguration::init()
settings()->setDefaultValue(NEED_TO_SHOW_ADD_FIGURED_BASS_ERROR_MESSAGE_KEY, Val(true));
settings()->setDefaultValue(NEED_TO_SHOW_ADD_BOXES_ERROR_MESSAGE_KEY, Val(true));
settings()->setDefaultValue(PIANO_KEYBOARD_NUMBER_OF_KEYS, Val(88));
m_pianoKeyboardNumberOfKeys.val = settings()->value(PIANO_KEYBOARD_NUMBER_OF_KEYS).toInt();
settings()->valueChanged(PIANO_KEYBOARD_NUMBER_OF_KEYS).onReceive(this, [this](const Val& val) {
m_pianoKeyboardNumberOfKeys.set(val.toInt());
});
engravingConfiguration()->scoreInversionChanged().onNotify(this, [this]() {
m_foregroundChanged.notify();
});
@ -811,6 +819,16 @@ void NotationConfiguration::setNeedToShowAddBoxesErrorMessage(bool show)
settings()->setSharedValue(NEED_TO_SHOW_ADD_BOXES_ERROR_MESSAGE_KEY, Val(show));
}
ValCh<int> NotationConfiguration::pianoKeyboardNumberOfKeys() const
{
return m_pianoKeyboardNumberOfKeys;
}
void NotationConfiguration::setPianoKeyboardNumberOfKeys(int number)
{
settings()->setSharedValue(PIANO_KEYBOARD_NUMBER_OF_KEYS, Val(number));
}
io::path NotationConfiguration::firstScoreOrderListPath() const
{
return settings()->value(FIRST_SCORE_ORDER_LIST_KEY).toString();

View file

@ -176,6 +176,9 @@ public:
bool needToShowAddBoxesErrorMessage() const override;
void setNeedToShowAddBoxesErrorMessage(bool show) override;
ValCh<int> pianoKeyboardNumberOfKeys() const override;
void setPianoKeyboardNumberOfKeys(int number) override;
private:
io::path firstInstrumentListPath() const;
void setFirstInstrumentListPath(const io::path& path);
@ -198,6 +201,7 @@ private:
async::Notification m_scoreOrderListPathsChanged;
async::Notification m_isLimitCanvasScrollAreaChanged;
async::Notification m_isPlayRepeatsChanged;
ValCh<int> m_pianoKeyboardNumberOfKeys;
};
}

View file

@ -50,6 +50,9 @@
#include "view/selectionfiltermodel.h"
#include "view/editgridsizedialogmodel.h"
#include "view/pianokeyboard/pianokeyboardview.h"
#include "view/pianokeyboard/pianokeyboardpanelcontextmenumodel.h"
#include "ui/iinteractiveuriregister.h"
#include "ui/uitypes.h"
#include "view/widgets/editstyle.h"
@ -186,6 +189,8 @@ void NotationModule::registerUiTypes()
qmlRegisterType<TimelineView>("MuseScore.NotationScene", 1, 0, "TimelineView");
qmlRegisterType<SelectionFilterModel>("MuseScore.NotationScene", 1, 0, "SelectionFilterModel");
qmlRegisterType<EditGridSizeDialogModel>("MuseScore.NotationScene", 1, 0, "EditGridSizeDialogModel");
qmlRegisterType<PianoKeyboardView>("MuseScore.NotationScene", 1, 0, "PianoKeyboardView");
qmlRegisterType<PianoKeyboardPanelContextMenuModel>("MuseScore.NotationScene", 1, 0, "PianoKeyboardPanelContextMenuModel");
qmlRegisterUncreatableType<StyleItem>("MuseScore.NotationScene", 1, 0, "StyleItem", "Cannot create StyleItem from QML");
qmlRegisterType<NotesPageModel>("MuseScore.NotationScene", 1, 0, "NotesPageModel");

View file

@ -33,5 +33,6 @@
<file>qml/MuseScore/NotationScene/internal/CustomiseView.qml</file>
<file>qml/MuseScore/NotationScene/internal/NoteInputBarCustomisePopup.qml</file>
<file>qml/MuseScore/NotationScene/internal/NotationScrollBar.qml</file>
<file>qml/MuseScore/NotationScene/PianoKeyboardPanel.qml</file>
</qresource>
</RCC>

View file

@ -0,0 +1,88 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.15
import MuseScore.NotationScene 1.0
import MuseScore.UiComponents 1.0
Item {
id: root
property alias contextMenuModel: contextMenuModel
PianoKeyboardPanelContextMenuModel {
id: contextMenuModel
keyWidthScaling: keyboardView.keyWidthScaling
onSetKeyWidthScalingRequested: function(scaling) {
keyboardView.keyWidthScaling = scaling
}
}
Component.onCompleted: {
contextMenuModel.load()
}
PinchArea {
anchors.fill: parent
onPinchUpdated: function(pinch) {
keyboardView.scale(pinch.scale / pinch.previousScale, pinch.center.x)
}
PianoKeyboardView {
id: keyboardView
anchors.fill: parent
numberOfKeys: contextMenuModel.numberOfKeys
//! NOTE: bidirectional binding; C++ code should guard against loops
scrollBarPosition: scrollBar.position
StyledScrollBar {
id: scrollBar
orientation: Qt.Horizontal
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: "black"
border.color: "white"
border.width: 1
position: keyboardView.scrollBarPosition
size: keyboardView.scrollBarSize
onPositionChanged: {
activate()
}
onSizeChanged: {
activate()
}
}
}
}
}

View file

@ -11,3 +11,4 @@ UndoRedoToolBar 1.0 UndoRedoToolBar.qml
Timeline 1.0 Timeline.qml
SelectionFilterPanel 1.0 SelectionFilterPanel.qml
NotationScrollAndZoomArea 1.0 NotationScrollAndZoomArea.qml
PianoKeyboardPanel 1.0 PianoKeyboardPanel.qml

View file

@ -0,0 +1,161 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "pianokeyboardcontroller.h"
#include "defer.h"
#include "log.h"
using namespace mu::notation;
using namespace mu::midi;
void PianoKeyboardController::init()
{
onNotationChanged();
context()->currentNotationChanged().onNotify(this, [this]() {
onNotationChanged();
});
}
KeyState PianoKeyboardController::keyState(piano_key_t key) const
{
if (m_pressedKey == key) {
return KeyState::Played;
}
if (m_keysInSelection.find(key) != m_keysInSelection.cend()) {
return KeyState::Selected;
}
if (m_otherNotesInSelectedChord.find(key) != m_otherNotesInSelectedChord.cend()) {
return KeyState::OtherInSelectedChord;
}
return KeyState::None;
}
mu::async::Notification PianoKeyboardController::keyStatesChanged() const
{
return m_keyStatesChanged;
}
std::optional<piano_key_t> PianoKeyboardController::pressedKey() const
{
return m_pressedKey;
}
void PianoKeyboardController::setPressedKey(std::optional<piano_key_t> key)
{
if (m_pressedKey == key) {
return;
}
if (m_pressedKey.has_value()) {
sendNoteOff(m_pressedKey.value());
}
if (key.has_value()) {
sendNoteOn(key.value());
}
m_pressedKey = key;
m_keyStatesChanged.notify();
}
void PianoKeyboardController::onNotationChanged()
{
updateSelectedKeys();
if (auto notation = currentNotation()) {
notation->interaction()->selectionChanged().onNotify(this, [this]() {
updateSelectedKeys();
});
}
}
void PianoKeyboardController::updateSelectedKeys()
{
std::unordered_set<piano_key_t> newKeysInSelection;
std::unordered_set<piano_key_t> newOtherNotesInSelectedChord;
DEFER {
if (newKeysInSelection != m_keysInSelection
|| newOtherNotesInSelectedChord != m_otherNotesInSelectedChord) {
m_keysInSelection = newKeysInSelection;
m_otherNotesInSelectedChord = newOtherNotesInSelectedChord;
m_keyStatesChanged.notify();
}
};
auto notation = currentNotation();
if (!notation) {
return;
}
auto selection = notation->interaction()->selection();
if (selection->isNone()) {
return;
}
for (const Ms::Note* note : selection->notes()) {
newKeysInSelection.insert(static_cast<piano_key_t>(note->epitch()));
for (const Ms::Note* otherNote : note->chord()->notes()) {
newOtherNotesInSelectedChord.insert(static_cast<piano_key_t>(otherNote->epitch()));
}
}
}
void PianoKeyboardController::sendNoteOn(piano_key_t key)
{
auto notation = currentNotation();
if (!notation) {
return;
}
Event ev;
ev.setMessageType(Event::MessageType::ChannelVoice10);
ev.setOpcode(Event::Opcode::NoteOn);
ev.setNote(key);
ev.setVelocity(80);
notation->midiInput()->onMidiEventReceived(ev);
}
void PianoKeyboardController::sendNoteOff(piano_key_t key)
{
auto notation = currentNotation();
if (!notation) {
return;
}
Event ev;
ev.setMessageType(Event::MessageType::ChannelVoice10);
ev.setOpcode(Event::Opcode::NoteOff);
ev.setNote(key);
notation->midiInput()->onMidiEventReceived(ev);
}
INotationPtr PianoKeyboardController::currentNotation() const
{
return context()->currentNotation();
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MU_NOTATION_PIANOKEYBOARDCONTROLLER_H
#define MU_NOTATION_PIANOKEYBOARDCONTROLLER_H
#include "async/asyncable.h"
#include "modularity/ioc.h"
#include "context/iglobalcontext.h"
#include "pianokeyboardtypes.h"
namespace mu::notation {
class PianoKeyboardController : public async::Asyncable
{
INJECT(notation, context::IGlobalContext, context)
public:
PianoKeyboardController() = default;
void init();
std::optional<piano_key_t> pressedKey() const;
void setPressedKey(std::optional<piano_key_t> key);
KeyState keyState(piano_key_t key) const;
async::Notification keyStatesChanged() const;
private:
INotationPtr currentNotation() const;
void onNotationChanged();
void updateSelectedKeys();
void sendNoteOn(piano_key_t key);
void sendNoteOff(piano_key_t key);
std::optional<piano_key_t> m_pressedKey = std::nullopt;
std::unordered_set<piano_key_t> m_keysInSelection;
std::unordered_set<piano_key_t> m_otherNotesInSelectedChord;
async::Notification m_keyStatesChanged;
};
}
#endif // MU_NOTATION_PIANOKEYBOARDCONTROLLER_H

View file

@ -0,0 +1,188 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "pianokeyboardpanelcontextmenumodel.h"
#include "actions/actiontypes.h"
#include "pianokeyboardtypes.h"
#include "log.h"
using namespace mu::notation;
using namespace mu::actions;
using namespace mu::ui;
using namespace mu::uicomponents;
static const ActionCode SET_KEY_WIDTH_SCALING_CODE("piano-keyboard-set-key-width-scaling");
static const ActionCode SET_NUMBER_OF_KEYS_CODE("piano-keyboard-set-number-of-keys");
PianoKeyboardPanelContextMenuModel::PianoKeyboardPanelContextMenuModel(QObject* parent)
: AbstractMenuModel(parent)
{
}
void PianoKeyboardPanelContextMenuModel::load()
{
MenuItemList items {
makeViewMenu()
};
setItems(items);
}
qreal PianoKeyboardPanelContextMenuModel::keyWidthScaling() const
{
return m_currentKeyWidthScaling;
}
void PianoKeyboardPanelContextMenuModel::setKeyWidthScaling(qreal scaling)
{
if (qFuzzyCompare(m_currentKeyWidthScaling, scaling)) {
return;
}
m_currentKeyWidthScaling = scaling;
updateKeyWidthScalingItems();
emit keyWidthScalingChanged();
}
int PianoKeyboardPanelContextMenuModel::numberOfKeys() const
{
return configuration()->pianoKeyboardNumberOfKeys().val;
}
MenuItem* PianoKeyboardPanelContextMenuModel::makeViewMenu()
{
MenuItemList items;
std::vector<std::pair<QString, qreal> > possibleKeyWidthScalings {
{ qtrc("notation", "Large"), LARGE_KEY_WIDTH_SCALING },
{ qtrc("notation", "Normal"), NORMAL_KEY_WIDTH_SCALING },
{ qtrc("notation", "Small"), SMALL_KEY_WIDTH_SCALING },
};
for (auto [title, scaling] : possibleKeyWidthScalings) {
items << makeKeyWidthScalingItem(title, scaling);
}
updateKeyWidthScalingItems();
dispatcher()->reg(this, SET_KEY_WIDTH_SCALING_CODE, [this](const ActionData& args) {
IF_ASSERT_FAILED(args.count() > 0) {
return;
}
emit setKeyWidthScalingRequested(args.arg<qreal>(0));
});
items << makeSeparator();
std::vector<std::pair<QString, int> > possibleNumbersOfKeys {
{ qtrc("notation", "128 notes (full)"), 128 },
{ qtrc("notation", "88 notes (piano)"), 88 },
{ qtrc("notation", "61 notes"), 61 },
{ qtrc("notation", "49 notes"), 49 },
{ qtrc("notation", "25 notes"), 25 },
};
for (auto [title, numberOfKeys] : possibleNumbersOfKeys) {
items << makeNumberOfKeysItem(title, numberOfKeys);
}
configuration()->pianoKeyboardNumberOfKeys().ch.onReceive(this, [this](int) {
emit numberOfKeysChanged();
});
dispatcher()->reg(this, SET_NUMBER_OF_KEYS_CODE, [this](const ActionData& args) {
IF_ASSERT_FAILED(args.count() > 0) {
return;
}
configuration()->setPianoKeyboardNumberOfKeys(args.arg<int>(0));
});
return makeMenu(qtrc("notation", "View"), items);
}
MenuItem* PianoKeyboardPanelContextMenuModel::makeKeyWidthScalingItem(const QString& title, qreal scaling)
{
UiAction action;
action.title = title;
action.code = SET_KEY_WIDTH_SCALING_CODE;
action.checkable = Checkable::Yes;
MenuItem* item = new MenuItem(action, this);
item->setId(QString::number(scaling));
item->setState(UiActionState::make_enabled());
item->setArgs(ActionData::make_arg1<qreal>(scaling));
m_keyWidthScalingItems << item;
return item;
}
MenuItem* PianoKeyboardPanelContextMenuModel::makeNumberOfKeysItem(const QString& title, int numberOfKeys)
{
UiAction action;
action.title = title;
action.code = SET_NUMBER_OF_KEYS_CODE;
action.checkable = Checkable::Yes;
MenuItem* item = new MenuItem(action, this);
item->setId(QString::number(numberOfKeys));
ValCh<int> currentNumberOfKeys = configuration()->pianoKeyboardNumberOfKeys();
bool checked = numberOfKeys == currentNumberOfKeys.val;
item->setState(UiActionState::make_enabled(checked));
currentNumberOfKeys.ch.onReceive(item, [item, numberOfKeys](int num) {
bool checked = numberOfKeys == num;
item->setState(UiActionState::make_enabled(checked));
});
item->setArgs(ActionData::make_arg1<int>(numberOfKeys));
return item;
}
void PianoKeyboardPanelContextMenuModel::updateKeyWidthScalingItems()
{
qreal roundedCurrentScaling = NORMAL_KEY_WIDTH_SCALING;
constexpr qreal betweenNormalAndSmall = (NORMAL_KEY_WIDTH_SCALING + SMALL_KEY_WIDTH_SCALING) / 2;
constexpr qreal betweenNormalAndLarge = (NORMAL_KEY_WIDTH_SCALING + LARGE_KEY_WIDTH_SCALING) / 2;
if (m_currentKeyWidthScaling < betweenNormalAndSmall) {
roundedCurrentScaling = SMALL_KEY_WIDTH_SCALING;
} else if (m_currentKeyWidthScaling > betweenNormalAndLarge) {
roundedCurrentScaling = LARGE_KEY_WIDTH_SCALING;
}
for (MenuItem* item : m_keyWidthScalingItems) {
IF_ASSERT_FAILED(item->args().count() > 0) {
continue;
}
bool checked = qFuzzyCompare(item->args().arg<qreal>(0), roundedCurrentScaling);
item->setState(UiActionState::make_enabled(checked));
}
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MU_NOTATION_PIANOKEYBOARDPANELCONTEXTMENUMODEL_H
#define MU_NOTATION_PIANOKEYBOARDPANELCONTEXTMENUMODEL_H
#include "uicomponents/view/abstractmenumodel.h"
#include "actions/actionable.h"
#include "modularity/ioc.h"
#include "inotationconfiguration.h"
#include "actions/iactionsdispatcher.h"
namespace mu::notation {
class PianoKeyboardPanelContextMenuModel : public uicomponents::AbstractMenuModel, public actions::Actionable
{
Q_OBJECT
INJECT(notation, INotationConfiguration, configuration)
INJECT(notation, actions::IActionsDispatcher, dispatcher)
Q_PROPERTY(int numberOfKeys READ numberOfKeys NOTIFY numberOfKeysChanged)
Q_PROPERTY(qreal keyWidthScaling READ keyWidthScaling WRITE setKeyWidthScaling NOTIFY keyWidthScalingChanged)
public:
explicit PianoKeyboardPanelContextMenuModel(QObject* parent = nullptr);
Q_INVOKABLE void load() override;
qreal keyWidthScaling() const;
void setKeyWidthScaling(qreal scaling);
int numberOfKeys() const;
signals:
void keyWidthScalingChanged();
void setKeyWidthScalingRequested(qreal scaling);
void numberOfKeysChanged();
private:
uicomponents::MenuItem* makeViewMenu();
uicomponents::MenuItem* makeKeyWidthScalingItem(const QString& title, qreal scaling);
uicomponents::MenuItem* makeNumberOfKeysItem(const QString& title, int numberOfKeys);
void updateKeyWidthScalingItems();
uicomponents::MenuItemList m_keyWidthScalingItems;
qreal m_currentKeyWidthScaling = 0.0;
};
}
#endif // MU_NOTATION_PIANOKEYBOARDPANELCONTEXTMENUMODEL_H

View file

@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MU_NOTATION_PIANOKEYBOARDTYPES_H
#define MU_NOTATION_PIANOKEYBOARDTYPES_H
namespace mu::notation {
using piano_key_t = uint8_t;
static constexpr qreal SMALL_KEY_WIDTH_SCALING = 0.5;
static constexpr qreal NORMAL_KEY_WIDTH_SCALING = 1.0;
static constexpr qreal LARGE_KEY_WIDTH_SCALING = 2.0;
enum class KeyState {
None,
OtherInSelectedChord,
Selected,
Played
};
}
#endif // MU_NOTATION_PIANOKEYBOARDTYPES_H

View file

@ -0,0 +1,512 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "pianokeyboardview.h"
#include <QPainter>
#include "pianokeyboardcontroller.h"
#include "log.h"
using namespace mu::notation;
static const QColor backgroundColor(36, 36, 39);
static constexpr bool isBlackKey(piano_key_t key)
{
constexpr bool isBlack[12] { false, true, false, true, false, false, true, false, true, false, true, false };
return isBlack[key % 12];
}
static qreal abs2d(qreal x, qreal y)
{
return sqrt(x * x + y * y) * (y > -x ? 1 : -1);
}
static QColor mixedColors(QColor background, QColor foreground, qreal opacity)
{
QColor result;
result.setRed(opacity * foreground.red() + (1 - opacity) * background.red());
result.setGreen(opacity * foreground.green() + (1 - opacity) * background.green());
result.setBlue(opacity * foreground.blue() + (1 - opacity) * background.blue());
return result;
}
PianoKeyboardView::PianoKeyboardView(QQuickItem* parent)
: QQuickPaintedItem(parent), m_controller(new PianoKeyboardController())
{
calculateKeyRects();
connect(this, &QQuickItem::widthChanged, this, &PianoKeyboardView::calculateKeyRects);
connect(this, &QQuickItem::heightChanged, this, &PianoKeyboardView::calculateKeyRects);
uiConfiguration()->fontChanged().onNotify(this, [this]() {
determineOctaveLabelsFont();
update();
});
updateKeyStateColors();
uiConfiguration()->currentThemeChanged().onNotify(this, [this]() {
updateKeyStateColors();
update();
});
m_controller->init();
m_controller->keyStatesChanged().onNotify(this, [this]() {
update();
});
setAcceptedMouseButtons(Qt::LeftButton);
}
PianoKeyboardView::~PianoKeyboardView()
{
delete m_controller;
}
void PianoKeyboardView::calculateKeyRects()
{
TRACEFUNC;
determineOctaveLabelsFont();
m_blackKeyRects.clear();
m_whiteKeyRects.clear();
constexpr qreal defaultSpacing = 2.0;
constexpr qreal maxSpacing = 2.0;
m_spacing = std::min(defaultSpacing * m_keyWidthScaling, maxSpacing);
constexpr qreal defaultWhiteKeyWidth = 30.0;
constexpr qreal defaultBlackKeyWidth = 20.0;
qreal whiteKeyWidth = defaultWhiteKeyWidth * m_keyWidthScaling + m_spacing;
qreal blackKeyWidth = defaultBlackKeyWidth * m_keyWidthScaling;
constexpr qreal defaultWhiteKeyHeight = 128.0;
constexpr qreal defaultBlackKeyHeight = 84.0;
m_whiteKeyHeight = std::min(height(), defaultWhiteKeyHeight * m_keyWidthScaling + m_spacing * 2);
qreal actualKeyHeightScaling = (m_whiteKeyHeight - m_spacing * 2) / defaultWhiteKeyHeight;
m_blackKeyHeight = defaultBlackKeyHeight * actualKeyHeightScaling + m_spacing;
qreal hPos = m_spacing / 2;
piano_key_t numKeys = std::min<piano_key_t>(m_lowestKey + m_numberOfKeys, MAX_NUM_KEYS);
for (piano_key_t key = m_lowestKey; key < numKeys; ++key) {
if (isBlackKey(key)) {
constexpr qreal offsets[12] {
0.0, -13.0, 0.0, -7.0, 0.0, 0.0, -13.0, 0.0, -10.0, 0.0, -7.0, 0.0
};
qreal offset = offsets[key % 12] * m_keyWidthScaling;
m_blackKeyRects[key] = QRectF(hPos + offset, 0.0, blackKeyWidth, m_blackKeyHeight);
} else {
m_whiteKeyRects[key] = QRectF(hPos, 0.0, whiteKeyWidth, m_whiteKeyHeight);
hPos += whiteKeyWidth;
}
}
m_keysAreaRect.setSize(QSizeF(hPos + m_spacing / 2, m_whiteKeyHeight));
adjustKeysAreaPosition();
}
void PianoKeyboardView::adjustKeysAreaPosition()
{
TRACEFUNC;
qreal keysAreaTop = (height() - m_keysAreaRect.height()) / 2;
qreal minScrollOffset = std::min(width() - m_keysAreaRect.width(), (width() - m_keysAreaRect.width()) / 2);
qreal maxScrollOffset = std::max(0.0, (width() - m_keysAreaRect.width()) / 2);
m_scrollOffset = std::clamp(m_scrollOffset, minScrollOffset, maxScrollOffset);
m_keysAreaRect.moveTo(QPointF(m_scrollOffset, keysAreaTop));
updateScrollBar();
}
void PianoKeyboardView::determineOctaveLabelsFont()
{
m_octaveLabelsFont.setFamily(QString::fromStdString(uiConfiguration()->fontFamily()));
m_octaveLabelsFont.setPixelSize(uiConfiguration()->fontSize() * std::min(m_keyWidthScaling, 1.5));
}
void PianoKeyboardView::updateKeyStateColors()
{
auto themeValues = uiConfiguration()->currentTheme().values;
QColor accentColor = themeValues[ui::ACCENT_COLOR].toString();
m_whiteKeyStateColors[KeyState::None] = Qt::white;
m_whiteKeyStateColors[KeyState::OtherInSelectedChord] = mixedColors(Qt::white, accentColor, 0.25);
m_whiteKeyStateColors[KeyState::Selected] = mixedColors(Qt::white, accentColor, 0.5);
m_whiteKeyStateColors[KeyState::Played] = mixedColors(Qt::white, accentColor, 0.8);
QColor blackKeysBaseColor(78, 78, 78);
m_blackKeyStateColors[KeyState::None] = blackKeysBaseColor;
m_blackKeyStateColors[KeyState::OtherInSelectedChord] = mixedColors(blackKeysBaseColor, accentColor, 0.4);
m_blackKeyStateColors[KeyState::Selected] = mixedColors(blackKeysBaseColor, accentColor, 0.8);
m_blackKeyStateColors[KeyState::Played] = mixedColors(blackKeysBaseColor, accentColor, 1.0);
}
void PianoKeyboardView::paint(QPainter* painter)
{
TRACEFUNC;
painter->setRenderHint(QPainter::Antialiasing);
paintBackground(painter);
QPointF pos = m_keysAreaRect.topLeft();
painter->translate(pos);
QRectF viewport = QRectF(0.0, 0.0, width(), height()).translated(-pos);
paintWhiteKeys(painter, viewport);
paintBlackKeys(painter, viewport);
}
void PianoKeyboardView::paintBackground(QPainter* painter)
{
painter->fillRect(m_keysAreaRect, backgroundColor);
}
void PianoKeyboardView::paintWhiteKeys(QPainter* painter, const QRectF& viewport)
{
QPainterPath path;
for (auto [key, rect] : m_whiteKeyRects) {
if (!viewport.intersects(rect)) {
continue;
}
qreal inset = m_spacing / 2;
qreal left = inset, top = m_spacing,
right = rect.width() - inset, bottom = rect.height() - m_spacing;
if (path.isEmpty()) {
qreal cornerRadius = 2.0 * m_keyWidthScaling;
path.moveTo(left, top);
path.lineTo(left, bottom - cornerRadius);
path.quadTo(left, bottom, left + cornerRadius, bottom);
path.lineTo(right - cornerRadius, bottom);
path.quadTo(right, bottom, right, bottom - cornerRadius);
path.lineTo(right, top);
path.closeSubpath();
}
painter->translate(rect.topLeft());
QColor fillColor = m_whiteKeyStateColors[m_controller->keyState(key)];
painter->fillPath(path, fillColor);
if (key % 12 == 0) {
// Draw octave label
qreal availableHeight = m_whiteKeyHeight - m_blackKeyHeight;
qreal requiredHeight = m_octaveLabelsFont.pixelSize();
qreal bottomOffsetToCenterTextInAvailableSpace = (availableHeight - requiredHeight) / 2;
constexpr qreal maxBottomOffset = 8.0;
constexpr qreal defaultBottomOffset = 8.0;
qreal bottomOffset = std::min({
bottomOffsetToCenterTextInAvailableSpace,
defaultBottomOffset * m_keyWidthScaling,
maxBottomOffset
});
int octaveNumber = (key / 12) - 1;
QString octaveLabel = "C" + QString::number(octaveNumber);
QRect octaveLabelRect(left, top, right - left, bottom - top - bottomOffset);
painter->setPen(backgroundColor);
painter->setFont(m_octaveLabelsFont);
painter->drawText(octaveLabelRect, Qt::AlignHCenter | Qt::AlignBottom, octaveLabel);
}
painter->translate(-rect.topLeft());
}
}
void PianoKeyboardView::paintBlackKeys(QPainter* painter, const QRectF& viewport)
{
QLinearGradient topPieceGradient;
QLinearGradient bottomPieceGradient;
QRectF backgroundRect;
QPainterPath topPiecePath;
QPainterPath bottomPiecePath;
for (auto [key, rect] : m_blackKeyRects) {
if (!viewport.intersects(rect)) {
continue;
}
if (backgroundRect.isEmpty()) {
qreal blackKeyInset = 2.0 * m_keyWidthScaling;
qreal cornerRadius = blackKeyInset;
qreal bottomPieceHeight = 8.0 * m_keyWidthScaling;
// Make background rect
// Adjust the top, to make sure that the rect fully covers the white keys,
// but does not extend beyond the background of the whole keyboard (which
// theoretically shouldn't happen, but in practice does, probably due to
// antialiasing)
backgroundRect.setSize(rect.size());
backgroundRect.setTop(0.5 * m_spacing);
// Make top piece
qreal top = m_spacing + 0.5 * blackKeyInset,
left = blackKeyInset,
right = rect.width() - blackKeyInset,
bottom = rect.height() - blackKeyInset - bottomPieceHeight - 0.5 * blackKeyInset;
qreal center = rect.width() / 2;
topPiecePath.moveTo(left, top);
topPiecePath.lineTo(left, bottom - cornerRadius);
topPiecePath.quadTo(left, bottom, center, bottom);
topPiecePath.quadTo(right, bottom, right, bottom - cornerRadius);
topPiecePath.lineTo(right, top);
topPiecePath.closeSubpath();
topPieceGradient.setColorAt(0.0, backgroundColor);
topPieceGradient.setStart(0.0, top);
topPieceGradient.setFinalStop(0.0, bottom);
// Make bottom piece
top = rect.height() - blackKeyInset - bottomPieceHeight;
bottom = rect.height() - blackKeyInset;
bottomPiecePath.moveTo(left, top + cornerRadius);
bottomPiecePath.quadTo(left, top, center, top);
bottomPiecePath.quadTo(right, top, right, top + cornerRadius);
bottomPiecePath.lineTo(right, bottom);
bottomPiecePath.lineTo(left, bottom);
bottomPiecePath.closeSubpath();
bottomPieceGradient.setColorAt(1.0, backgroundColor);
bottomPieceGradient.setStart(0.0, top);
bottomPieceGradient.setFinalStop(0.0, bottom);
}
QColor highlightColor = m_blackKeyStateColors[m_controller->keyState(key)];
topPieceGradient.setColorAt(1.0, highlightColor);
bottomPieceGradient.setColorAt(0.0, highlightColor);
painter->translate(rect.topLeft());
painter->fillRect(backgroundRect, backgroundColor);
painter->fillPath(topPiecePath, topPieceGradient);
painter->fillPath(bottomPiecePath, bottomPieceGradient);
painter->translate(-rect.topLeft());
}
}
int PianoKeyboardView::numberOfKeys() const
{
return m_numberOfKeys;
}
void PianoKeyboardView::setNumberOfKeys(int number)
{
if (m_numberOfKeys == number) {
return;
}
switch (number) {
case 128:
m_lowestKey = 0;
m_numberOfKeys = 128;
break;
case 88:
m_lowestKey = 21;
m_numberOfKeys = 88;
break;
case 61:
m_lowestKey = 36;
m_numberOfKeys = 61;
break;
case 49:
m_lowestKey = 36;
m_numberOfKeys = 49;
break;
case 25:
m_lowestKey = 48;
m_numberOfKeys = 25;
break;
default:
return;
}
emit numberOfKeysChanged();
calculateKeyRects();
update();
}
void PianoKeyboardView::moveCanvas(qreal dx)
{
setScrollOffset(m_scrollOffset + dx);
}
void PianoKeyboardView::setScrollOffset(qreal offset)
{
qreal oldScrollOffset = m_scrollOffset;
m_scrollOffset = offset;
adjustKeysAreaPosition();
if (qFuzzyCompare(m_scrollOffset, oldScrollOffset)) {
return;
}
update();
}
qreal PianoKeyboardView::keyWidthScaling() const
{
return m_keyWidthScaling;
}
void PianoKeyboardView::scale(qreal factor, qreal x)
{
setScaling(m_keyWidthScaling * factor, x);
}
void PianoKeyboardView::setScaling(qreal scaling, qreal x)
{
qreal newScaling = std::clamp(scaling, SMALL_KEY_WIDTH_SCALING, LARGE_KEY_WIDTH_SCALING);
qreal correctedFactor = newScaling / m_keyWidthScaling;
if (qFuzzyCompare(correctedFactor, 1.0)) {
return;
}
m_keyWidthScaling = newScaling;
m_scrollOffset *= correctedFactor;
m_scrollOffset += x * (1 - correctedFactor);
emit keyWidthScalingChanged();
calculateKeyRects();
update();
}
qreal PianoKeyboardView::scrollBarPosition() const
{
return m_scrollBarPosition;
}
qreal PianoKeyboardView::scrollBarSize() const
{
return m_scrollBarSize;
}
void PianoKeyboardView::updateScrollBar()
{
qreal newPosition = -m_scrollOffset / m_keysAreaRect.width();
qreal newSize = width() / m_keysAreaRect.width();
if (qFuzzyCompare(newPosition, m_scrollBarPosition)
&& qFuzzyCompare(newSize, m_scrollBarSize)) {
return;
}
m_scrollBarPosition = newPosition;
m_scrollBarSize = newSize;
emit scrollBarChanged();
}
void PianoKeyboardView::setScrollBarPosition(qreal position)
{
setScrollOffset(-position * m_keysAreaRect.width());
}
std::optional<piano_key_t> PianoKeyboardView::keyAt(const QPointF& position) const
{
QPointF correctedPosition = position - m_keysAreaRect.topLeft();
for (auto [key, rect] : m_blackKeyRects) {
if (rect.contains(correctedPosition)) {
return key;
}
}
for (auto [key, rect] : m_whiteKeyRects) {
if (rect.contains(correctedPosition)) {
return key;
}
}
return std::nullopt;
}
void PianoKeyboardView::wheelEvent(QWheelEvent* event)
{
QPoint delta = event->pixelDelta();
if (delta.isNull()) {
delta = event->angleDelta();
}
Qt::KeyboardModifiers keyState = event->modifiers();
// Windows touch pad pinches also execute this
if (keyState & Qt::ControlModifier) {
// For every pixel being scrolled, zoom by a factor or 1.002 (value is found empirically)
constexpr qreal zoomSpeed = 1.002;
qreal abs = abs2d(delta.x(), delta.y());
qreal factor = pow(zoomSpeed, abs);
scale(factor, event->position().x());
return;
}
if (delta.x() == 0) {
// Make life easy for people who can only scroll vertically
moveCanvas(delta.y());
} else if (keyState & Qt::ShiftModifier) {
qreal abs = abs2d(delta.x(), delta.y());
moveCanvas(abs);
} else {
moveCanvas(delta.x());
}
}
void PianoKeyboardView::mousePressEvent(QMouseEvent* event)
{
m_controller->setPressedKey(keyAt(event->pos()));
}
void PianoKeyboardView::mouseMoveEvent(QMouseEvent* event)
{
m_controller->setPressedKey(keyAt(event->pos()));
}
void PianoKeyboardView::mouseReleaseEvent(QMouseEvent*)
{
m_controller->setPressedKey(std::nullopt);
}

View file

@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2022 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MU_NOTATION_PIANOKEYBOARDVIEW_H
#define MU_NOTATION_PIANOKEYBOARDVIEW_H
#include <QQuickPaintedItem>
#include "async/asyncable.h"
#include "modularity/ioc.h"
#include "inotationconfiguration.h"
#include "ui/iuiconfiguration.h"
#include "pianokeyboardtypes.h"
namespace mu::notation {
class PianoKeyboardController;
class PianoKeyboardView : public QQuickPaintedItem, public async::Asyncable
{
Q_OBJECT
INJECT(notation, INotationConfiguration, configuration)
INJECT(notation, ui::IUiConfiguration, uiConfiguration)
Q_PROPERTY(int numberOfKeys READ numberOfKeys WRITE setNumberOfKeys NOTIFY numberOfKeysChanged)
Q_PROPERTY(qreal keyWidthScaling READ keyWidthScaling WRITE setScaling NOTIFY keyWidthScalingChanged)
Q_PROPERTY(qreal scrollBarPosition READ scrollBarPosition WRITE setScrollBarPosition NOTIFY scrollBarChanged)
Q_PROPERTY(qreal scrollBarSize READ scrollBarSize NOTIFY scrollBarChanged)
public:
explicit PianoKeyboardView(QQuickItem* parent = nullptr);
~PianoKeyboardView() override;
void paint(QPainter* painter) override;
int numberOfKeys() const;
void setNumberOfKeys(int number);
qreal keyWidthScaling() const;
Q_INVOKABLE void scale(qreal factor, qreal x);
void setScaling(qreal scaling, qreal x = 0.0);
qreal scrollBarPosition() const;
qreal scrollBarSize() const;
void setScrollBarPosition(qreal position);
signals:
void numberOfKeysChanged();
void keyWidthScalingChanged();
void scrollBarChanged();
private:
void calculateKeyRects();
void adjustKeysAreaPosition();
void determineOctaveLabelsFont();
void updateKeyStateColors();
void paintBackground(QPainter* painter);
void paintWhiteKeys(QPainter* painter, const QRectF& viewport);
void paintBlackKeys(QPainter* painter, const QRectF& viewport);
void moveCanvas(qreal dx);
void setScrollOffset(qreal offset);
void updateScrollBar();
void wheelEvent(QWheelEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
std::optional<piano_key_t> keyAt(const QPointF& position) const;
static constexpr piano_key_t MIN_KEY = 0;
static constexpr piano_key_t MAX_NUM_KEYS = 128;
piano_key_t m_lowestKey = MIN_KEY;
piano_key_t m_numberOfKeys = MAX_NUM_KEYS;
PianoKeyboardController* m_controller = nullptr;
QRectF m_keysAreaRect;
std::map<piano_key_t, QRectF> m_blackKeyRects;
std::map<piano_key_t, QRectF> m_whiteKeyRects;
QFont m_octaveLabelsFont;
std::map<KeyState, QColor> m_whiteKeyStateColors;
std::map<KeyState, QColor> m_blackKeyStateColors;
qreal m_keyWidthScaling = 1.0;
qreal m_scrollOffset = 0.0;
qreal m_spacing = 0.0;
qreal m_whiteKeyHeight = 0.0;
qreal m_blackKeyHeight = 0.0;
qreal m_scrollBarPosition = 0.0;
qreal m_scrollBarSize = 0.0;
};
}
#endif // MU_NOTATION_PIANOKEYBOARDVIEW_H

View file

@ -46,8 +46,6 @@ signals:
void expandCollapseAllRequested(bool expand);
private:
void buildMenu();
uicomponents::MenuItem* createIsSingleClickToOpenPaletteItem();
uicomponents::MenuItem* createIsSinglePaletteItem();
uicomponents::MenuItem* createExpandCollapseAllItem(bool expand);