From c69bec6dcf9594230d16c3c1fb2a80803243c011 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Wed, 21 May 2025 16:59:17 -0400 Subject: [PATCH] Graphic EQ controls; focus/click bugs. --- src/Lv2Pedalboard.cpp | 1 + src/PiPedalUI.hpp | 2 + src/PluginHost.cpp | 7 + src/PluginHost.hpp | 6 + vite/src/pipedal/GraphicEqCtl.tsx | 111 ++++++++++++++ vite/src/pipedal/Lv2Plugin.tsx | 12 +- vite/src/pipedal/PluginControl.tsx | 234 +++++++++++++++++++++-------- 7 files changed, 307 insertions(+), 66 deletions(-) create mode 100644 vite/src/pipedal/GraphicEqCtl.tsx diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 8bed348..fbb1950 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -324,6 +324,7 @@ void Lv2Pedalboard::PrepareMidiMap(const PedalboardItem &pedalboardItem) mapping.midiBinding = binding; mapping.instanceId = pedalboardItem.instanceId(); + if (pPortInfo->mod_momentaryOffByDefault() || pPortInfo->mod_momentaryOnByDefault()) { mapping.mappingType = MidiControlType::MomentarySwitch; diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index df203f2..a39d688 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -67,6 +67,8 @@ #define PIPEDAL_UI__ledColor PIPEDAL_UI_PREFIX "ledColor" +#define PIPEDAL_UI__graphicEq PIPEDAL_UI_PREFIX "graphicEq" + namespace pipedal { diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index b8a9133..08077f3 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -159,6 +159,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom); pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog); pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width); + pipedalUI__graphicEq = lilv_new_uri(pWorld,PIPEDAL_UI__graphicEq); ui__portNotification = lilv_new_uri(pWorld, LV2_UI__portNotification); ui__plugin = lilv_new_uri(pWorld, LV2_UI__plugin); @@ -1005,8 +1006,12 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri); this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault); this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault); + this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq); + + this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri); + this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled); this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); @@ -1729,6 +1734,7 @@ json_map::storage_type Lv2PortInfo::jmap{ MAP_REF(Lv2PortInfo, toggled_property), MAP_REF(Lv2PortInfo, mod_momentaryOffByDefault), MAP_REF(Lv2PortInfo, mod_momentaryOnByDefault), + MAP_REF(Lv2PortInfo, pipedal_graphicEq), MAP_REF(Lv2PortInfo, not_on_gui), MAP_REF(Lv2PortInfo, buffer_type), MAP_REF(Lv2PortInfo, port_group), @@ -1796,6 +1802,7 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ MAP_REF(Lv2PluginUiPort, mod_momentaryOffByDefault), MAP_REF(Lv2PluginUiPort, mod_momentaryOnByDefault), + MAP_REF(Lv2PluginUiPort, pipedal_graphicEq), MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, not_on_gui), MAP_REF(Lv2PluginUiPort, toggled_property), diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index c6dc501..ddc5d84 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -221,6 +221,7 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool pipedal_graphicEq_ = false; bool not_on_gui_ = false; std::string buffer_type_; @@ -294,6 +295,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); + LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(not_on_gui); @@ -513,6 +515,7 @@ namespace pipedal integer_property_(pPort->integer_property()), mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()), mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()), + pipedal_graphicEq_(pPort->pipedal_graphicEq()), enumeration_property_(pPort->enumeration_property()), toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), @@ -558,6 +561,8 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool pipedal_graphicEq_ = false; + bool enumeration_property_ = false; bool not_on_gui_ = false; bool toggled_property_ = false; @@ -728,6 +733,7 @@ namespace pipedal AutoLilvNode pipedalUI__yTop; AutoLilvNode pipedalUI__yBottom; AutoLilvNode pipedalUI__width; + AutoLilvNode pipedalUI__graphicEq; AutoLilvNode pipedalUI__outputPorts; diff --git a/vite/src/pipedal/GraphicEqCtl.tsx b/vite/src/pipedal/GraphicEqCtl.tsx new file mode 100644 index 0000000..289e14e --- /dev/null +++ b/vite/src/pipedal/GraphicEqCtl.tsx @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import SvgPathBuilder from "./SvgPathBuilder"; + +export interface GraphicEqCtlProps { + position: number; + imgRef: React.Ref | undefined; + onTouchStart: React.TouchEventHandler | undefined; + onTouchMove: React.TouchEventHandler | undefined; + onPointerDown: React.PointerEventHandler | undefined; + onPointerUp: React.PointerEventHandler | undefined; + onPointerMoveCapture: React.PointerEventHandler | undefined; + onDrag: React.DragEventHandler | undefined; + dialColor: string; + opacity: number; +}; + +const STROKE_HEIGHT=8.0; + +function MakeGraphicEqPath(value: number) +{ + + let x0 = 6; + let xN = 64-x0; + let xC0 = (64-STROKE_HEIGHT)/2; + let xC1 = xC0+STROKE_HEIGHT; + + let h = 64-STROKE_HEIGHT*2.5; + + let y0 = 0; + let y1 = h-h*value;; + let y2 = y1 + STROKE_HEIGHT; + let y3 = y2 + STROKE_HEIGHT/2; + let y4 = y3 + STROKE_HEIGHT; + let y5 = 64; + + + let s = new SvgPathBuilder(); + s.moveTo(x0,y1) + .lineTo(xC0,y1) + .lineTo(xC0,y0) + .lineTo(xC1,y0) + .lineTo(xC1,y1) + .lineTo(xN,y1) + .lineTo(xN,y2) + .lineTo(x0,y2) + .closePath() + + .moveTo(x0,y3) + .lineTo(x0,y4) + .lineTo(xC0,y4) + .lineTo(xC0,y5) + .lineTo(xC1,y5) + .lineTo(xC1,y4) + .lineTo(xN,y4) + .lineTo(xN,y3) + .lineTo(x0,y3) + .closePath(); + return s.toString(); + +} +export function UpdateGraphicEqPath(el: SVGSVGElement, value: number) { + if (!el) return; + let p = MakeGraphicEqPath(value); + let pathEl = el.getElementById("myPath"); + pathEl?.setAttribute("d",p); +} + +export default function GraphicEqCtl(props: GraphicEqCtlProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index e5cba80..177b1b2 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -453,6 +453,7 @@ export enum ControlType { Trigger, Momentary, MomentaryOnByDefault, + GraphicEq, Tuner, Vu, @@ -497,6 +498,7 @@ deserialize(input: any): UiControl { this.integer_property = input.integer_property; this.mod_momentaryOffByDefault = input.mod_momentaryOffByDefault; this.mod_momentaryOnByDefault = input.mod_momentaryOnByDefault; + this.pipedal_graphicEq = input.pipedal_graphicEq; this.enumeration_property = input.enumeration_property; this.toggled_property = input.toggled_property; this.trigger_property = input.trigger_property; @@ -548,7 +550,11 @@ deserialize(input: any): UiControl { } if (this.is_input) { - if (this.mod_momentaryOnByDefault) { + if (this.pipedal_graphicEq) + { + this.controlType = ControlType.GraphicEq; + } + else if (this.mod_momentaryOnByDefault) { this.controlType = ControlType.MomentaryOnByDefault; } else if (this.mod_momentaryOffByDefault) { this.controlType = ControlType.Momentary; @@ -604,6 +610,7 @@ deserialize(input: any): UiControl { integer_property: boolean = false; mod_momentaryOffByDefault: boolean = false; mod_momentaryOnByDefault: boolean = false; + pipedal_graphicEq: boolean = false; enumeration_property: boolean = false; trigger_property: boolean = false; not_on_gui: boolean = false; @@ -647,6 +654,9 @@ deserialize(input: any): UiControl { isAbToggle(): boolean { return this.controlType === ControlType.ABSwitch; } + isGraphicEq() : boolean { + return this.controlType === ControlType.GraphicEq; + } isSelect(): boolean { return this.controlType === ControlType.Select; } diff --git a/vite/src/pipedal/PluginControl.tsx b/vite/src/pipedal/PluginControl.tsx index 4bf8acf..aee58fc 100644 --- a/vite/src/pipedal/PluginControl.tsx +++ b/vite/src/pipedal/PluginControl.tsx @@ -40,6 +40,7 @@ import ControlTooltip from './ControlTooltip'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import StopIcon from '@mui/icons-material/Stop'; import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; +import GraphicEqCtl, { UpdateGraphicEqPath } from './GraphicEqCtl'; const MIN_ANGLE = -135; const MAX_ANGLE = 135; @@ -59,6 +60,21 @@ const ULTRA_FINE_RANGE_SCALE = RANGE_SCALE * 50; // 12000 pixels to move from 0 export const StandardItemSize = { width: 80, height: 140 } +function preventNextClickAfterDrag() { + // the broswer fires a click event when the pointer goes up + // on ANY element under the mouse. Prevent this click event + // (and any other click event) from happening for 100ms + // after the drag stops. + let clickHandler = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + document.addEventListener('click',clickHandler,true); + window.setTimeout(() => { + document.removeEventListener('click',clickHandler,true); + },100); +} + function androidEmoji(text: string) { // android chrome doesn't support these characters properly. if (text === "⏹") { @@ -67,9 +83,8 @@ function androidEmoji(text: string) { if (text === "⏺") { return () } - if (text === "⏵") - { - return (); + if (text === "⏵") { + return (); } return text; } @@ -129,6 +144,7 @@ export interface PluginControlProps extends WithStyles; imgRef: React.RefObject; + graphicEqControlRef: React.RefObject; inputRef: React.RefObject; selectRef: React.RefObject; @@ -148,10 +165,12 @@ const PluginControl = super(props); this.state = { - error: false + error: false, + editFocused: false }; this.model = PiPedalModelFactory.getInstance(); this.imgRef = React.createRef(); + this.graphicEqControlRef = React.createRef(); this.inputRef = React.createRef(); this.selectRef = React.createRef(); this.displayValueRef = React.createRef(); @@ -195,16 +214,19 @@ const PluginControl = inputChanged: boolean = false; onInputLostFocus(event: any): void { + this.setState({editFocused: false}); if (this.inputChanged) // validation requried? { this.inputChanged = false; this.validateInput(event, true); } - this.displayValueRef.current!.style.display = "block"; + + //this.displayValueRef.current!.style.display = "block"; } onInputFocus(event: SyntheticEvent): void { - this.displayValueRef.current!.style.display = "none"; + //this.displayValueRef.current!.style.display = "none"; + this.setState({editFocused: true}); if (Utility.hasIMEKeyboard()) { event.preventDefault(); event.stopPropagation(); @@ -273,6 +295,7 @@ const PluginControl = onDrag(e: SyntheticEvent) { e.preventDefault(); + e.stopPropagation(); } isValidPointer(e: PointerEvent): boolean { @@ -333,9 +356,19 @@ const PluginControl = if (!this.mouseDown && this.isValidPointer(e)) { e.preventDefault(); e.stopPropagation(); + if (document.activeElement) + { + let e = document.activeElement as any; + if (e.blur) + { + e.blur(); + } + } if (this.isTouchDevice()) { - if (this.props.uiControl?.isDial() ?? false) { + if (this.props.uiControl + && (this.props.uiControl.isDial() || this.props.uiControl.isGraphicEq()) + ) { this.isTap = false; this.showZoomedControl(); return; @@ -390,7 +423,9 @@ const PluginControl = onPointerLostCapture(e: PointerEvent) { if (this.isCapturedPointer(e)) { - --this.pointersDown; + if (this.pointersDown !== 0) { + --this.pointersDown; + } this.isTap = false; @@ -438,7 +473,7 @@ const PluginControl = onPointerDoubleTap() { let uiControl = this.props.uiControl; if (uiControl) { - if (uiControl.isDial()) { + if (uiControl.isDial() || uiControl.isGraphicEq()) { this.resetToDefaultValue(uiControl); } } @@ -457,10 +492,14 @@ const PluginControl = onPointerUp(e: PointerEvent) { if (this.isCapturedPointer(e)) { - --this.pointersDown; - + if (this.pointersDown !== 0) + { + --this.pointersDown; + } e.preventDefault(); + e.stopPropagation(); + let dRange = this.updateRange(e) this.previewRange(dRange, true); @@ -471,9 +510,15 @@ const PluginControl = this.onPointerTap(); } } + // prevent click from firing on other elements + // when the pointer goes up0. + preventNextClickAfterDrag(); } else { - --this.pointersDown; + if (this.pointersDown !== 0) + { + --this.pointersDown; + } } } @@ -518,7 +563,7 @@ const PluginControl = this.isTap = false; } } - + } handleButtonMouseLeave(buttonStyle: ButtonStyle) { } @@ -527,10 +572,10 @@ const PluginControl = if (uiControl) { switch (buttonStyle) { case ButtonStyle.Momentary: - this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.max_value); + this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, uiControl.max_value); break; case ButtonStyle.MomentaryOnByDefault: - this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.min_value); + this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, uiControl.min_value); break; case ButtonStyle.Trigger: { @@ -539,7 +584,7 @@ const PluginControl = value = uiControl.min_value; } this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value); - + } break; } @@ -550,10 +595,10 @@ const PluginControl = if (uiControl) { switch (buttonStyle) { case ButtonStyle.Momentary: - this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.min_value); + this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, uiControl.min_value); break; case ButtonStyle.MomentaryOnByDefault: - this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.max_value); + this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, uiControl.max_value); break; case ButtonStyle.Trigger: { @@ -567,14 +612,22 @@ const PluginControl = let range = this.valueToRange(value); value = this.rangeToValue(range); - let transform = this.rangeToRotationTransform(range); - if (this.mouseDown && !commitValue) { - transform += " scale(1.5, 1.5)"; - } let imgElement = this.imgRef.current if (imgElement) { - if (imgElement.style) { - imgElement.style.transform = transform; + if (this.props.uiControl?.isGraphicEq()) { + let imgElement = this.imgRef.current + if (imgElement) { + UpdateGraphicEqPath(imgElement, range); + } + } else { + + let transform = this.rangeToRotationTransform(range); + if (this.mouseDown && !commitValue) { + transform += " scale(1.5, 1.5)"; + } + if (imgElement.style) { + imgElement.style.transform = transform; + } } } if (commitValue) { @@ -594,10 +647,18 @@ const PluginControl = // apply value quantization and clipping. range = this.valueToRange(value); - let imgElement = this.imgRef.current - if (imgElement) { - if (imgElement.style) { - imgElement.style.transform = this.rangeToRotationTransform(range); + if (this.props.uiControl?.isGraphicEq()) { + let imgElement = this.imgRef.current + if (imgElement) { + UpdateGraphicEqPath(imgElement, range); + } + + } else { + let imgElement = this.imgRef.current + if (imgElement) { + if (imgElement.style) { + imgElement.style.transform = this.rangeToRotationTransform(range); + } } } let inputElement = this.inputRef.current; @@ -779,6 +840,16 @@ const PluginControl = return "rotate(" + angle + "deg)"; } + getEqPosition(): number { + let range = 0; + let uiControl = this.props.uiControl; + if (uiControl) { + let value = this.props.value; + range = this.valueToRange(value); + } + return range; + + } getRotationTransform(): string { let range = 0; let uiControl = this.props.uiControl; @@ -812,6 +883,8 @@ const PluginControl = let isButton = false; let buttonStyle: ButtonStyle = ButtonStyle.None; + let isGraphicEq = control.isGraphicEq(); + if (control.isTrigger()) { isButton = true; buttonStyle = ButtonStyle.Trigger; @@ -833,6 +906,9 @@ const PluginControl = if (isButton) { item_width = undefined; } + if (isGraphicEq) { + item_width = 58; + } return ( @@ -862,27 +938,29 @@ const PluginControl = ( control.name.length !== 1 ? (