listen for midi binding control range.

This commit is contained in:
Robin E. R. Davies
2025-06-26 10:24:48 -04:00
parent 2974033128
commit 07af383211
20 changed files with 591 additions and 522 deletions
+20 -12
View File
@@ -294,12 +294,14 @@ class SystemMidiBinding
private: private:
MidiBinding currentBinding; MidiBinding currentBinding;
bool controlState = false; bool controlState = false;
uint8_t lastControlValue = 0;
public: public:
void SetBinding(const MidiBinding &binding) void SetBinding(const MidiBinding &binding)
{ {
currentBinding = binding; currentBinding = binding;
controlState = false; controlState = false;
lastControlValue = 0;
} }
bool IsMatch(const MidiEvent &event); bool IsMatch(const MidiEvent &event);
@@ -335,13 +337,18 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event)
return false; return false;
if (event.buffer[1] != currentBinding.control()) if (event.buffer[1] != currentBinding.control())
return false; 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; result = value >= 0x64 && lastControlValue < 0x64;
return state; } else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY)
{
result = true;
} }
return false; lastControlValue = value;
return result;
} }
break; break;
default: default:
@@ -918,11 +925,12 @@ private:
if (event.size >= 3) if (event.size >= 3)
{ {
uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0); 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; bool isControl = cmd == 0xB0;
if (isNote || isControl) 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) if (command == RingBufferCommand::OnMidiListen)
{ {
uint16_t msg; MidiNotifyBody body;
hostReader.read(&msg); hostReader.read(&body);
if (this->pNotifyCallbacks) if (this->pNotifyCallbacks)
{ {
pNotifyCallbacks->OnNotifyMidiListen((msg & 0xFF00) != 0, (uint8_t)msg); pNotifyCallbacks->OnNotifyMidiListen(body.cc0_, body.cc1_, body.cc2_);
} }
} }
else if (command == RingBufferCommand::MidiValueChanged) else if (command == RingBufferCommand::MidiValueChanged)
@@ -2080,8 +2088,8 @@ public:
return result; return result;
} }
volatile bool listenForMidiEvent = false; std::atomic<bool> listenForMidiEvent = false;
volatile bool listenForAtomOutput = false; std::atomic<bool> listenForAtomOutput = false;
virtual void SetListenForMidiEvent(bool listen) virtual void SetListenForMidiEvent(bool listen)
{ {
+1 -1
View File
@@ -158,7 +158,7 @@ namespace pipedal
virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) = 0; virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) = 0;
virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0;
virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 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( virtual void OnNotifyPathPatchPropertyReceived(
int64_t instanceId, int64_t instanceId,
+1 -1
View File
@@ -824,7 +824,7 @@ void Lv2Pedalboard::OnMidiMessage(size_t size, uint8_t *message,
case MidiControlType::Dial: case MidiControlType::Dial:
{ {
IEffect *pEffect = this->realtimeEffects[mapping.effectIndex]; IEffect *pEffect = this->realtimeEffects[mapping.effectIndex];
range = mapping.midiBinding.adjustRange(range); float range = mapping.midiBinding.calculateRange(value);
float currentValue = mapping.pPortInfo->rangeToValue(range); float currentValue = mapping.pPortInfo->rangeToValue(range);
if (pEffect->GetControlValue(mapping.controlIndex) != currentValue) if (pEffect->GetControlValue(mapping.controlIndex) != currentValue)
{ {
+2
View File
@@ -58,6 +58,8 @@ JSON_MAP_BEGIN(MidiBinding)
JSON_MAP_REFERENCE(MidiBinding,bindingType) JSON_MAP_REFERENCE(MidiBinding,bindingType)
JSON_MAP_REFERENCE(MidiBinding,note) JSON_MAP_REFERENCE(MidiBinding,note)
JSON_MAP_REFERENCE(MidiBinding,control) JSON_MAP_REFERENCE(MidiBinding,control)
JSON_MAP_REFERENCE(MidiBinding,minControlValue)
JSON_MAP_REFERENCE(MidiBinding,maxControlValue)
JSON_MAP_REFERENCE(MidiBinding,minValue) JSON_MAP_REFERENCE(MidiBinding,minValue)
JSON_MAP_REFERENCE(MidiBinding,maxValue) JSON_MAP_REFERENCE(MidiBinding,maxValue)
JSON_MAP_REFERENCE(MidiBinding,rotaryScale) JSON_MAP_REFERENCE(MidiBinding,rotaryScale)
+19 -2
View File
@@ -80,6 +80,8 @@ private:
int bindingType_ = BINDING_TYPE_NONE; int bindingType_ = BINDING_TYPE_NONE;
int note_ = 12*4+24; int note_ = 12*4+24;
int control_ = 1; int control_ = 1;
int minControlValue_ = 0;
int maxControlValue_ = 127;
float minValue_ = 0; float minValue_ = 0;
float maxValue_ = 1; float maxValue_ = 1;
float rotaryScale_ = 1; float rotaryScale_ = 1;
@@ -99,6 +101,8 @@ public:
&& this->bindingType_ == other.bindingType_ && this->bindingType_ == other.bindingType_
&& this->note_ == other.note_ && this->note_ == other.note_
&& this->control_ == other.control_ && this->control_ == other.control_
&& this->minControlValue_ == other.minControlValue_
&& this->maxControlValue_ == other.maxControlValue_
&& this->minValue_ == other.minValue_ && this->minValue_ == other.minValue_
&& this->maxValue_ == other.maxValue_ && this->maxValue_ == other.maxValue_
&& this->rotaryScale_ == other.rotaryScale_ && this->rotaryScale_ == other.rotaryScale_
@@ -111,6 +115,8 @@ public:
GETTER_SETTER(bindingType); GETTER_SETTER(bindingType);
GETTER_SETTER(note); GETTER_SETTER(note);
GETTER_SETTER(control); GETTER_SETTER(control);
GETTER_SETTER(minControlValue);
GETTER_SETTER(maxControlValue);
GETTER_SETTER(minValue); GETTER_SETTER(minValue);
GETTER_SETTER(maxValue); GETTER_SETTER(maxValue);
GETTER_SETTER(rotaryScale); GETTER_SETTER(rotaryScale);
@@ -120,9 +126,20 @@ public:
void switchControlType(SwitchControlTypeT value) { switchControlType_ = (int)value; } 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); DECLARE_JSON_MAP(MidiBinding);
+10
View File
@@ -136,6 +136,15 @@ bool Pedalboard::SetControlValue(int64_t pedalItemId, const std::string &symbol,
return item->SetControlValue(symbol,value); 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() PedalboardItem Pedalboard::MakeEmptyItem()
{ {
@@ -503,6 +512,7 @@ JSON_MAP_BEGIN(PedalboardItem)
JSON_MAP_REFERENCE(PedalboardItem,lv2State) JSON_MAP_REFERENCE(PedalboardItem,lv2State)
JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri) JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri)
JSON_MAP_REFERENCE(PedalboardItem,pathProperties) JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
JSON_MAP_REFERENCE(PedalboardItem,title)
JSON_MAP_END() JSON_MAP_END()
+4
View File
@@ -98,6 +98,7 @@ public:
Lv2PluginState lv2State_; Lv2PluginState lv2State_;
std::string lilvPresetUri_; std::string lilvPresetUri_;
std::map<std::string,std::string> pathProperties_; std::map<std::string,std::string> pathProperties_;
std::string title_;
// non persistent state. // non persistent state.
PropertyMap patchProperties; PropertyMap patchProperties;
@@ -126,6 +127,8 @@ public:
GETTER_SETTER_REF(midiChannelBinding) GETTER_SETTER_REF(midiChannelBinding)
GETTER_SETTER(stateUpdateCount) GETTER_SETTER(stateUpdateCount)
GETTER_SETTER_REF(lv2State) GETTER_SETTER_REF(lv2State)
GETTER_SETTER_REF(title)
Lv2PluginState&lv2State() { return lv2State_; } // non-const version. Lv2PluginState&lv2State() { return lv2State_; } // non-const version.
GETTER_SETTER_REF(lilvPresetUri) 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 INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume.
static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output 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 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); bool SetItemEnabled(int64_t pedalItemId, bool enabled);
void SetCurrentSnapshotModified(bool modified); void SetCurrentSnapshotModified(bool modified);
+34 -15
View File
@@ -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<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> 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) for (int i = 0; i < midiEventListeners.size(); ++i)
{ {
auto &listener = midiEventListeners[i]; auto &listener = midiEventListeners[i];
if ((!isNote) == (listener.listenForControls)) auto subscriber = this->GetNotificationSubscriber(listener.clientId);
if (subscriber)
{ {
auto subscriber = this->GetNotificationSubscriber(listener.clientId); subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2);
if (subscriber) }
{ else
subscriber->OnNotifyMidiListener(listener.clientHandle, isNote, noteOrControl); {
} midiEventListeners.erase(midiEventListeners.begin() + i);
else --i;
{
midiEventListeners.erase(midiEventListeners.begin() + i);
--i;
}
} }
} }
audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); 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<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
MidiListener listener{clientId, clientHandle, listenForControls}; MidiListener listener{clientId, clientHandle};
midiEventListeners.push_back(listener); midiEventListeners.push_back(listener);
audioHost->SetListenForMidiEvent(true); audioHost->SetListenForMidiEvent(true);
} }
@@ -2946,4 +2952,17 @@ void PiPedalModel::MoveAudioFile(
} }
AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory);
dir->MoveAudioFile(directory, fromPosition, toPosition); dir->MoveAudioFile(directory, fromPosition, toPosition);
} }
void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title)
{
std::lock_guard<std::recursive_mutex> 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);
}
+5 -4
View File
@@ -78,7 +78,7 @@ namespace pipedal
virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0; virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0;
virtual void OnLoadPluginPreset(int64_t instanceId, const std::vector<ControlValue> &controlValues) = 0; virtual void OnLoadPluginPreset(int64_t instanceId, const std::vector<ControlValue> &controlValues) = 0;
virtual void OnMidiValueChanged(int64_t instanceId, const std::string &symbol, float value) = 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 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 OnWifiConfigSettingsChanged(const WifiConfigSettings &wifiConfigSettings) = 0;
virtual void OnWifiDirectConfigSettingsChanged(const WifiDirectConfigSettings &wifiDirectConfigSettings) = 0; virtual void OnWifiDirectConfigSettingsChanged(const WifiDirectConfigSettings &wifiDirectConfigSettings) = 0;
@@ -146,7 +146,6 @@ namespace pipedal
public: public:
int64_t clientId; int64_t clientId;
int64_t clientHandle; int64_t clientHandle;
bool listenForControls;
}; };
class AtomOutputListener class AtomOutputListener
{ {
@@ -234,7 +233,7 @@ namespace pipedal
virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) override; virtual void OnNotifyVusSubscription(const std::vector<VuUpdate> &updates) override;
virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override;
virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) 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 OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override;
virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override;
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override;
@@ -437,7 +436,7 @@ namespace pipedal
JackServerSettings GetJackServerSettings(); JackServerSettings GetJackServerSettings();
void SetJackServerSettings(const JackServerSettings &jackServerSettings); 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 CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle);
void MonitorPatchProperty(int64_t clientId, int64_t clientHandle, uint64_t instanceId, const std::string &propertyUri); 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, const std::string & path,
int32_t from, int32_t from,
int32_t to); int32_t to);
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title);
}; };
} // namespace pipedal. } // namespace pipedal.
+33 -11
View File
@@ -174,18 +174,36 @@ JSON_MAP_REFERENCE(SetPatchPropertyBody, propertyUri)
JSON_MAP_REFERENCE(SetPatchPropertyBody, value) JSON_MAP_REFERENCE(SetPatchPropertyBody, value)
JSON_MAP_END() 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 class NotifyMidiListenerBody
{ {
public: public:
int64_t clientHandle_; int64_t clientHandle_;
bool isNote_; uint16_t cc0_;
int32_t noteOrControl_; uint16_t cc1_;
uint16_t cc2_;
DECLARE_JSON_MAP(NotifyMidiListenerBody); DECLARE_JSON_MAP(NotifyMidiListenerBody);
}; };
JSON_MAP_BEGIN(NotifyMidiListenerBody) JSON_MAP_BEGIN(NotifyMidiListenerBody)
JSON_MAP_REFERENCE(NotifyMidiListenerBody, clientHandle) JSON_MAP_REFERENCE(NotifyMidiListenerBody, clientHandle)
JSON_MAP_REFERENCE(NotifyMidiListenerBody, isNote) JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc0)
JSON_MAP_REFERENCE(NotifyMidiListenerBody, noteOrControl) JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc1)
JSON_MAP_REFERENCE(NotifyMidiListenerBody, cc2)
JSON_MAP_END() JSON_MAP_END()
class NotifyAtomOutputBody class NotifyAtomOutputBody
@@ -208,13 +226,11 @@ JSON_MAP_END()
class ListenForMidiEventBody class ListenForMidiEventBody
{ {
public: public:
bool listenForControls_;
int64_t handle_; int64_t handle_;
DECLARE_JSON_MAP(ListenForMidiEventBody); DECLARE_JSON_MAP(ListenForMidiEventBody);
}; };
JSON_MAP_BEGIN(ListenForMidiEventBody) JSON_MAP_BEGIN(ListenForMidiEventBody)
JSON_MAP_REFERENCE(ListenForMidiEventBody, listenForControls)
JSON_MAP_REFERENCE(ListenForMidiEventBody, handle) JSON_MAP_REFERENCE(ListenForMidiEventBody, handle)
JSON_MAP_END() JSON_MAP_END()
@@ -1099,7 +1115,7 @@ public:
{ {
ListenForMidiEventBody body; ListenForMidiEventBody body;
pReader->read(&body); pReader->read(&body);
this->model.ListenForMidiEvent(this->clientId, body.handle_, body.listenForControls_); this->model.ListenForMidiEvent(this->clientId, body.handle_);
} }
else if (message == "cancelListenForMidiEvent") else if (message == "cancelListenForMidiEvent")
{ {
@@ -1503,7 +1519,12 @@ public:
{ this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error) { this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error)
{ this->SendError(replyTo, error.c_str()); }); { 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") else if (message == "getPatchProperty")
{ {
GetPatchPropertyBody body; GetPatchPropertyBody body;
@@ -2121,12 +2142,13 @@ private:
Send("onNotifyPathPatchPropertyChanged", body); 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; NotifyMidiListenerBody body;
body.clientHandle_ = clientHandle; body.clientHandle_ = clientHandle;
body.isNote_ = isNote; body.cc0_ = cc0;
body.noteOrControl_ = noteOrControl; body.cc1_ = cc1;
body.cc2_ = cc2;
Send("onNotifyMidiListener", body); Send("onNotifyMidiListener", body);
} }
+20 -8
View File
@@ -31,6 +31,20 @@ namespace pipedal
{ {
class IndexedSnapshot; 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 enum class RingBufferCommand : int64_t
{ {
Invalid = 0, Invalid = 0,
@@ -38,7 +52,7 @@ namespace pipedal
EffectReplaced, EffectReplaced,
SetValue, SetValue,
SetBypass, SetBypass,
//AudioStopped, // AudioStopped,
AudioTerminatedAbnormally, // specifically for an ALSA loss of connection. AudioTerminatedAbnormally, // specifically for an ALSA loss of connection.
SetVuSubscriptions, SetVuSubscriptions,
FreeVuSubscriptions, FreeVuSubscriptions,
@@ -327,7 +341,7 @@ namespace pipedal
return; return;
} }
} }
template <typename T> template <typename T>
void write(RingBufferCommand command, const T &value, size_t dataLength, uint8_t *variableData) void write(RingBufferCommand command, const T &value, size_t dataLength, uint8_t *variableData)
{ {
@@ -374,12 +388,9 @@ namespace pipedal
write(RingBufferCommand::MidiValueChanged, body); write(RingBufferCommand::MidiValueChanged, body);
} }
void OnMidiListen(bool isNote, uint8_t noteOrControl) void OnMidiListen(const MidiNotifyBody &body)
{ {
uint16_t msg = noteOrControl; write(RingBufferCommand::OnMidiListen, body);
if (isNote)
msg |= 0x100;
write(RingBufferCommand::OnMidiListen, msg);
} }
/** /**
@@ -485,7 +496,8 @@ namespace pipedal
{ {
write(RingBufferCommand::AckMidiProgramChange, requestId); write(RingBufferCommand::AckMidiProgramChange, requestId);
} }
void AckMidiSnapshotRequest(uint64_t snapshotRequestId) { void AckMidiSnapshotRequest(uint64_t snapshotRequestId)
{
write(RingBufferCommand::AckMidiSnapshotRequest, snapshotRequestId); write(RingBufferCommand::AckMidiSnapshotRequest, snapshotRequestId);
} }
-10
View File
@@ -1,19 +1,9 @@
window.addEventListener .... done wrong everywhere!
touch scroll in file propertty dialog.
channel bindings help dialog. <ChannelBindingHelpDialog
draggable UI: scrolloffset!?
- pipewire aux in? - pipewire aux in?
Check that OnNotifyPatchProperty still worsk for
- FilePropertyControl
- PowerStage2.
- ToobSpectrumAnaylyzer.
pcm.pipedal_aux_in { pcm.pipedal_aux_in {
type file type file
+8
View File
@@ -25,6 +25,9 @@ export default class MidiBinding {
this.bindingType = input.bindingType; this.bindingType = input.bindingType;
this.note = input.note; this.note = input.note;
this.control = input.control; this.control = input.control;
this.minControlValue = input.minControlValue?? 0;
this.maxControlValue = input.maxControlValue?? 127;
this.rotaryScale = input.rotaryScale?? 1;
this.minValue = input.minValue; this.minValue = input.minValue;
this.maxValue = input.maxValue; this.maxValue = input.maxValue;
this.linearControlType = input.linearControlType; this.linearControlType = input.linearControlType;
@@ -53,6 +56,9 @@ export default class MidiBinding {
&& (this.note === other.note) && (this.note === other.note)
&& (this.control === other.control) && (this.control === other.control)
&& (this.minValue === other.minValue) && (this.minValue === other.minValue)
&& (this.minControlValue === other.minControlValue)
&& (this.maxControlValue === other.maxControlValue)
&& (this.rotaryScale === other.rotaryScale)
&& (this.maxValue === other.maxValue) && (this.maxValue === other.maxValue)
&& (this.linearControlType === other.linearControlType) && (this.linearControlType === other.linearControlType)
&& (this.switchControlType === other.switchControlType) && (this.switchControlType === other.switchControlType)
@@ -73,6 +79,8 @@ export default class MidiBinding {
bindingType: number = MidiBinding.BINDING_TYPE_NONE; bindingType: number = MidiBinding.BINDING_TYPE_NONE;
note: number = 12*4+24; // C4. note: number = 12*4+24; // C4.
control: number = 1; control: number = 1;
minControlValue: number = 0;
maxControlValue: number = 127;
minValue: number = 0; minValue: number = 0;
maxValue: number = 1; maxValue: number = 1;
rotaryScale: number = 1; rotaryScale: number = 1;
+311 -66
View File
@@ -18,11 +18,12 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { Component } from 'react'; import { Component } from 'react';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { ListenHandle, MidiMessage, PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui"; import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import Snackbar from '@mui/material/Snackbar';
import { UiPlugin } from './Lv2Plugin'; import { UiPlugin } from './Lv2Plugin';
@@ -47,7 +48,7 @@ const styles = (theme: Theme) => createStyles({
} }
}); });
enum MidiControlType { export enum MidiControlType {
None, None,
Select, Select,
Dial, Dial,
@@ -56,18 +57,57 @@ enum MidiControlType {
MomentarySwitch 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<typeof styles> { interface MidiBindingViewProps extends WithStyles<typeof styles> {
instanceId: number; instanceId: number;
listen: boolean;
midiBinding: MidiBinding; midiBinding: MidiBinding;
uiPlugin: UiPlugin; midiControlType: MidiControlType;
onChange: (instanceId: number, newBinding: MidiBinding) => void; onChange: (instanceId: number, newBinding: MidiBinding) => void;
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
} }
interface MidiBindingViewState { interface MidiBindingViewState {
midiControlType: MidiControlType; listenSymbol: string;
listenForRangeSymbol: string;
listenForRangeMax: number;
listenForRangeMin: number;
listenSnackbarOpen: boolean;
} }
@@ -83,7 +123,11 @@ const MidiBindingView =
super(props); super(props);
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
this.state = { this.state = {
midiControlType: this.getControlType() listenSymbol: "",
listenForRangeSymbol: "",
listenForRangeMax: 127,
listenForRangeMin: 0,
listenSnackbarOpen: false
}; };
} }
@@ -139,6 +183,16 @@ const MidiBindingView =
newBinding.maxValue = value; newBinding.maxValue = value;
this.props.onChange(this.props.instanceId, newBinding); 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 { handleScaleChange(value: number): void {
let newBinding = this.props.midiBinding.clone(); let newBinding = this.props.midiBinding.clone();
newBinding.rotaryScale = value; newBinding.rotaryScale = value;
@@ -158,10 +212,23 @@ const MidiBindingView =
return result; return result;
} }
mounted: boolean = false;
componentDidMount() {
super.componentDidMount?.();
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
this.cancelListenForControl();
super.componentWillUnmount?.();
}
validateSwitchControlType(midiBinding: MidiBinding) { validateSwitchControlType(midiBinding: MidiBinding) {
// :-( // :-(
let controlType = this.state.midiControlType; let controlType = this.props.midiControlType;
if (controlType === MidiControlType.Toggle if (controlType === MidiControlType.Toggle
&& midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) { && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) {
if (midiBinding.switchControlType !== MidiBinding.TRIGGER_ON_RISING_EDGE // Toggle on Note On. if (midiBinding.switchControlType !== MidiBinding.TRIGGER_ON_RISING_EDGE // Toggle on Note On.
@@ -181,41 +248,154 @@ const MidiBindingView =
} }
getControlType(): MidiControlType { getControlType(): MidiControlType {
return this.props.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;
} }
///////////////////////////////////////
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() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let midiBinding = this.props.midiBinding; let midiBinding = this.props.midiBinding;
let uiPlugin = this.props.uiPlugin; if (this.props.midiControlType === MidiControlType.None) {
if (!uiPlugin) {
return (<div />); return (<div />);
} }
let controlType = this.state.midiControlType; let controlType = this.props.midiControlType;
let showLinearRange = let showLinearRange =
controlType === MidiControlType.Dial && controlType === MidiControlType.Dial &&
@@ -238,9 +418,7 @@ const MidiBindingView =
value={midiBinding.bindingType} value={midiBinding.bindingType}
> >
<MenuItem value={0}>None</MenuItem> <MenuItem value={0}>None</MenuItem>
{(controlType === MidiControlType.Toggle {(canBindToNote(this.props.midiControlType)) && (
|| controlType === MidiControlType.Trigger
|| controlType === MidiControlType.MomentarySwitch) && (
<MenuItem value={1}>Note</MenuItem> <MenuItem value={1}>Note</MenuItem>
)} )}
<MenuItem value={2}>Control</MenuItem> <MenuItem value={2}>Control</MenuItem>
@@ -262,15 +440,15 @@ const MidiBindingView =
</Select> </Select>
<IconButtonEx <IconButtonEx
tooltip="Listen for MIDI input" tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => { onClick={() => {
if (this.props.listen) { this.handleListen()
this.props.onListen(-2, "", false)
} else {
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
}
}} }}
size="large"> size="large">
{this.props.listen ? ( {this.state.listenSymbol !== "" ? (
<MicOutlinedIcon /> <MicOutlinedIcon />
) : ( ) : (
<MicNoneOutlinedIcon /> <MicNoneOutlinedIcon />
@@ -296,15 +474,15 @@ const MidiBindingView =
</Select> </Select>
<IconButtonEx <IconButtonEx
tooltip="Listen for MIDI input" tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => { onClick={() => {
if (this.props.listen) { this.handleListen()
this.props.onListen(-2, "", false)
} else {
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, true)
}
}} }}
size="large"> size="large">
{this.props.listen ? ( {this.state.listenSymbol !== "" ? (
<MicOutlinedIcon /> <MicOutlinedIcon />
) : ( ) : (
<MicNoneOutlinedIcon /> <MicNoneOutlinedIcon />
@@ -314,6 +492,27 @@ const MidiBindingView =
</div> </div>
) )
} }
{midiBinding.bindingType === MidiBinding.BINDING_TYPE_NONE &&
(
<IconButtonEx
tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
this.handleListen()
}}
size="large">
{this.state.listenSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
)
}
{ {
((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) && ((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
( (
@@ -378,25 +577,58 @@ const MidiBindingView =
<MenuItem value={MidiBinding.CIRCULAR_CONTROL_TYPE}>Rotary</MenuItem> <MenuItem value={MidiBinding.CIRCULAR_CONTROL_TYPE}>Rotary</MenuItem>
</Select> </Select>
</div> </div>
)}
) {showLinearRange && (
} <div style={{ display: "flex", flexFlow: "row wrap", alignItems: "center" }}>
{
showLinearRange && (
<div className={classes.controlDiv}> <div className={classes.controlDiv}>
<Typography display="inline">Min:&nbsp;</Typography> <Typography display="inline" noWrap>Min Ctl:&nbsp;</Typography>
<NumericInput defaultValue={midiBinding.minValue} ariaLabel='min' <NumericInput integer value={midiBinding.minControlValue} ariaLabel='min ctl val'
min={0} max={1} onChange={(value) => { this.handleMinChange(value); }} min={0} max={127} step={1} onChange={(value) => { this.handleCtlMinChange(value); }}
/> />
</div> </div>
) <div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
<div className={classes.controlDiv}>
<Typography display="inline" noWrap>Max Ctl:&nbsp;</Typography>
<NumericInput value={midiBinding.maxControlValue} ariaLabel='max ctl val'
integer={true}
min={0} max={127} step={1} onChange={(value) => { this.handleCtlMaxChange(value); }}
/>
</div>
<IconButtonEx
tooltip="Listen for control range"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
this.handleListenForControlRange(this.props.instanceId, this.props.midiBinding.symbol);
}}
size="large">
{this.state.listenForRangeSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
</div>
</div>
)
} }
{ {
showLinearRange && ( showLinearRange && (
<div className={classes.controlDiv}> <div style={{ display: "flex", flexFlow: "row wrap", alignItems: "center" }}>
<Typography display="inline">Max:&nbsp;</Typography> <div className={classes.controlDiv}>
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='max' <Typography noWrap display="inline">Min Val:&nbsp;</Typography>
min={0} max={1} onChange={(value) => { this.handleMaxChange(value); }} /> <NumericInput value={midiBinding.minValue} ariaLabel='min'
min={0} max={1} onChange={(value) => { this.handleMinChange(value); }}
/>
</div>
<div className={classes.controlDiv}>
<Typography noWrap display="inline">Max Val:&nbsp;</Typography>
<NumericInput value={midiBinding.maxValue} ariaLabel='max'
min={0} max={1} onChange={(value) => { this.handleMaxChange(value); }} />
</div>
</div> </div>
) )
} }
@@ -404,11 +636,24 @@ const MidiBindingView =
canRotaryScale && ( canRotaryScale && (
<div className={classes.controlDiv}> <div className={classes.controlDiv}>
<Typography display="inline">Scale:&nbsp;</Typography> <Typography display="inline">Scale:&nbsp;</Typography>
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='scale' <NumericInput value={midiBinding.maxValue} ariaLabel='scale'
min={-100} max={100} onChange={(value) => { this.handleScaleChange(value); }} /> min={-100} max={100} onChange={(value) => { this.handleScaleChange(value); }} />
</div> </div>
) )
} }
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.listenSnackbarOpen}
autoHideDuration={1500}
onClose={() => this.setState({ listenSnackbarOpen: false })}
message="Listening for MIDI input"
/>
</div> </div>
); );
+10 -88
View File
@@ -20,7 +20,7 @@
import React, { SyntheticEvent } from 'react'; import React, { SyntheticEvent } from 'react';
import DialogEx from './DialogEx'; import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
@@ -32,8 +32,7 @@ import Toolbar from '@mui/material/Toolbar';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx'; import IconButtonEx from './IconButtonEx';
import MidiBinding from './MidiBinding'; import MidiBinding from './MidiBinding';
import MidiBindingView from './MidiBindingView'; import MidiBindingView, { getMidiControlType } from './MidiBindingView';
import Snackbar from '@mui/material/Snackbar';
import { PortGroup, UiPlugin, makeSplitUiPlugin } from './Lv2Plugin'; import { PortGroup, UiPlugin, makeSplitUiPlugin } from './Lv2Plugin';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
@@ -81,9 +80,6 @@ export interface MidiBindingDialogProps extends WithStyles<typeof styles> {
} }
export interface MidiBindingDialogState { export interface MidiBindingDialogState {
listenInstanceId: number;
listenSymbol: string;
listenSnackbarOpen: boolean;
} }
@@ -98,7 +94,10 @@ export const MidiBindingDialog =
this.state = { this.state = {
listenInstanceId: -2, listenInstanceId: -2,
listenSymbol: "", listenSymbol: "",
listenSnackbarOpen: false listenForRangeSymbol: "",
listenSnackbarOpen: false,
listenForRangeMin: 127,
listenForRangeMax: 0
}; };
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
this.handleClose = this.handleClose.bind(this); this.handleClose = this.handleClose.bind(this);
@@ -108,64 +107,9 @@ export const MidiBindingDialog =
hasHooks: boolean = false; hasHooks: boolean = false;
handleClose() { handleClose() {
this.cancelListenForControl();
this.props.onClose(); 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 { onWindowSizeChanged(width: number, height: number): void {
} }
@@ -176,10 +120,11 @@ export const MidiBindingDialog =
this.mounted = true; this.mounted = true;
} }
componentWillUnmount() { componentWillUnmount() {
super.componentWillUnmount();
this.mounted = false; this.mounted = false;
super.componentWillUnmount();
} }
componentDidUpdate() { componentDidUpdate() {
@@ -255,15 +200,7 @@ export const MidiBindingDialog =
</td> </td>
<td className={classes.bindingTd}> <td className={classes.bindingTd}>
<MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")} <MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")}
uiPlugin={plugin} midiControlType={ getMidiControlType(plugin, "__bypass") }
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
if (instanceId === -2) {
this.cancelListenForControl();
} else {
this.handleListenForControl(instanceId, symbol, listenForControl);
}
}}
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === "__bypass"}
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/> />
</td> </td>
@@ -305,13 +242,8 @@ export const MidiBindingDialog =
</td> </td>
<td className={classes.bindingTd}> <td className={classes.bindingTd}>
<MidiBindingView instanceId={item.instanceId} <MidiBindingView instanceId={item.instanceId}
uiPlugin={plugin} midiControlType={getMidiControlType(plugin, symbol)}
midiBinding={item.getMidiBinding(symbol)} midiBinding={item.getMidiBinding(symbol)}
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === symbol}
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
this.handleListenForControl(instanceId, symbol, listenForControl);
}}
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/> />
</td> </td>
@@ -379,16 +311,6 @@ export const MidiBindingDialog =
</table> </table>
</div> </div>
</div> </div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.listenSnackbarOpen}
autoHideDuration={1500}
onClose={() => this.setState({ listenSnackbarOpen: false })}
message="Listening for MIDI input"
/>
</DialogEx > </DialogEx >
); );
} }
+42 -18
View File
@@ -35,14 +35,18 @@ const styles = ({ palette }: Theme) => createStyles({
interface NumericInputProps extends WithStyles<typeof styles> { interface NumericInputProps extends WithStyles<typeof styles> {
ariaLabel: string; ariaLabel: string;
defaultValue: number; value: number;
min: number; min: number;
max: number; max: number;
step?: number;
integer?: boolean;
onChange: (value: number) => void; onChange: (value: number) => void;
} }
interface NumericInputState { interface NumericInputState {
stateValue: string;
error: boolean; error: boolean;
focused: boolean;
} }
export const NumericInput = export const NumericInput =
@@ -54,22 +58,28 @@ export const NumericInput =
constructor(props: NumericInputProps) { constructor(props: NumericInputProps) {
super(props); super(props);
this.state = { this.state = {
error: false stateValue: this.toDisplayValue(props.value),
error: false,
focused: false
}; };
} }
changed: boolean = false; changed: boolean = false;
handleChange(event: any): void { handleChange(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {
this.changed = true; this.changed = true;
let strValue = event.target.value; let strValue = event.target.value;
this.setState({ stateValue: strValue });
try { try {
let value = Number(strValue); let value = Number(strValue);
if (isNaN(value)) if (isNaN(value))
{ {
this.setState({ error: true }); 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) { } else if (value >= this.props.min && value <= this.props.max) {
this.setState({ error: false }); this.setState({ error: false });
this.props.onChange(value);
} else { } else {
this.setState({ error: true }); this.setState({ error: true });
} }
@@ -80,6 +90,10 @@ export const NumericInput =
} }
toDisplayValue(value: number): string { toDisplayValue(value: number): string {
if (this.props.integer === true)
{
return Math.round(value).toFixed(0);
}
if (value <= -1000 || value >= 1000) if (value <= -1000 || value >= 1000)
{ {
return value.toFixed(0); return value.toFixed(0);
@@ -96,7 +110,7 @@ export const NumericInput =
} }
} }
apply(input: HTMLInputElement) { apply(input: HTMLInputElement| HTMLTextAreaElement) {
if (this.changed) { if (this.changed) {
let strValue = input.value; let strValue = input.value;
let value = Number(strValue); let value = Number(strValue);
@@ -104,28 +118,34 @@ export const NumericInput =
{ {
if (value < this.props.min) value = this.props.min; if (value < this.props.min) value = this.props.min;
if (value > this.props.max) value = this.props.max; if (value > this.props.max) value = this.props.max;
if (this.props.integer === true) value = Math.round(value);
input.value = this.toDisplayValue(value); input.value = this.toDisplayValue(value);
this.props.onChange(value); this.props.onChange(value);
} else { } 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.changed = false;
this.setState({ error: false }); this.setState({ error: false });
} }
} }
handleLostFocus(e: any) { handleFocus(e: React.FocusEvent<HTMLInputElement| HTMLTextAreaElement>) {
this.apply(e.currentTarget as HTMLInputElement); this.changed = false;
this.setState({ focused: true });
e.currentTarget.select();
this.setState({ stateValue: this.toDisplayValue(this.props.value) });
} }
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { handleBlur(e: any) {
this.apply(e.currentTarget as HTMLInputElement);
this.setState({ focused: false });
}
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) {
if (e.key === "Enter") if (e.key === "Enter")
{ {
this.apply(e.currentTarget as HTMLInputElement); this.setState({ stateValue: this.toDisplayValue(this.props.value) });
} else if (e.key === "Escape") this.changed = false;
{ }
e.currentTarget.value = this.toDisplayValue(this.props.defaultValue);
this.changed = false;
}
} }
@@ -135,11 +155,15 @@ export const NumericInput =
inputProps={{ inputProps={{
'aria-label': this.props.ariaLabel, 'aria-label': this.props.ariaLabel,
style: { textAlign: 'right' }, style: { textAlign: 'right' },
"onBlur": (e: any) => this.handleLostFocus(e), "min": this.props.min,
"onKeyDown": (e: any) => this.handleKeyDown(e) "max": this.props.max,
"step:": this.props.step
}} }}
onChange={(event) => this.handleChange(event)} onFocus={(e) => {this.handleFocus(e);}}
defaultValue={this.toDisplayValue(this.props.defaultValue)} 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} error={this.state.error}
/> />
+39 -15
View File
@@ -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 { class MidiEventListener {
constructor(handle: number, callback: (isNote: boolean, noteOrControl: number) => void) { constructor(handle: number, callback: (message: MidiMessage) => void) {
this.handle = handle; this.handle = handle;
this.callback = callback; this.callback = callback;
} }
handle: number; handle: number;
callback: (isNote: boolean, noteOrControl: number) => void; callback: (midiMessage: MidiMessage) => void;
}; };
export type PatchPropertyListener = (instanceId: number, propertyUri: string, atomObject: any) => void; export type PatchPropertyListener = (instanceId: number, propertyUri: string, atomObject: any) => void;
@@ -679,10 +696,13 @@ export class PiPedalModel //implements PiPedalModel
} }
} else if (message === "onNotifyMidiListener") { } else if (message === "onNotifyMidiListener") {
let clientHandle = body.clientHandle as number; let notifyBody = body as { clientHandle: number, cc0: number, cc1: number, cc2: number };
let isNote = body.isNote as boolean; let clientHandle = notifyBody.clientHandle as number;
let noteOrControl = body.noteOrControl as number; this.handleNotifyMidiListener(
this.handleNotifyMidiListener(clientHandle, isNote, noteOrControl); clientHandle,
notifyBody.cc0,
notifyBody.cc1,
notifyBody.cc2);
} else if (message === "onNotifyPathPatchPropertyChanged") { } else if (message === "onNotifyPathPatchPropertyChanged") {
let instanceId = body.instanceId as number; let instanceId = body.instanceId as number;
let propertyUri = body.propertyUri as string; let propertyUri = body.propertyUri as string;
@@ -2355,12 +2375,12 @@ export class PiPedalModel //implements PiPedalModel
private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = []; private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = [];
nextListenHandle = 1; nextListenHandle = 1;
listenForMidiEvent(listenForControl: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle { listenForMidiEvent(onComplete: (midiMessage: MidiMessage) => void): ListenHandle {
let handle = this.nextListenHandle++; let handle = this.nextListenHandle++;
this.midiListeners.push(new MidiEventListener(handle, onComplete)); this.midiListeners.push(new MidiEventListener(handle, onComplete));
this.webSocket?.send("listenForMidiEvent", { listenForControls: listenForControl, handle: handle }); this.webSocket?.send("listenForMidiEvent", { handle: handle });
return { return {
_handle: handle _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) { for (let i = 0; i < this.midiListeners.length; ++i) {
let listener = this.midiListeners[i]; let listener = this.midiListeners[i];
if (listener.handle === clientHandle) { 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; let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = url; link.href = url;
link.target = "_blank"; link.target = "_blank";
link.download = "download.piPreset"; link.download = "download.piPreset";
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
@@ -2705,8 +2729,8 @@ export class PiPedalModel //implements PiPedalModel
}); });
} }
copyFilePropertyFile( copyFilePropertyFile(
oldRelativePath: string, oldRelativePath: string,
newRelativePath: string, newRelativePath: string,
uiFileProperty: UiFileProperty, uiFileProperty: UiFileProperty,
overwrite: boolean overwrite: boolean
): Promise<string> { ): Promise<string> {
@@ -3167,7 +3191,7 @@ export class PiPedalModel //implements PiPedalModel
item.title = title; item.title = title;
this.pedalboard.set(newPedalboard); this.pedalboard.set(newPedalboard);
// notify the server. // notify the server.
// xxx; this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title });
} }
}; };
+10 -189
View File
@@ -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 MidiBinding from './MidiBinding';
import Utility from './Utility'; import MidiBindingView, {MidiControlType} from './MidiBindingView';
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; interface SystemMidiBindingViewProps {
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<typeof styles> {
instanceId: number; instanceId: number;
listen: boolean;
midiBinding: MidiBinding; midiBinding: MidiBinding;
onChange: (instanceId: number, newBinding: MidiBinding) => void; onChange: (instanceId: number, newBinding: MidiBinding) => void;
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
} }
function SystemMidiBindingView(props: SystemMidiBindingViewProps) {
return (
interface SystemMidiBindingViewState { <MidiBindingView
instanceId={props.instanceId}
midiBinding={props.midiBinding}
onChange={props.onChange}
midiControlType={MidiControlType.Trigger}
/>)
} }
const SystemMidiBindingView =
withStyles(
class extends Component<SystemMidiBindingViewProps, SystemMidiBindingViewState> {
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(
<MenuItem value={i}>{Utility.midiNoteName(i)}</MenuItem>
)
}
return result;
}
generateControlSelects(): React.ReactNode[] {
return Utility.validMidiControllers.map((control) => (
<MenuItem key={control.value} value={control.value}>{control.displayName}</MenuItem>
)
);
}
render() {
const classes = withStyles.getClasses(this.props);
let midiBinding = this.props.midiBinding;
return (
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "left", alignItems: "center", minHeight: 48 }}>
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80, }}
onChange={(e, extra) => this.handleTypeChange(e, extra)}
value={midiBinding.bindingType}
>
<MenuItem value={0}>None</MenuItem>
<MenuItem value={1}>Note</MenuItem>
<MenuItem value={2}>Control</MenuItem>
</Select>
</div>
{
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
(
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80, verticalAlign: 'center' }}
onChange={(e, extra) => this.handleNoteChange(e, extra)}
value={midiBinding.note}
>
{
this.generateMidiSelects()
}
</Select>
</div>
)
}
{
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
(
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80 }}
onChange={(e, extra) => this.handleControlChange(e, extra)}
value={midiBinding.control}
renderValue={(value) => { return "CC-" + value }}
>
{
this.generateControlSelects()
}
</Select>
</div>
)
}
<IconButtonEx
tooltip="Listen for MIDI input"
onClick={() => {
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 ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
</div>
);
}
},
styles
);
export default SystemMidiBindingView; export default SystemMidiBindingView;
+1 -66
View File
@@ -27,7 +27,7 @@ import React, { SyntheticEvent } from 'react';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import DialogEx from './DialogEx'; import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
@@ -182,66 +182,9 @@ export const SystemMidiBindingDialog =
hasHooks: boolean = false; hasHooks: boolean = false;
handleClose() { handleClose() {
this.cancelListenForControl();
this.props.onClose(); 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 { onWindowSizeChanged(width: number, height: number): void {
} }
@@ -293,14 +236,6 @@ export const SystemMidiBindingDialog =
</td> </td>
<td className={classes.bindingTd}> <td className={classes.bindingTd}>
<SystemMidiBindingView instanceId={item.instanceId} midiBinding={item.midiBinding} <SystemMidiBindingView instanceId={item.instanceId} midiBinding={item.midiBinding}
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
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)} onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/> />
</td> </td>
+21 -16
View File
@@ -48,9 +48,8 @@ function ToolTipEx(props: ToolTipExProps) {
setTimeoutInstance(timeoutInstance + 1); // make useeffect run. setTimeoutInstance(timeoutInstance + 1); // make useeffect run.
} }
function stopTimeout() { function stopTimeout() {
if (timeout > 0) { setTimeout(0);
startTimeout(0); setTimeoutInstance(0);
}
} }
function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) { function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) {
setOpen(false); setOpen(false);
@@ -60,6 +59,7 @@ function ToolTipEx(props: ToolTipExProps) {
function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) { function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) {
setOpen(false); setOpen(false);
stopTimeout(); stopTimeout();
setLongPressLeaving(false);
} }
function handlePointerDownCapture(event: React.PointerEvent<HTMLDivElement>) { function handlePointerDownCapture(event: React.PointerEvent<HTMLDivElement>) {
@@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) {
if (valueTooltip === undefined) // no timeout if there's a value tooltip if (valueTooltip === undefined) // no timeout if there's a value tooltip
{ {
if (t > 0) { if (t > 0) {
// console.log("ToolTipEx: starting timeout for ", t); console.log("ToolTipEx: starting timeout for ", t);
handle = window.setTimeout(() => { handle = window.setTimeout(() => {
setOpen(true); setOpen(true);
},t); },t);
@@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) {
} }
return () => { return () => {
if (handle !== null) { if (handle !== null) {
// console.log("ToolTipEx: clearing timeout for ", t); console.log("ToolTipEx: clearing timeout for ", t);
window.clearTimeout(handle); window.clearTimeout(handle);
} }
}; };
@@ -179,41 +179,46 @@ function ToolTipEx(props: ToolTipExProps) {
stopTimeout(); // Reset hover timeout on click stopTimeout(); // Reset hover timeout on click
} }
return ( return (
<div <div style={{ display: 'inline-block' }}
onClickCapture={(e) => { onClickCapture={(e) => {
// console.log("ToolTipEx: onClickCapture"); console.log("ToolTipEx: onClickCapture");
handleClickCapture(e); handleClickCapture(e);
setTimeout(0);
setOpen(false);
setIsLongPress(false);
setLongPressLeaving(true);
}} }}
onMouseEnter={(e)=> { onMouseEnter={(e)=> {
// console.log("ToolTipEx: onMouseEnter"); console.log("ToolTipEx: onMouseEnter");
if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state
handleMouseEnter(e); handleMouseEnter(e);
}
}} }}
onMouseLeave={(e)=> { onMouseLeave={(e)=> {
// console.log("ToolTipEx: onMouseLeave"); console.log("ToolTipEx: onMouseLeave");
handleMouseLeave(e); handleMouseLeave(e);
}} }}
onPointerCancelCapture={(e) => { onPointerCancelCapture={(e) => {
// console.log("ToolTipEx: onPointerCancelCapture"); console.log("ToolTipEx: onPointerCancelCapture");
handlePointerCancel(e); handlePointerCancel(e);
}} }}
onContextMenuCapture={(e) => { onContextMenuCapture={(e) => {
// console.log("ToolTipEx: onContextMenuCapture"); console.log("ToolTipEx: onContextMenuCapture");
handleConextMenu(e); handleConextMenu(e);
return false; return false;
}} }}
onPointerDownCapture={(e) => { onPointerDownCapture={(e) => {
// console.log("ToolTipEx: onPointerDownCapture"); console.log("ToolTipEx: onPointerDownCapture");
handlePointerDownCapture(e); handlePointerDownCapture(e);
}} }}
onPointerMoveCapture={(e) => { onPointerMoveCapture={(e) => {
// console.log("ToolTipEx: onPointerMoveCapture"); console.log("ToolTipEx: onPointerMoveCapture");
handlePointerMoveCapture(e); handlePointerMoveCapture(e);
}} }}
onPointerUpCapture={(e) => { onPointerUpCapture={(e) => {
// console.log("ToolTipEx: onPointerUpCapture"); console.log("ToolTipEx: onPointerUpCapture");
handlePointerUpCapture(e); handlePointerUpCapture(e);
}} }}
> >