From 07af38321157b8c7463c29e1fd57e652aaa3a52a Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Thu, 26 Jun 2025 10:24:48 -0400 Subject: [PATCH] listen for midi binding control range. --- src/AudioHost.cpp | 32 +- src/AudioHost.hpp | 2 +- src/Lv2Pedalboard.cpp | 2 +- src/MidiBinding.cpp | 2 + src/MidiBinding.hpp | 21 +- src/Pedalboard.cpp | 10 + src/Pedalboard.hpp | 4 + src/PiPedalModel.cpp | 49 ++- src/PiPedalModel.hpp | 9 +- src/PiPedalSocket.cpp | 44 +- src/RingBufferReader.hpp | 28 +- todo.txt | 10 - vite/src/pipedal/MidiBinding.tsx | 8 + vite/src/pipedal/MidiBindingView.tsx | 377 +++++++++++++++--- vite/src/pipedal/MidiBindingsDialog.tsx | 98 +---- vite/src/pipedal/NumericInput.tsx | 60 ++- vite/src/pipedal/PiPedalModel.tsx | 54 ++- vite/src/pipedal/SystemMidiBindingView.tsx | 199 +-------- vite/src/pipedal/SystemMidiBindingsDialog.tsx | 67 +--- vite/src/pipedal/ToolTipEx.tsx | 37 +- 20 files changed, 591 insertions(+), 522 deletions(-) diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 8270400..047237b 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -294,12 +294,14 @@ class SystemMidiBinding private: MidiBinding currentBinding; bool controlState = false; + uint8_t lastControlValue = 0; public: void SetBinding(const MidiBinding &binding) { currentBinding = binding; controlState = false; + lastControlValue = 0; } bool IsMatch(const MidiEvent &event); @@ -335,13 +337,18 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event) return false; if (event.buffer[1] != currentBinding.control()) return false; - bool state = event.buffer[2] >= 0x64; - if (state != this->controlState) + + uint8_t value = event.buffer[2]; + bool result = false; + if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_RISING_EDGE) { - this->controlState = state; - return state; + result = value >= 0x64 && lastControlValue < 0x64; + } else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY) + { + result = true; } - return false; + lastControlValue = value; + return result; } break; default: @@ -918,11 +925,12 @@ private: if (event.size >= 3) { uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0); - bool isNote = cmd == 0x90; + bool isNote = cmd == 0x90 && event.buffer[2] != 0; // note on with velocity > 0. bool isControl = cmd == 0xB0; if (isNote || isControl) { - realtimeWriter.OnMidiListen(isNote, event.buffer[1]); + MidiNotifyBody notifyBody (event.buffer[0],event.buffer[1], event.buffer[2]); + realtimeWriter.OnMidiListen(notifyBody); } } } @@ -1333,11 +1341,11 @@ public: { if (command == RingBufferCommand::OnMidiListen) { - uint16_t msg; - hostReader.read(&msg); + MidiNotifyBody body; + hostReader.read(&body); if (this->pNotifyCallbacks) { - pNotifyCallbacks->OnNotifyMidiListen((msg & 0xFF00) != 0, (uint8_t)msg); + pNotifyCallbacks->OnNotifyMidiListen(body.cc0_, body.cc1_, body.cc2_); } } else if (command == RingBufferCommand::MidiValueChanged) @@ -2080,8 +2088,8 @@ public: return result; } - volatile bool listenForMidiEvent = false; - volatile bool listenForAtomOutput = false; + std::atomic listenForMidiEvent = false; + std::atomic listenForAtomOutput = false; virtual void SetListenForMidiEvent(bool listen) { diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 6868520..4fbf8a7 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -158,7 +158,7 @@ namespace pipedal virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0; - virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) = 0; + virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) = 0; virtual void OnNotifyPathPatchPropertyReceived( int64_t instanceId, diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 3220bcf..5fa82e0 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -824,7 +824,7 @@ void Lv2Pedalboard::OnMidiMessage(size_t size, uint8_t *message, case MidiControlType::Dial: { IEffect *pEffect = this->realtimeEffects[mapping.effectIndex]; - range = mapping.midiBinding.adjustRange(range); + float range = mapping.midiBinding.calculateRange(value); float currentValue = mapping.pPortInfo->rangeToValue(range); if (pEffect->GetControlValue(mapping.controlIndex) != currentValue) { diff --git a/src/MidiBinding.cpp b/src/MidiBinding.cpp index cf29f1b..25cd338 100644 --- a/src/MidiBinding.cpp +++ b/src/MidiBinding.cpp @@ -58,6 +58,8 @@ JSON_MAP_BEGIN(MidiBinding) JSON_MAP_REFERENCE(MidiBinding,bindingType) JSON_MAP_REFERENCE(MidiBinding,note) JSON_MAP_REFERENCE(MidiBinding,control) + JSON_MAP_REFERENCE(MidiBinding,minControlValue) + JSON_MAP_REFERENCE(MidiBinding,maxControlValue) JSON_MAP_REFERENCE(MidiBinding,minValue) JSON_MAP_REFERENCE(MidiBinding,maxValue) JSON_MAP_REFERENCE(MidiBinding,rotaryScale) diff --git a/src/MidiBinding.hpp b/src/MidiBinding.hpp index f66c14e..295da45 100644 --- a/src/MidiBinding.hpp +++ b/src/MidiBinding.hpp @@ -80,6 +80,8 @@ private: int bindingType_ = BINDING_TYPE_NONE; int note_ = 12*4+24; int control_ = 1; + int minControlValue_ = 0; + int maxControlValue_ = 127; float minValue_ = 0; float maxValue_ = 1; float rotaryScale_ = 1; @@ -99,6 +101,8 @@ public: && this->bindingType_ == other.bindingType_ && this->note_ == other.note_ && this->control_ == other.control_ + && this->minControlValue_ == other.minControlValue_ + && this->maxControlValue_ == other.maxControlValue_ && this->minValue_ == other.minValue_ && this->maxValue_ == other.maxValue_ && this->rotaryScale_ == other.rotaryScale_ @@ -111,6 +115,8 @@ public: GETTER_SETTER(bindingType); GETTER_SETTER(note); GETTER_SETTER(control); + GETTER_SETTER(minControlValue); + GETTER_SETTER(maxControlValue); GETTER_SETTER(minValue); GETTER_SETTER(maxValue); GETTER_SETTER(rotaryScale); @@ -120,9 +126,20 @@ public: void switchControlType(SwitchControlTypeT value) { switchControlType_ = (int)value; } - float adjustRange(float range) + float calculateRange(uint8_t value) { - return maxValue_*range+minValue_*(1.0f-range); + float controlRange; + if (maxControlValue_ == minControlValue_) + { + controlRange = minControlValue_; + } else { + controlRange = + (float)((int8_t)value - (int8_t)minControlValue_) / + (float)((int8_t)maxControlValue_ - (int8_t)minControlValue_); + } + if (controlRange < 0) controlRange = 0; + else if (controlRange > 1) controlRange = 1; + return maxValue_*controlRange+minValue_*(1.0f-controlRange); } DECLARE_JSON_MAP(MidiBinding); diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 89a16ba..ce694ab 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -136,6 +136,15 @@ bool Pedalboard::SetControlValue(int64_t pedalItemId, const std::string &symbol, return item->SetControlValue(symbol,value); } +bool Pedalboard::SetItemTitle(int64_t pedalItemId, const std::string &title) +{ + PedalboardItem*item = GetItem(pedalItemId); + if (!item) return false; + if (item->title() == title) return false; // no change. + item->title(title); + return true; +} + PedalboardItem Pedalboard::MakeEmptyItem() { @@ -503,6 +512,7 @@ JSON_MAP_BEGIN(PedalboardItem) JSON_MAP_REFERENCE(PedalboardItem,lv2State) JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri) JSON_MAP_REFERENCE(PedalboardItem,pathProperties) + JSON_MAP_REFERENCE(PedalboardItem,title) JSON_MAP_END() diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 867ddc3..1dcf00b 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -98,6 +98,7 @@ public: Lv2PluginState lv2State_; std::string lilvPresetUri_; std::map pathProperties_; + std::string title_; // non persistent state. PropertyMap patchProperties; @@ -126,6 +127,8 @@ public: GETTER_SETTER_REF(midiChannelBinding) GETTER_SETTER(stateUpdateCount) GETTER_SETTER_REF(lv2State) + GETTER_SETTER_REF(title) + Lv2PluginState&lv2State() { return lv2State_; } // non-const version. GETTER_SETTER_REF(lilvPresetUri) @@ -198,6 +201,7 @@ public: static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); + bool SetItemTitle(int64_t pedalItemId, const std::string &title); bool SetItemEnabled(int64_t pedalItemId, bool enabled); void SetCurrentSnapshotModified(bool modified); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index ab37917..51f8448 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2192,34 +2192,40 @@ void PiPedalModel::OnNotifyPathPatchPropertyReceived( } } -void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) +void PiPedalModel::OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) { std::lock_guard lock(mutex); + bool isNote = (cc0 & 0xF0) == 0x90; // Note On + if (isNote && cc2 == 0) { + return; // Note off. Oopsie. + } + bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change + if (!isNote && !isControl) { + return; // Not a note on or control change. + } + for (int i = 0; i < midiEventListeners.size(); ++i) { auto &listener = midiEventListeners[i]; - if ((!isNote) == (listener.listenForControls)) + auto subscriber = this->GetNotificationSubscriber(listener.clientId); + if (subscriber) { - auto subscriber = this->GetNotificationSubscriber(listener.clientId); - if (subscriber) - { - subscriber->OnNotifyMidiListener(listener.clientHandle, isNote, noteOrControl); - } - else - { - midiEventListeners.erase(midiEventListeners.begin() + i); - --i; - } + subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2); + } + else + { + midiEventListeners.erase(midiEventListeners.begin() + i); + --i; } } audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); } -void PiPedalModel::ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControls) +void PiPedalModel::ListenForMidiEvent(int64_t clientId, int64_t clientHandle) { std::lock_guard lock(mutex); - MidiListener listener{clientId, clientHandle, listenForControls}; + MidiListener listener{clientId, clientHandle}; midiEventListeners.push_back(listener); audioHost->SetListenForMidiEvent(true); } @@ -2946,4 +2952,17 @@ void PiPedalModel::MoveAudioFile( } AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); dir->MoveAudioFile(directory, fromPosition, toPosition); -} \ No newline at end of file +} +void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title) +{ + std::lock_guard lock(mutex); + if (!this->pedalboard.SetItemTitle(instanceId,title)) + { + return; + } + // no need to reload the pedalboard, but we do need to notify subscribers. + this->SetPresetChanged(-1, true); + this->FirePedalboardChanged(-1,false); + +} + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 0f35c13..5fd9997 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -78,7 +78,7 @@ namespace pipedal virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0; virtual void OnLoadPluginPreset(int64_t instanceId, const std::vector &controlValues) = 0; virtual void OnMidiValueChanged(int64_t instanceId, const std::string &symbol, float value) = 0; - virtual void OnNotifyMidiListener(int64_t clientHandle, bool isNote, uint8_t noteOrControl) = 0; + virtual void OnNotifyMidiListener(int64_t clientHandle, uint8_t cc0, uint8_t cc1, uint8_t cc2) = 0; virtual void OnNotifyPatchProperty(int64_t clientModel, uint64_t instanceId, const std::string &propertyUri, const std::string &atomJson) = 0; virtual void OnWifiConfigSettingsChanged(const WifiConfigSettings &wifiConfigSettings) = 0; virtual void OnWifiDirectConfigSettingsChanged(const WifiDirectConfigSettings &wifiDirectConfigSettings) = 0; @@ -146,7 +146,6 @@ namespace pipedal public: int64_t clientId; int64_t clientHandle; - bool listenForControls; }; class AtomOutputListener { @@ -234,7 +233,7 @@ namespace pipedal virtual void OnNotifyVusSubscription(const std::vector &updates) override; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override; - virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) override; + virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override; virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; @@ -437,7 +436,7 @@ namespace pipedal JackServerSettings GetJackServerSettings(); void SetJackServerSettings(const JackServerSettings &jackServerSettings); - void ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControls); + void ListenForMidiEvent(int64_t clientId, int64_t clientHandle); void CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle); void MonitorPatchProperty(int64_t clientId, int64_t clientHandle, uint64_t instanceId, const std::string &propertyUri); @@ -469,6 +468,8 @@ namespace pipedal const std::string & path, int32_t from, int32_t to); + + void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index ae4f965..c24ab81 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -174,18 +174,36 @@ JSON_MAP_REFERENCE(SetPatchPropertyBody, propertyUri) JSON_MAP_REFERENCE(SetPatchPropertyBody, value) JSON_MAP_END() +class SetPedalboardItemTitleBody +{ +public: + uint64_t instanceId_; + std::string title_; + DECLARE_JSON_MAP(SetPedalboardItemTitleBody); +}; + +JSON_MAP_BEGIN(SetPedalboardItemTitleBody) +JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, instanceId) +JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) +JSON_MAP_END() + + + + class NotifyMidiListenerBody { public: int64_t clientHandle_; - bool isNote_; - int32_t noteOrControl_; + uint16_t cc0_; + uint16_t cc1_; + uint16_t cc2_; DECLARE_JSON_MAP(NotifyMidiListenerBody); }; JSON_MAP_BEGIN(NotifyMidiListenerBody) JSON_MAP_REFERENCE(NotifyMidiListenerBody, clientHandle) -JSON_MAP_REFERENCE(NotifyMidiListenerBody, isNote) -JSON_MAP_REFERENCE(NotifyMidiListenerBody, noteOrControl) +JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc0) +JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc1) +JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc2) JSON_MAP_END() class NotifyAtomOutputBody @@ -208,13 +226,11 @@ JSON_MAP_END() class ListenForMidiEventBody { public: - bool listenForControls_; int64_t handle_; DECLARE_JSON_MAP(ListenForMidiEventBody); }; JSON_MAP_BEGIN(ListenForMidiEventBody) -JSON_MAP_REFERENCE(ListenForMidiEventBody, listenForControls) JSON_MAP_REFERENCE(ListenForMidiEventBody, handle) JSON_MAP_END() @@ -1099,7 +1115,7 @@ public: { ListenForMidiEventBody body; pReader->read(&body); - this->model.ListenForMidiEvent(this->clientId, body.handle_, body.listenForControls_); + this->model.ListenForMidiEvent(this->clientId, body.handle_); } else if (message == "cancelListenForMidiEvent") { @@ -1503,7 +1519,12 @@ public: { this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error) { this->SendError(replyTo, error.c_str()); }); } - + else if (message == "setPedalboardItemTitle") + { + SetPedalboardItemTitleBody body; + pReader->read(&body); + model.SetPedalboardItemTitle(body.instanceId_, body.title_); + } else if (message == "getPatchProperty") { GetPatchPropertyBody body; @@ -2121,12 +2142,13 @@ private: Send("onNotifyPathPatchPropertyChanged", body); } - virtual void OnNotifyMidiListener(int64_t clientHandle, bool isNote, uint8_t noteOrControl) + virtual void OnNotifyMidiListener(int64_t clientHandle, uint8_t cc0, uint8_t cc1, uint8_t cc2) override { NotifyMidiListenerBody body; body.clientHandle_ = clientHandle; - body.isNote_ = isNote; - body.noteOrControl_ = noteOrControl; + body.cc0_ = cc0; + body.cc1_ = cc1; + body.cc2_ = cc2; Send("onNotifyMidiListener", body); } diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 634d354..c3260fb 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -31,6 +31,20 @@ namespace pipedal { class IndexedSnapshot; + class MidiNotifyBody + { + public: + MidiNotifyBody() = default; + MidiNotifyBody(uint8_t cc0, uint8_t cc1, uint8_t cc2) + : cc0_(cc0), cc1_(cc1), cc2_(cc2) + { + } + + uint8_t cc0_; + uint8_t cc1_; + uint8_t cc2_; + }; + enum class RingBufferCommand : int64_t { Invalid = 0, @@ -38,7 +52,7 @@ namespace pipedal EffectReplaced, SetValue, SetBypass, - //AudioStopped, + // AudioStopped, AudioTerminatedAbnormally, // specifically for an ALSA loss of connection. SetVuSubscriptions, FreeVuSubscriptions, @@ -327,7 +341,7 @@ namespace pipedal return; } } - + template void write(RingBufferCommand command, const T &value, size_t dataLength, uint8_t *variableData) { @@ -374,12 +388,9 @@ namespace pipedal write(RingBufferCommand::MidiValueChanged, body); } - void OnMidiListen(bool isNote, uint8_t noteOrControl) + void OnMidiListen(const MidiNotifyBody &body) { - uint16_t msg = noteOrControl; - if (isNote) - msg |= 0x100; - write(RingBufferCommand::OnMidiListen, msg); + write(RingBufferCommand::OnMidiListen, body); } /** @@ -485,7 +496,8 @@ namespace pipedal { write(RingBufferCommand::AckMidiProgramChange, requestId); } - void AckMidiSnapshotRequest(uint64_t snapshotRequestId) { + void AckMidiSnapshotRequest(uint64_t snapshotRequestId) + { write(RingBufferCommand::AckMidiSnapshotRequest, snapshotRequestId); } diff --git a/todo.txt b/todo.txt index b3e48db..fd1dddc 100644 --- a/todo.txt +++ b/todo.txt @@ -1,19 +1,9 @@ -window.addEventListener .... done wrong everywhere! - -touch scroll in file propertty dialog. - -channel bindings help dialog. createStyles({ } }); -enum MidiControlType { +export enum MidiControlType { None, Select, Dial, @@ -56,18 +57,57 @@ enum MidiControlType { MomentarySwitch } +export function getMidiControlType(uiPlugin: UiPlugin | undefined, symbol: string): MidiControlType { + if (!uiPlugin) return MidiControlType.None; + let port = uiPlugin.getControl(symbol); + if (!port) return MidiControlType.None; + + if (symbol === "__bypass") { + return MidiControlType.Toggle; + } + + + if (!port) return MidiControlType.None; + + if (port.mod_momentaryOffByDefault || port.mod_momentaryOnByDefault) { + return MidiControlType.MomentarySwitch; + } + if (port.trigger_property) { + return MidiControlType.Trigger; + } + if (port.trigger_property) { + return MidiControlType.Trigger; + } + if (port.isAbToggle() || port.isOnOffSwitch()) { + return MidiControlType.Toggle; + } + if (port.isSelect()) { + return MidiControlType.Select; + } + return MidiControlType.Dial; +} + +export function canBindToNote(controlType: MidiControlType): boolean { + return controlType === MidiControlType.Toggle + || controlType === MidiControlType.Trigger + || controlType === MidiControlType.MomentarySwitch +} + interface MidiBindingViewProps extends WithStyles { instanceId: number; - listen: boolean; midiBinding: MidiBinding; - uiPlugin: UiPlugin; + midiControlType: MidiControlType; onChange: (instanceId: number, newBinding: MidiBinding) => void; - onListen: (instanceId: number, key: string, listenForControl: boolean) => void; } interface MidiBindingViewState { - midiControlType: MidiControlType; + listenSymbol: string; + listenForRangeSymbol: string; + listenForRangeMax: number; + listenForRangeMin: number; + listenSnackbarOpen: boolean; + } @@ -83,7 +123,11 @@ const MidiBindingView = super(props); this.model = PiPedalModelFactory.getInstance(); this.state = { - midiControlType: this.getControlType() + listenSymbol: "", + listenForRangeSymbol: "", + listenForRangeMax: 127, + listenForRangeMin: 0, + listenSnackbarOpen: false }; } @@ -139,6 +183,16 @@ const MidiBindingView = newBinding.maxValue = value; this.props.onChange(this.props.instanceId, newBinding); } + handleCtlMinChange(value: number): void { + let newBinding = this.props.midiBinding.clone(); + newBinding.minControlValue = Math.round(value); + this.props.onChange(this.props.instanceId, newBinding); + } + handleCtlMaxChange(value: number): void { + let newBinding = this.props.midiBinding.clone(); + newBinding.maxControlValue = Math.round(value); + this.props.onChange(this.props.instanceId, newBinding); + } handleScaleChange(value: number): void { let newBinding = this.props.midiBinding.clone(); newBinding.rotaryScale = value; @@ -158,10 +212,23 @@ const MidiBindingView = return result; } + mounted: boolean = false; + + componentDidMount() { + super.componentDidMount?.(); + this.mounted = true; + } + + + componentWillUnmount() { + this.mounted = false; + this.cancelListenForControl(); + super.componentWillUnmount?.(); + } validateSwitchControlType(midiBinding: MidiBinding) { // :-( - let controlType = this.state.midiControlType; + let controlType = this.props.midiControlType; if (controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) { if (midiBinding.switchControlType !== MidiBinding.TRIGGER_ON_RISING_EDGE // Toggle on Note On. @@ -181,41 +248,154 @@ const MidiBindingView = } getControlType(): MidiControlType { - - if (this.props.midiBinding.symbol === "__bypass") { - return MidiControlType.Toggle; - } - if (!this.props.uiPlugin) return MidiControlType.None; - - let port = this.props.uiPlugin.getControl(this.props.midiBinding.symbol); - - if (!port) return MidiControlType.None; - if (port.mod_momentaryOffByDefault || port.mod_momentaryOnByDefault) { - return MidiControlType.MomentarySwitch; - } - if (port.trigger_property) { - return MidiControlType.Trigger; - } - if (port.trigger_property) { - return MidiControlType.Trigger; - } - if (port.isAbToggle() || port.isOnOffSwitch()) { - return MidiControlType.Toggle; - } - if (port.isSelect()) { - return MidiControlType.Select; - } - return MidiControlType.Dial; + return this.props.midiControlType; } + /////////////////////////////////////// + listenTimeoutHandle?: number; + + listenHandle?: ListenHandle; + + cancelListenForControl() { + this.stopListenTimeout(); + if (this.listenHandle) { + this.model.cancelListenForMidiEvent(this.listenHandle) + this.listenHandle = undefined; + } + + this.setState({ listenSymbol: "", listenForRangeSymbol: "" }); + + } + + handleListenForRangeSucceeded(instanceId: number, symbol: string, midiMessage: MidiMessage) { + if (!midiMessage.isControl()) return; + + let binding = this.props.midiBinding; + if (binding.control != midiMessage.cc1) { + return; + } + + + let value = midiMessage.cc2; + let min = this.state.listenForRangeMin; + let max = this.state.listenForRangeMax; + + let update = false; + if (value < min) { + min = value; + update = true; + } + if (value > max) { + max = value; + update = true; + } + if (!update) { + return; // No change, so ignore. + } + + let newBinding = binding.clone(); + newBinding.minControlValue = min; + newBinding.maxControlValue = max; + + this.props.onChange(this.props.instanceId, newBinding); + this.setState({ listenForRangeMin: min, listenForRangeMax: max }); + this.startListenTimeout(20000); + + } + + handleListenSucceeded(instanceId: number, symbol: string, midiMessage: MidiMessage) { + if (instanceId !== this.props.instanceId) { + return; + } + if (symbol !== this.props.midiBinding.symbol) { + return; + } + + + let binding = this.props.midiBinding; + let newBinding = binding.clone(); + if (midiMessage.isNote()) { + if (!canBindToNote(this.props.midiControlType)) { + return; + } + newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE + newBinding.note = midiMessage.cc1; + } else if (midiMessage.isControl()) { + newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL + newBinding.control = midiMessage.cc1; + } else { + return; + } + this.props.onChange(this.props.instanceId, newBinding); + this.cancelListenForControl(); + } + handleListenForControlRange(instanceId: number, symbol: string): void { + if (this.state.listenForRangeSymbol !== "") { + this.cancelListenForControl(); + return; + } + this.cancelListenForControl(); + + this.setState({ + listenSymbol: "", + listenForRangeSymbol: symbol, + listenForRangeMin: 127, + listenForRangeMax: 0, + listenSnackbarOpen: true + }); + this.startListenTimeout(20000); + + this.listenHandle = this.model.listenForMidiEvent( + (midiMessage) => { + this.handleListenForRangeSucceeded(instanceId, symbol, midiMessage); + }); + } + + stopListenTimeout(): void { + if (this.listenTimeoutHandle) { + clearTimeout(this.listenTimeoutHandle); + } + } + + startListenTimeout(timeout: number): void { + this.stopListenTimeout(); + this.listenTimeoutHandle = setTimeout(() => { + this.cancelListenForControl(); + this.listenTimeoutHandle = undefined; + }, timeout); + } + + handleListen(): void { + if (this.state.listenSymbol !== "") { + this.cancelListenForControl(); + return; + } + this.cancelListenForControl(); + + this.setState({ + listenSymbol: this.props.midiBinding.symbol, + listenForRangeSymbol: "", + listenSnackbarOpen: true + }); + let instanceId = this.props.instanceId; + let symbol = this.props.midiBinding.symbol; + + this.startListenTimeout(8000); + + this.listenHandle = this.model.listenForMidiEvent( + (midiMessage) => { + this.handleListenSucceeded(instanceId, symbol, midiMessage); + }); + } + + /////////////////////////////////////// render() { const classes = withStyles.getClasses(this.props); let midiBinding = this.props.midiBinding; - let uiPlugin = this.props.uiPlugin; - if (!uiPlugin) { + if (this.props.midiControlType === MidiControlType.None) { return (
); } - let controlType = this.state.midiControlType; + let controlType = this.props.midiControlType; let showLinearRange = controlType === MidiControlType.Dial && @@ -238,9 +418,7 @@ const MidiBindingView = value={midiBinding.bindingType} > None - {(controlType === MidiControlType.Toggle - || controlType === MidiControlType.Trigger - || controlType === MidiControlType.MomentarySwitch) && ( + {(canBindToNote(this.props.midiControlType)) && ( Note )} Control @@ -262,15 +440,15 @@ const MidiBindingView = { + this.cancelListenForControl(); + }} + onClick={() => { - if (this.props.listen) { - this.props.onListen(-2, "", false) - } else { - this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false) - } + this.handleListen() }} size="large"> - {this.props.listen ? ( + {this.state.listenSymbol !== "" ? ( ) : ( @@ -296,15 +474,15 @@ const MidiBindingView = { + this.cancelListenForControl(); + }} + onClick={() => { - if (this.props.listen) { - this.props.onListen(-2, "", false) - } else { - this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, true) - } + this.handleListen() }} size="large"> - {this.props.listen ? ( + {this.state.listenSymbol !== "" ? ( ) : ( @@ -314,6 +492,27 @@ const MidiBindingView =
) } + {midiBinding.bindingType === MidiBinding.BINDING_TYPE_NONE && + ( + { + this.cancelListenForControl(); + }} + + onClick={() => { + this.handleListen() + }} + size="large"> + {this.state.listenSymbol !== "" ? ( + + ) : ( + + )} + + + ) + } { ((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) && ( @@ -378,25 +577,58 @@ const MidiBindingView = Rotary - - ) - } - { - showLinearRange && ( + )} + {showLinearRange && ( +
- Min:  - { this.handleMinChange(value); }} + Min Ctl:  + { this.handleCtlMinChange(value); }} />
- ) +
+
+ Max Ctl:  + { this.handleCtlMaxChange(value); }} + /> +
+ + { + this.cancelListenForControl(); + }} + onClick={() => { + this.handleListenForControlRange(this.props.instanceId, this.props.midiBinding.symbol); + }} + size="large"> + {this.state.listenForRangeSymbol !== "" ? ( + + ) : ( + + )} + +
+ +
+ ) } { showLinearRange && ( -
- Max:  - { this.handleMaxChange(value); }} /> +
+
+ Min Val:  + { this.handleMinChange(value); }} + /> +
+
+ Max Val:  + { this.handleMaxChange(value); }} /> +
) } @@ -404,11 +636,24 @@ const MidiBindingView = canRotaryScale && (
Scale:  - { this.handleScaleChange(value); }} />
) } + + this.setState({ listenSnackbarOpen: false })} + message="Listening for MIDI input" + /> + +
); diff --git a/vite/src/pipedal/MidiBindingsDialog.tsx b/vite/src/pipedal/MidiBindingsDialog.tsx index d38db41..7ab6f9a 100644 --- a/vite/src/pipedal/MidiBindingsDialog.tsx +++ b/vite/src/pipedal/MidiBindingsDialog.tsx @@ -20,7 +20,7 @@ import React, { SyntheticEvent } from 'react'; import DialogEx from './DialogEx'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; -import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import Typography from '@mui/material/Typography'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; @@ -32,8 +32,7 @@ import Toolbar from '@mui/material/Toolbar'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import IconButtonEx from './IconButtonEx'; import MidiBinding from './MidiBinding'; -import MidiBindingView from './MidiBindingView'; -import Snackbar from '@mui/material/Snackbar'; +import MidiBindingView, { getMidiControlType } from './MidiBindingView'; import { PortGroup, UiPlugin, makeSplitUiPlugin } from './Lv2Plugin'; import { css } from '@emotion/react'; @@ -81,9 +80,6 @@ export interface MidiBindingDialogProps extends WithStyles { } export interface MidiBindingDialogState { - listenInstanceId: number; - listenSymbol: string; - listenSnackbarOpen: boolean; } @@ -98,7 +94,10 @@ export const MidiBindingDialog = this.state = { listenInstanceId: -2, listenSymbol: "", - listenSnackbarOpen: false + listenForRangeSymbol: "", + listenSnackbarOpen: false, + listenForRangeMin: 127, + listenForRangeMax: 0 }; this.model = PiPedalModelFactory.getInstance(); this.handleClose = this.handleClose.bind(this); @@ -108,64 +107,9 @@ export const MidiBindingDialog = hasHooks: boolean = false; handleClose() { - this.cancelListenForControl(); this.props.onClose(); } - listenTimeoutHandle?: number; - - listenHandle?: ListenHandle; - - cancelListenForControl() { - if (this.listenTimeoutHandle) { - clearTimeout(this.listenTimeoutHandle); - this.listenTimeoutHandle = undefined; - } - if (this.listenHandle) { - this.model.cancelListenForMidiEvent(this.listenHandle) - this.listenHandle = undefined; - } - - this.setState({ listenInstanceId: -2, listenSymbol: "" }); - - } - - handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) { - this.cancelListenForControl(); - - let pedalboard = this.model.pedalboard.get(); - let item = pedalboard.getItem(instanceId); - if (!item) return; - - let binding = item.getMidiBinding(symbol); - let newBinding = binding.clone(); - if (isNote) { - newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE; - newBinding.note = noteOrControl; - } else { - newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL; - newBinding.control = noteOrControl; - } - - this.model.setMidiBinding(instanceId, newBinding); - } - - - handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void { - this.cancelListenForControl(); - this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true }); - this.listenTimeoutHandle = setTimeout(() => { - this.cancelListenForControl(); - }, 8000); - - this.listenHandle = this.model.listenForMidiEvent(listenForControl, - (isNote: boolean, noteOrControl: number) => { - this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl); - }); - - - - } onWindowSizeChanged(width: number, height: number): void { } @@ -176,10 +120,11 @@ export const MidiBindingDialog = this.mounted = true; } componentWillUnmount() { - super.componentWillUnmount(); this.mounted = false; + super.componentWillUnmount(); + } componentDidUpdate() { @@ -255,15 +200,7 @@ export const MidiBindingDialog = { - if (instanceId === -2) { - this.cancelListenForControl(); - } else { - this.handleListenForControl(instanceId, symbol, listenForControl); - } - }} - listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === "__bypass"} + midiControlType={ getMidiControlType(plugin, "__bypass") } onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} /> @@ -305,13 +242,8 @@ export const MidiBindingDialog = { - this.handleListenForControl(instanceId, symbol, listenForControl); - }} - onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} /> @@ -379,16 +311,6 @@ export const MidiBindingDialog = - this.setState({ listenSnackbarOpen: false })} - message="Listening for MIDI input" - /> ); } diff --git a/vite/src/pipedal/NumericInput.tsx b/vite/src/pipedal/NumericInput.tsx index 73e50d6..a2c8dd9 100644 --- a/vite/src/pipedal/NumericInput.tsx +++ b/vite/src/pipedal/NumericInput.tsx @@ -35,14 +35,18 @@ const styles = ({ palette }: Theme) => createStyles({ interface NumericInputProps extends WithStyles { ariaLabel: string; - defaultValue: number; + value: number; min: number; max: number; + step?: number; + integer?: boolean; onChange: (value: number) => void; } interface NumericInputState { + stateValue: string; error: boolean; + focused: boolean; } export const NumericInput = @@ -54,22 +58,28 @@ export const NumericInput = constructor(props: NumericInputProps) { super(props); this.state = { - error: false + stateValue: this.toDisplayValue(props.value), + error: false, + focused: false }; } changed: boolean = false; - handleChange(event: any): void { + handleChange(event: React.ChangeEvent): void { this.changed = true; let strValue = event.target.value; + this.setState({ stateValue: strValue }); try { let value = Number(strValue); if (isNaN(value)) { this.setState({ error: true }); + } else if (this.props.integer === true && !Number.isInteger(value)) { + this.setState({ error: true }); } else if (value >= this.props.min && value <= this.props.max) { this.setState({ error: false }); + this.props.onChange(value); } else { this.setState({ error: true }); } @@ -80,6 +90,10 @@ export const NumericInput = } toDisplayValue(value: number): string { + if (this.props.integer === true) + { + return Math.round(value).toFixed(0); + } if (value <= -1000 || value >= 1000) { return value.toFixed(0); @@ -96,7 +110,7 @@ export const NumericInput = } } - apply(input: HTMLInputElement) { + apply(input: HTMLInputElement| HTMLTextAreaElement) { if (this.changed) { let strValue = input.value; let value = Number(strValue); @@ -104,28 +118,34 @@ export const NumericInput = { if (value < this.props.min) value = this.props.min; if (value > this.props.max) value = this.props.max; + if (this.props.integer === true) value = Math.round(value); input.value = this.toDisplayValue(value); this.props.onChange(value); } else { - input.value = this.toDisplayValue(this.props.defaultValue); + input.value = this.toDisplayValue(this.props.value); + this.props.onChange(this.props.value); } this.changed = false; this.setState({ error: false }); } } - handleLostFocus(e: any) { - this.apply(e.currentTarget as HTMLInputElement); + handleFocus(e: React.FocusEvent) { + this.changed = false; + this.setState({ focused: true }); + e.currentTarget.select(); + this.setState({ stateValue: this.toDisplayValue(this.props.value) }); } - handleKeyDown(e: React.KeyboardEvent) { + handleBlur(e: any) { + this.apply(e.currentTarget as HTMLInputElement); + this.setState({ focused: false }); + } + handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter") { - this.apply(e.currentTarget as HTMLInputElement); - } else if (e.key === "Escape") - { - e.currentTarget.value = this.toDisplayValue(this.props.defaultValue); - this.changed = false; - } + this.setState({ stateValue: this.toDisplayValue(this.props.value) }); + this.changed = false; + } } @@ -135,11 +155,15 @@ export const NumericInput = inputProps={{ 'aria-label': this.props.ariaLabel, style: { textAlign: 'right' }, - "onBlur": (e: any) => this.handleLostFocus(e), - "onKeyDown": (e: any) => this.handleKeyDown(e) + "min": this.props.min, + "max": this.props.max, + "step:": this.props.step }} - onChange={(event) => this.handleChange(event)} - defaultValue={this.toDisplayValue(this.props.defaultValue)} + onFocus={(e) => {this.handleFocus(e);}} + onBlur= {(e) => { this.handleBlur(e); }} + onKeyDown={(e) => {this.handleKeyDown(e);}} + onChange={(e) => this.handleChange(e)} + value={this.state.focused ? this.state.stateValue: this.toDisplayValue(this.props.value)} error={this.state.error} /> diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 09fa78f..c00728b 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -158,14 +158,31 @@ interface ControlValueChangeItem { }; +export class MidiMessage { + constructor(cc0: number, cc1: number, cc2: number) { + this.cc0 = cc0; + this.cc1 = cc1; + this.cc2 = cc2; + } + cc0: number; + cc1: number; + cc2: number; + + isNote() { + return (this.cc0 & 0xF0) == 0x90 && this.cc2 !== 0; + } + isControl() { + return (this.cc0 & 0xF0) == 0xB0; + } +}; class MidiEventListener { - constructor(handle: number, callback: (isNote: boolean, noteOrControl: number) => void) { + constructor(handle: number, callback: (message: MidiMessage) => void) { this.handle = handle; this.callback = callback; } handle: number; - callback: (isNote: boolean, noteOrControl: number) => void; + callback: (midiMessage: MidiMessage) => void; }; export type PatchPropertyListener = (instanceId: number, propertyUri: string, atomObject: any) => void; @@ -679,10 +696,13 @@ export class PiPedalModel //implements PiPedalModel } } else if (message === "onNotifyMidiListener") { - let clientHandle = body.clientHandle as number; - let isNote = body.isNote as boolean; - let noteOrControl = body.noteOrControl as number; - this.handleNotifyMidiListener(clientHandle, isNote, noteOrControl); + let notifyBody = body as { clientHandle: number, cc0: number, cc1: number, cc2: number }; + let clientHandle = notifyBody.clientHandle as number; + this.handleNotifyMidiListener( + clientHandle, + notifyBody.cc0, + notifyBody.cc1, + notifyBody.cc2); } else if (message === "onNotifyPathPatchPropertyChanged") { let instanceId = body.instanceId as number; let propertyUri = body.propertyUri as string; @@ -2355,12 +2375,12 @@ export class PiPedalModel //implements PiPedalModel private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = []; nextListenHandle = 1; - listenForMidiEvent(listenForControl: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle { + listenForMidiEvent(onComplete: (midiMessage: MidiMessage) => void): ListenHandle { let handle = this.nextListenHandle++; this.midiListeners.push(new MidiEventListener(handle, onComplete)); - this.webSocket?.send("listenForMidiEvent", { listenForControls: listenForControl, handle: handle }); + this.webSocket?.send("listenForMidiEvent", { handle: handle }); return { _handle: handle }; @@ -2418,12 +2438,16 @@ export class PiPedalModel //implements PiPedalModel } - handleNotifyMidiListener(clientHandle: number, isNote: boolean, noteOrControl: number) { + handleNotifyMidiListener(clientHandle: number, cc0: number, cc1: number, cc2: number): void { + let midiMessage = new MidiMessage(cc0, cc1, cc2); + + if (!midiMessage.isNote() && !midiMessage.isControl()) { + return; + } for (let i = 0; i < this.midiListeners.length; ++i) { let listener = this.midiListeners[i]; if (listener.handle === clientHandle) { - listener.callback(isNote, noteOrControl); - + listener.callback(midiMessage); } } } @@ -2464,7 +2488,7 @@ export class PiPedalModel //implements PiPedalModel let link = window.document.createElement("A") as HTMLAnchorElement; link.href = url; link.target = "_blank"; - link.download = "download.piPreset"; + link.download = "download.piPreset"; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -2705,8 +2729,8 @@ export class PiPedalModel //implements PiPedalModel }); } copyFilePropertyFile( - oldRelativePath: string, - newRelativePath: string, + oldRelativePath: string, + newRelativePath: string, uiFileProperty: UiFileProperty, overwrite: boolean ): Promise { @@ -3167,7 +3191,7 @@ export class PiPedalModel //implements PiPedalModel item.title = title; this.pedalboard.set(newPedalboard); // notify the server. - // xxx; + this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); } }; diff --git a/vite/src/pipedal/SystemMidiBindingView.tsx b/vite/src/pipedal/SystemMidiBindingView.tsx index dc52ed6..8b87e46 100644 --- a/vite/src/pipedal/SystemMidiBindingView.tsx +++ b/vite/src/pipedal/SystemMidiBindingView.tsx @@ -23,200 +23,21 @@ */ -import { Component } from 'react'; -import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; -import { Theme } from '@mui/material/styles'; -import WithStyles from './WithStyles'; -import { withStyles } from "tss-react/mui"; -import {createStyles} from './WithStyles'; - -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import MidiBinding from './MidiBinding'; -import Utility from './Utility'; -import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; -import MicOutlinedIcon from '@mui/icons-material/MicOutlined'; -import IconButtonEx from './IconButtonEx'; - - - -const styles = (theme: Theme) => createStyles({ - controlDiv: { flex: "0 0 auto", marginRight: 12, verticalAlign: "center", height: 48, paddingTop: 8, paddingBottom: 8 }, - controlDiv2: { - flex: "0 0 auto", marginRight: 12, verticalAlign: "center", - height: 48, paddingTop: 0, paddingBottom: 0, whiteSpace: "nowrap" - } -}); - -interface SystemMidiBindingViewProps extends WithStyles { +import MidiBindingView, {MidiControlType} from './MidiBindingView'; +interface SystemMidiBindingViewProps { instanceId: number; - listen: boolean; midiBinding: MidiBinding; onChange: (instanceId: number, newBinding: MidiBinding) => void; - onListen: (instanceId: number, key: string, listenForControl: boolean) => void; } - - -interface SystemMidiBindingViewState { +function SystemMidiBindingView(props: SystemMidiBindingViewProps) { + return ( + ) } - - - -const SystemMidiBindingView = - withStyles( - class extends Component { - - model: PiPedalModel; - - constructor(props: SystemMidiBindingViewProps) { - super(props); - this.model = PiPedalModelFactory.getInstance(); - this.state = { - }; - } - - handleTypeChange(e: any, extra: any) { - let newValue = parseInt(e.target.value); - let newBinding = this.props.midiBinding.clone(); - newBinding.bindingType = newValue; - this.props.onChange(this.props.instanceId, newBinding); - } - handleNoteChange(e: any, extra: any) { - let newValue = parseInt(e.target.value); - let newBinding = this.props.midiBinding.clone(); - newBinding.note = newValue; - this.props.onChange(this.props.instanceId, newBinding); - } - handleControlChange(e: any, extra: any) { - let newValue = parseInt(e.target.value); - let newBinding = this.props.midiBinding.clone(); - newBinding.control = newValue; - this.props.onChange(this.props.instanceId, newBinding); - } - handleLatchControlTypeChange(e: any, extra: any) { - let newValue = parseInt(e.target.value); - let newBinding = this.props.midiBinding.clone(); - newBinding.switchControlType = newValue; - this.props.onChange(this.props.instanceId, newBinding); - } - handleLinearControlTypeChange(e: any, extra: any) { - let newValue = parseInt(e.target.value); - let newBinding = this.props.midiBinding.clone(); - newBinding.linearControlType = newValue; - this.props.onChange(this.props.instanceId, newBinding); - } - handleMinChange(value: number): void { - let newBinding = this.props.midiBinding.clone(); - newBinding.minValue = value; - this.props.onChange(this.props.instanceId, newBinding); - } - handleMaxChange(value: number): void { - let newBinding = this.props.midiBinding.clone(); - newBinding.maxValue = value; - this.props.onChange(this.props.instanceId, newBinding); - } - handleScaleChange(value: number): void { - let newBinding = this.props.midiBinding.clone(); - newBinding.rotaryScale = value; - this.props.onChange(this.props.instanceId, newBinding); - } - - - generateMidiSelects(): React.ReactNode[] { - let result: React.ReactNode[] = []; - - for (let i = 0; i < 127; ++i) { - result.push( - {Utility.midiNoteName(i)} - ) - } - - return result; - } - generateControlSelects(): React.ReactNode[] { - - return Utility.validMidiControllers.map((control) => ( - {control.displayName} - ) - ); - } - - - render() { - const classes = withStyles.getClasses(this.props); - let midiBinding = this.props.midiBinding; - - return ( -
-
- -
- { - (midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) && - ( -
- -
- ) - } - { - (midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) && - ( -
- - -
- ) - } - { - if (this.props.listen) { - this.props.onListen(-2, "", false) - } else { - this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false) - } - }} - size="large"> - {this.props.listen ? ( - - ) : ( - - )} - -
- ); - - } - }, - styles - ); - export default SystemMidiBindingView; \ No newline at end of file diff --git a/vite/src/pipedal/SystemMidiBindingsDialog.tsx b/vite/src/pipedal/SystemMidiBindingsDialog.tsx index 2f41421..648b3f8 100644 --- a/vite/src/pipedal/SystemMidiBindingsDialog.tsx +++ b/vite/src/pipedal/SystemMidiBindingsDialog.tsx @@ -27,7 +27,7 @@ import React, { SyntheticEvent } from 'react'; import { css } from '@emotion/react'; import DialogEx from './DialogEx'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; -import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import Typography from '@mui/material/Typography'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; @@ -182,66 +182,9 @@ export const SystemMidiBindingDialog = hasHooks: boolean = false; handleClose() { - this.cancelListenForControl(); this.props.onClose(); } - listenTimeoutHandle?: number; - - listenHandle?: ListenHandle; - - cancelListenForControl() { - if (this.listenTimeoutHandle) { - clearTimeout(this.listenTimeoutHandle); - this.listenTimeoutHandle = undefined; - } - if (this.listenHandle) { - this.model.cancelListenForMidiEvent(this.listenHandle) - this.listenHandle = undefined; - } - - this.setState({ listenInstanceId: -2, listenSymbol: "" }); - - } - - handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) { - this.cancelListenForControl(); - - for (var binding of this.state.systemMidiBindings) { - if (binding.instanceId === instanceId) { - let newBinding = binding.midiBinding.clone(); - - if (isNote) { - newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE; - newBinding.note = noteOrControl; - } else { - newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL; - newBinding.control = noteOrControl; - } - - this.model.setSystemMidiBinding(instanceId, newBinding); - return; - } - } - } - - - handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void { - this.cancelListenForControl(); - this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true }); - this.listenTimeoutHandle = setTimeout(() => { - this.cancelListenForControl(); - }, 8000); - - this.listenHandle = this.model.listenForMidiEvent(listenForControl, - (isNote: boolean, noteOrControl: number) => { - this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl); - }); - - - - } - onWindowSizeChanged(width: number, height: number): void { } @@ -293,14 +236,6 @@ export const SystemMidiBindingDialog = { - if (instanceId === -2) { - this.cancelListenForControl(); - } else { - this.handleListenForControl(instanceId, symbol, listenForControl); - } - }} - listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === item.midiBinding.symbol} onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} /> diff --git a/vite/src/pipedal/ToolTipEx.tsx b/vite/src/pipedal/ToolTipEx.tsx index b2e0bfa..a7a3dd7 100644 --- a/vite/src/pipedal/ToolTipEx.tsx +++ b/vite/src/pipedal/ToolTipEx.tsx @@ -48,9 +48,8 @@ function ToolTipEx(props: ToolTipExProps) { setTimeoutInstance(timeoutInstance + 1); // make useeffect run. } function stopTimeout() { - if (timeout > 0) { - startTimeout(0); - } + setTimeout(0); + setTimeoutInstance(0); } function handleMouseEnter(event: React.MouseEvent) { setOpen(false); @@ -60,6 +59,7 @@ function ToolTipEx(props: ToolTipExProps) { function handleMouseLeave(event: React.MouseEvent) { setOpen(false); stopTimeout(); + setLongPressLeaving(false); } function handlePointerDownCapture(event: React.PointerEvent) { @@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) { if (valueTooltip === undefined) // no timeout if there's a value tooltip { if (t > 0) { - // console.log("ToolTipEx: starting timeout for ", t); + console.log("ToolTipEx: starting timeout for ", t); handle = window.setTimeout(() => { setOpen(true); },t); @@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) { } return () => { if (handle !== null) { - // console.log("ToolTipEx: clearing timeout for ", t); + console.log("ToolTipEx: clearing timeout for ", t); window.clearTimeout(handle); } }; @@ -179,41 +179,46 @@ function ToolTipEx(props: ToolTipExProps) { stopTimeout(); // Reset hover timeout on click } return ( -
{ - // console.log("ToolTipEx: onClickCapture"); + console.log("ToolTipEx: onClickCapture"); handleClickCapture(e); + setTimeout(0); + setOpen(false); + setIsLongPress(false); + setLongPressLeaving(true); }} onMouseEnter={(e)=> { - // console.log("ToolTipEx: onMouseEnter"); - - handleMouseEnter(e); + console.log("ToolTipEx: onMouseEnter"); + if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state + handleMouseEnter(e); + } }} onMouseLeave={(e)=> { - // console.log("ToolTipEx: onMouseLeave"); + console.log("ToolTipEx: onMouseLeave"); handleMouseLeave(e); }} onPointerCancelCapture={(e) => { - // console.log("ToolTipEx: onPointerCancelCapture"); + console.log("ToolTipEx: onPointerCancelCapture"); handlePointerCancel(e); }} onContextMenuCapture={(e) => { - // console.log("ToolTipEx: onContextMenuCapture"); + console.log("ToolTipEx: onContextMenuCapture"); handleConextMenu(e); return false; }} onPointerDownCapture={(e) => { - // console.log("ToolTipEx: onPointerDownCapture"); + console.log("ToolTipEx: onPointerDownCapture"); handlePointerDownCapture(e); }} onPointerMoveCapture={(e) => { - // console.log("ToolTipEx: onPointerMoveCapture"); + console.log("ToolTipEx: onPointerMoveCapture"); handlePointerMoveCapture(e); }} onPointerUpCapture={(e) => { - // console.log("ToolTipEx: onPointerUpCapture"); + console.log("ToolTipEx: onPointerUpCapture"); handlePointerUpCapture(e); }} >