From 96584fd4a7e1b28e9b4b5b058b1d67d5b2bd4ab0 Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Fri, 31 Dec 2021 10:30:31 -0500 Subject: [PATCH] Street testing fixes --- react/CMakeLists.txt | 1 + react/src/App.tsx | 44 +++- react/src/BankDialog.tsx | 2 +- react/src/FullScreenIME.tsx | 2 +- react/src/GxTunerControl.tsx | 15 +- react/src/Lv2Plugin.tsx | 105 ++++++++- react/src/PedalBoardView.tsx | 11 +- react/src/PiPedalModel.tsx | 85 ++++++++ react/src/PiPedalSocket.tsx | 19 +- react/src/PluginControl.tsx | 122 +++++------ react/src/PluginControlView.tsx | 4 +- react/src/SplitControlView.tsx | 14 +- react/src/Utility.tsx | 3 +- react/src/VuMeter.tsx | 12 +- react/src/ZoomedDial.tsx | 369 ++++++++++++++++++++++++++++++++ react/src/ZoomedUiControl.tsx | 168 +++++++++++++++ src/Storage.cpp | 8 +- 17 files changed, 868 insertions(+), 116 deletions(-) create mode 100644 react/src/ZoomedDial.tsx create mode 100644 react/src/ZoomedUiControl.tsx diff --git a/react/CMakeLists.txt b/react/CMakeLists.txt index e4eb2f7..614ce8b 100644 --- a/react/CMakeLists.txt +++ b/react/CMakeLists.txt @@ -85,6 +85,7 @@ add_custom_command( src/WifiConfigDialog.tsx src/NoChangePassword.tsx src/XxxSnippet.tsx + src/ZoomedUiControl.tsx public/css/roboto.css public/fonts/Roboto-BlackItalic.woff2 diff --git a/react/src/App.tsx b/react/src/App.tsx index d0b8caa..ff692e9 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -48,7 +48,8 @@ import SettingsDialog from './SettingsDialog'; import AboutDialog from './AboutDialog'; import BankDialog from './BankDialog'; -import { PiPedalModelFactory, PiPedalModel, State } from './PiPedalModel'; +import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel'; +import ZoomedUiControl from './ZoomedUiControl' import MainPage from './MainPage'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; @@ -59,6 +60,7 @@ import { BankIndex, BankIndexEntry } from './Banks'; import RenameDialog from './RenameDialog'; import JackStatusView from './JackStatusView'; + const theme = createMuiTheme({ palette: { primary: { @@ -86,7 +88,7 @@ const appStyles = ({ palette, spacing, mixins }: Theme) => createStyles({ zIndex: 2010 }, errorContent: { - + display: "flex", flexDirection: "column", justifyContent: "center", @@ -270,6 +272,8 @@ function preventDefault(e: SyntheticEvent): void { } type AppState = { + zoomedControlInfo: ZoomedControlInfo | undefined; + isDrawerOpen: boolean; errorMessage: string; displayState: State; @@ -288,6 +292,7 @@ type AppState = { aboutDialogOpen: boolean; bankDialogOpen: boolean; editBankDialogOpen: boolean; + zoomedControlOpen: boolean; presetName: string; @@ -309,7 +314,17 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent { + this.setState({ + zoomedControlOpen: this.model_.zoomedUiControl.get() !== undefined, + zoomedControlInfo: this.model_.zoomedUiControl.get() + }); + } + ); + this.state = { + zoomedControlInfo: this.model_.zoomedUiControl.get(), isDrawerOpen: false, errorMessage: this.model_.errorMessage.get(), displayState: this.model_.state.get(), @@ -328,6 +343,7 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent this.model_.showAlert(error)); @@ -475,8 +492,7 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent { e.preventDefault(); - if (this.model_.state.get() === State.Ready) - { + if (this.model_.state.get() === State.Ready) { e.returnValue = "Are you sure you want to leave this page?"; return "Are you sure you want to leave this page?"; } @@ -539,7 +555,7 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent 7) bankEntries = 7; this.setState({ bankDisplayItems: bankEntries }); @@ -609,12 +625,11 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent { - if (!this.model_.serverVersion?.debug??false) - { - e.preventDefault(); e.stopPropagation(); - } - }} + onContextMenu={(e) => { + if (!this.model_.serverVersion?.debug ?? false) { + e.preventDefault(); e.stopPropagation(); + } + }} > {(!this.state.tinyToolBar) ? @@ -769,6 +784,13 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent + { this.setState({zoomedControlOpen: false});} } + onDialogClosed={()=>{ this.model_.zoomedUiControl.set(undefined); } + } + /> {this.props.caption} { deserialize(input: any): UiControl @@ -210,9 +216,31 @@ export class UiControl implements Deserializable { this.scale_points = ScalePoint.deserialize_array(input.scale_points); this.port_group = input.port_group; this.units = input.units as Units; + + this.controlType = ControlType.Dial; + if (this.enumeration_property && this.scale_points.length === 2) + { + this.controlType = ControlType.Toggle; + } else { + if (this.min_value === 0 && this.max_value === 1) + { + if (this.toggled_property || this.enumeration_property || this.integer_property) + { + this.controlType = ControlType.OnOffSwitch; + } + } + } + if (this.controlType === ControlType.Dial && this.enumeration_property) + { + this.controlType = ControlType.Select; + } + return this; } + + controlType: ControlType = ControlType.Dial; // non-serializable. + static deserialize_array(input: any): UiControl[] { let result: UiControl[] = []; for (let i = 0; i < input.length; ++i) @@ -240,18 +268,18 @@ export class UiControl implements Deserializable { units: Units = Units.none; isOnOffSwitch() : boolean { - if (this.isAbToggle()) return false; - if (this.min_value !== 0 || this.max_value !== 1) return false; - return (this.toggled_property || this.enumeration_property || this.integer_property); - ; + return this.controlType === ControlType.OnOffSwitch; } isAbToggle(): boolean { - if (this.min_value !== 0 || this.max_value !== 1) return false; - return this.enumeration_property && this.scale_points.length === 2; + return this.controlType === ControlType.Toggle; } isSelect() : boolean { - return this.enumeration_property && !this.isOnOffSwitch() && !this.isAbToggle(); + return this.controlType === ControlType.Select; + } + + isDial() : boolean { + return this.controlType === ControlType.Dial; } valueToRange(value: number): number { @@ -289,10 +317,71 @@ export class UiControl implements Deserializable { return this.rangeToValue(this.valueToRange(value)); } - formatValue(value: number): string { + formatDisplayValue(value: number): string { if (this.integer_property || this.enumeration_property) { value = Math.round(value); } + + for (let i = 0; i < this.scale_points.length; ++i) + { + let scalePoint = this.scale_points[i]; + if (scalePoint.value === value) + { + return scalePoint.label; + } + } + let text = this.formatShortValue(value); + + switch (this.units) { + case Units.bpm: + text += "bpm"; + break; + case Units.cent: + text += "cents"; + break; + case Units.cm: + text += "cm"; + break; + case Units.db: + text += "dB"; + break; + case Units.hz: + text += "Hz"; + break; + case Units.khz: + text += "kHz"; + break; + case Units.km: + text += "km"; + break; + case Units.m: + text += "m"; + break; + case Units.mhz: + text += "MHz"; + break; + case Units.min: + text += "min"; + break; + case Units.ms: + text += "ms"; + break; + case Units.pc: + text += "%"; + break; + case Units.s: + text += "s"; + break; + // Midinote: not handled. + // semitone12TET not handled. + + + + } + return text; + } + formatShortValue(value: number): string + { if (this.enumeration_property) { for (let i = 0; i < this.scale_points.length; ++i) { let scale_point = this.scale_points[i]; diff --git a/react/src/PedalBoardView.tsx b/react/src/PedalBoardView.tsx index 042a399..adc30cf 100644 --- a/react/src/PedalBoardView.tsx +++ b/react/src/PedalBoardView.tsx @@ -29,6 +29,7 @@ import SvgPathBuilder from './SvgPathBuilder'; import Draggable from './Draggable' import Rect from './Rect'; import {PiPedalStateError} from './PiPedalError'; +import Utility from './Utility' import { @@ -645,12 +646,6 @@ const PedalBoardView = } } - isTouchDevice(): boolean { - return (('ontouchstart' in window) || - (navigator.maxTouchPoints > 0) || - (navigator["msMaxTouchPoints"] > 0)); - } - onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { event.preventDefault(); event.stopPropagation(); @@ -665,7 +660,7 @@ const PedalBoardView = event.preventDefault(); event.stopPropagation(); - if (!this.isTouchDevice()) { + if (!Utility.isTouchDevice()) { if (this.props.onDoubleClick && instanceId) { this.props.onDoubleClick(instanceId); } @@ -673,7 +668,7 @@ const PedalBoardView = } - + // XXX set keys on output objects !! renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { // let classes = this.props.classes; let x_ = item.bounds.x + CELL_WIDTH / 2; diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 24dcddc..3f527bc 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -41,9 +41,15 @@ export enum State { Loading, Ready, Error, + Background, Reconnecting }; +export interface ZoomedControlInfo { + source: HTMLElement; + instanceId: number; + uiControl: UiControl; +} export interface VuUpdateInfo { instanceId: number; @@ -218,6 +224,10 @@ export class PresetIndex { } +enum VisibilityState { + Visible, + Hidden +}; interface MonitorPortOutputBody { subscriptionHandle: number; value: number; @@ -261,7 +271,10 @@ export interface PiPedalModel { serverVersion?: PiPedalVersion; errorMessage: ObservableProperty; alertMessage: ObservableProperty; + state: ObservableProperty; + visibilityState: ObservableProperty; + ui_plugins: ObservableProperty; plugin_classes: ObservableProperty; jackConfiguration: ObservableProperty; @@ -272,6 +285,9 @@ export interface PiPedalModel { presets: ObservableProperty; + + zoomedUiControl: ObservableProperty< ZoomedControlInfo| undefined >; + setJackServerSettings(jackServerSettings: JackServerSettings): void; setMidiBinding(instanceId: number, midiBinding: MidiBinding): void; @@ -350,6 +366,9 @@ export interface PiPedalModel { getWifiChannels(countryIso3661: string): Promise; getAlsaDevices() : Promise; + + zoomUiControl(sourceElement: HTMLElement,instanceId: number,uiControl: UiControl): void; + clearZoomedControl(): void; }; class PiPedalModelImpl implements PiPedalModel { @@ -365,6 +384,8 @@ class PiPedalModelImpl implements PiPedalModel { ui_plugins: ObservableProperty = new ObservableProperty([]); state: ObservableProperty = new ObservableProperty(State.Loading); + visibilityState: ObservableProperty = new ObservableProperty(VisibilityState.Visible); + errorMessage: ObservableProperty = new ObservableProperty(""); alertMessage: ObservableProperty = new ObservableProperty(""); pedalBoard: ObservableProperty = new ObservableProperty(new PedalBoard()); @@ -384,6 +405,7 @@ class PiPedalModelImpl implements PiPedalModel { new PresetIndex() ); + zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); svgImgUrl(svgImage: string): string { //return this.varServerUrl + "img/" + svgImage; @@ -406,8 +428,10 @@ class PiPedalModelImpl implements PiPedalModel { this.onSocketMessage = this.onSocketMessage.bind(this); this.onSocketReconnecting = this.onSocketReconnecting.bind(this); this.onSocketReconnected = this.onSocketReconnected.bind(this); + this.onVisibilityChanged = this.onVisibilityChanged.bind(this); } onSocketReconnecting(retry: number, maxRetries: number): void { + if (this.visibilityState.get() === VisibilityState.Hidden) return; //if (retry !== 0) { this.setState(State.Reconnecting); //} @@ -415,9 +439,12 @@ class PiPedalModelImpl implements PiPedalModel { onSocketError(errorMessage: string, exception: any) { + if (this.visibilityState.get() === VisibilityState.Hidden) return; this.onError(errorMessage); } onSocketMessage(header: PiPedalMessageHeader, body?: any) { + if (this.visibilityState.get() === VisibilityState.Hidden) return; + let message = header.message; if (message === "onMonitorPortOutput") { let monitorPortOutputBody = body as MonitorPortOutputBody; @@ -557,6 +584,8 @@ class PiPedalModelImpl implements PiPedalModel { } onSocketReconnected() { + if (this.visibilityState.get() === VisibilityState.Hidden) return; + // reload state, but not configuration. this.getWebSocket().request("hello") .then(clientId => { @@ -771,9 +800,52 @@ class PiPedalModelImpl implements PiPedalModel { this.state.set(State.Error); } + + exitBackgroundState() + { + if (this.state.get() === State.Background) + { + console.log("Exiting background state."); + this.visibilityState.set(VisibilityState.Visible); + this.webSocket?.exitBackgroundState(); + } + + } + enterBackgroundState() + { + if (this.state.get() !== State.Background) + { + console.log("Entering background state."); + this.visibilityState.set(VisibilityState.Hidden); + this.setState(State.Background); + this.webSocket?.enterBackgroundState(); + } + } + + + onVisibilityChanged(doc: Document, event: Event) : any + { + if (document.visibilityState) { + + switch (document.visibilityState) + { + case "visible": + this.visibilityState.set(VisibilityState.Visible); + this.exitBackgroundState(); + break; + case "hidden": + this.visibilityState.set(VisibilityState.Hidden); + this.enterBackgroundState(); + break; + } + } + return false; + } + initialize(): void { this.setError(""); this.setState(State.Loading); + this.requestConfig() .then((succeeded) => { if (succeeded) { @@ -783,6 +855,11 @@ class PiPedalModelImpl implements PiPedalModel { .catch((error) => { this.setError("Failed to get server state. \n\n" + error); }); + let t = this.onVisibilityChanged; + + (document as any).addEventListener("visibilitychange",(doc: Document,event: Event) => { + return t(doc,event); + }); } getControl(uiPlugin: UiPlugin, portSymbol: string): UiControl | null { @@ -1598,6 +1675,14 @@ class PiPedalModelImpl implements PiPedalModel { return result; } + zoomUiControl(sourceElement: HTMLElement,instanceId: number,uiControl: UiControl): void + { + this.zoomedUiControl.set({source: sourceElement,instanceId: instanceId,uiControl: uiControl}); + } + clearZoomedControl(): void + { + this.zoomedUiControl.set(undefined); + } }; diff --git a/react/src/PiPedalSocket.tsx b/react/src/PiPedalSocket.tsx index dedc4d3..b5810bf 100644 --- a/react/src/PiPedalSocket.tsx +++ b/react/src/PiPedalSocket.tsx @@ -164,7 +164,7 @@ class PiPedalSocket { } } catch (error) { if (this.onError) { - this.onError("Invalid server response. " + error, error); + this.onError("Invalid server response. " + error, error as Error); } else { throw new PiPedalStateError("Invalid server response."); } @@ -189,6 +189,9 @@ class PiPedalSocket { return; } + this._reconnect(); + } + _reconnect() { this._discardReplyReservations(); this.retrying = true; this.retryCount = 0; @@ -199,6 +202,20 @@ class PiPedalSocket { this.reconnect(); } + isBackground: boolean = false; + + enterBackgroundState() + { + this.isBackground = true; + this.socket?.close(); + + } + exitBackgroundState() + { + this.isBackground = false; + this._reconnect(); + } + reconnect() { if (this.socket) diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index d71f782..97c3bfe 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -24,9 +24,9 @@ import Typography from '@material-ui/core/Typography'; import Input from '@material-ui/core/Input'; import Select from '@material-ui/core/Select'; import Switch from '@material-ui/core/Switch'; -import Units from './Units'; import Utility, {nullCast} from './Utility'; import MenuItem from '@material-ui/core/MenuItem'; +import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel'; const MIN_ANGLE = -135; @@ -78,6 +78,7 @@ const styles = (theme: Theme) => createStyles({ export interface PluginControlProps extends WithStyles { uiControl?: UiControl; + instanceId: number; value: number; onPreviewChange?: (value: number) => void; onChange: (value: number) => void; @@ -92,11 +93,15 @@ const PluginControl = withStyles(styles, { withTheme: true })( class extends Component { + + frameRef: React.RefObject; imgRef: React.RefObject; inputRef: React.RefObject; selectRef: React.RefObject; displayValueRef: React.RefObject; + model: PiPedalModel; + constructor(props: PluginControlProps) { super(props); @@ -104,11 +109,12 @@ const PluginControl = this.state = { error: false }; - + this.model = PiPedalModelFactory.getInstance(); this.imgRef = React.createRef(); this.inputRef = React.createRef(); this.selectRef = React.createRef(); this.displayValueRef = React.createRef(); + this.frameRef = React.createRef(); this.onPointerDown = this.onPointerDown.bind(this); @@ -127,7 +133,29 @@ const PluginControl = this.onInputFocus = this.onInputFocus.bind(this); this.onInputKeyPress = this.onInputKeyPress.bind(this); } + isTouchDevice(): boolean { + return Utility.isTouchDevice(); + } + showZoomedControl() + { + if (this.props.uiControl && this.frameRef.current) + { + this.model.zoomUiControl(this.frameRef.current,this.props.instanceId,this.props.uiControl); + } + } + hideZoomedControl() + { + if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current) + { + this.model.clearZoomedControl(); + } + } + + componentWillUnmount() + { + this.hideZoomedControl(); + } inputChanged: boolean = false; onInputLostFocus(event: any): void { @@ -190,11 +218,12 @@ const PluginControl = // clamp and quantize. let range = this.valueToRange(result); result = this.rangeToValue(range); + let displayVal = this.props.uiControl?.formatShortValue(result)??""; if (event.currentTarget) { - event.currentTarget.value = this.formatValue(this.props.uiControl, result); + event.currentTarget.value = displayVal; } this.previewInputValue(result, true); - this.inputRef.current.value = this.formatValue(this.props.uiControl, result); // no rerender because the value won't change. + this.inputRef.current.value = displayVal; // no rerender because the value won't change. } else { this.setState({ error: !valid }); if (valid) { @@ -266,12 +295,21 @@ const PluginControl = onPointerDown(e: PointerEvent): void { if (!this.mouseDown && this.isValidPointer(e)) { - ++this.pointersDown; - - e.preventDefault(); e.stopPropagation(); + if (this.isTouchDevice()) + { + if (this.props.uiControl?.isDial()??false) + { + this.showZoomedControl(); + return; + } + } + + ++this.pointersDown; + + this.mouseDown = true; this.pointerId = e.pointerId; @@ -443,7 +481,7 @@ const PluginControl = } let inputElement = this.inputRef.current; if (inputElement) { - let v = this.formatValue(this.props.uiControl, value); + let v = this.props.uiControl?.formatShortValue(value)??""; inputElement.value = v; } let displayValue = this.displayValueRef.current; @@ -528,69 +566,10 @@ const PluginControl = formatDisplayValue(uiControl: UiControl | undefined, value: number): string { if (!uiControl) return ""; - for (let i = 0; i < uiControl.scale_points.length; ++i) - { - let scalePoint = uiControl.scale_points[i]; - if (scalePoint.value === value) - { - return scalePoint.label; - } - } - let text = this.formatValue(uiControl, value); - switch (uiControl.units) { - case Units.bpm: - text += "bpm"; - break; - case Units.cent: - text += "cents"; - break; - case Units.cm: - text += "cm"; - break; - case Units.db: - text += "dB"; - break; - case Units.hz: - text += "Hz"; - break; - case Units.khz: - text += "kHz"; - break; - case Units.km: - text += "km"; - break; - case Units.m: - text += "m"; - break; - case Units.mhz: - text += "MHz"; - break; - case Units.min: - text += "min"; - break; - case Units.ms: - text += "ms"; - break; - case Units.pc: - text += "%"; - break; - case Units.s: - text += "s"; - break; - // Midinote: not handled. - // semitone12TET not handled. + return uiControl.formatDisplayValue(value); - - } - return text; - - - } - formatValue(uiControl: UiControl | undefined, value: number): string { - if (!uiControl) return ""; - return uiControl.formatValue(value); } valueToRange(value: number): number { @@ -681,7 +660,7 @@ const PluginControl = return ( -
+
{/* TITLE SECTION */}
{ this.onValueChanged(controlValue!.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }} requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)} @@ -353,7 +353,7 @@ const PluginControlView = let controls = shortList.map((controlValue) => ( - { this.onValueChanged(controlValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(controlValue.key, value) }} requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)} diff --git a/react/src/SplitControlView.tsx b/react/src/SplitControlView.tsx index 18c9912..36c8e9d 100644 --- a/react/src/SplitControlView.tsx +++ b/react/src/SplitControlView.tsx @@ -185,7 +185,7 @@ const SplitControlView =
- { this.onValueChanged(typeValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(typeValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -195,7 +195,7 @@ const SplitControlView = { typeValue.value === 0 && (
- { this.onValueChanged(selectValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(selectValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -208,7 +208,7 @@ const SplitControlView = { typeValue.value === 1 && (
- { this.onValueChanged(mixValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(mixValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -221,7 +221,7 @@ const SplitControlView = { typeValue.value === 2 && (
- { this.onValueChanged(panLValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(panLValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -233,7 +233,7 @@ const SplitControlView = { typeValue.value === 2 && (
- { this.onValueChanged(volumeLValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(volumeLValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -245,7 +245,7 @@ const SplitControlView = { typeValue.value === 2 && (
- { this.onValueChanged(panRValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(panRValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} @@ -257,7 +257,7 @@ const SplitControlView = { typeValue.value === 2 && (
- { this.onValueChanged(volumeRValue.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(volumeRValue.key, value) }} requestIMEEdit={(uiControl, value) => { }} diff --git a/react/src/Utility.tsx b/react/src/Utility.tsx index e57f4ca..6da4a6f 100644 --- a/react/src/Utility.tsx +++ b/react/src/Utility.tsx @@ -39,7 +39,8 @@ export interface ControlEntry { const Utility = class { static isTouchDevice(): boolean { - return navigator.maxTouchPoints > 0; + return (('ontouchstart' in window) && + ((navigator.maxTouchPoints??0) > 0) ); } static hasIMEKeyboard(): boolean { diff --git a/react/src/VuMeter.tsx b/react/src/VuMeter.tsx index 8c28c75..e0da61b 100644 --- a/react/src/VuMeter.tsx +++ b/react/src/VuMeter.tsx @@ -19,7 +19,7 @@ import React, { Component } from 'react'; import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles'; -import { PiPedalModel, PiPedalModelFactory, VuUpdateInfo, VuSubscriptionHandle } from './PiPedalModel'; +import { PiPedalModel, State,PiPedalModelFactory, VuUpdateInfo, VuSubscriptionHandle } from './PiPedalModel'; const DISPLAY_HEIGHT = 110; @@ -226,6 +226,7 @@ export const VuMeter = this.yYellow = dbToY(DB_YELLOW); this.onVuChanged = this.onVuChanged.bind(this); + this.onStateChanged = this.onStateChanged.bind(this); } @@ -401,13 +402,22 @@ export const VuMeter = } } + onStateChanged(state: State) { + // initial connection or reconnect + if (state === State.Ready) + { + this.addVuSubscription(); // re-subscribe. + } + } componentDidMount() { + this.model.state.addOnChangedHandler(this.onStateChanged); this.addVuSubscription(); } componentWillUnmount() { this.removeVuSubscription(); + this.model.state.removeOnChangedHandler(this.onStateChanged); } } ); diff --git a/react/src/ZoomedDial.tsx b/react/src/ZoomedDial.tsx new file mode 100644 index 0000000..b1b58c5 --- /dev/null +++ b/react/src/ZoomedDial.tsx @@ -0,0 +1,369 @@ +// Copyright (c) 2021 Robin Davies +// +// 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 React, { TouchEvent, PointerEvent, SyntheticEvent } from 'react'; import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles'; +import { PiPedalModel, PiPedalModelFactory, ZoomedControlInfo } from './PiPedalModel'; + +const SELECTED_OPACITY = 0.8; +const DEFAULT_OPACITY = 0.6; +const FINE_RANGE_SCALE = 0.1; // +const ULTRA_FINE_RANGE_SCALE = 0.02; // 12000 pixels to move from 0 to 1. + +const MIN_ANGLE = -135; +const MAX_ANGLE = 135; +const DEAD_ZONE_SIZE = 5; + + +const styles = (theme: Theme) => createStyles({ +}); + + +interface ZoomedDialProps extends WithStyles { + theme: Theme, + size: number, + + controlInfo: ZoomedControlInfo | undefined, + + onPreviewValue(value: number): void, + onSetValue(value: number): void +}; + +interface ZoomedDialState { + +}; + +const ZoomedDial = withStyles(styles, { withTheme: true })( + + class extends React.Component { + + model: PiPedalModel = PiPedalModelFactory.getInstance(); + imgRef: React.RefObject = React.createRef(); + + constructor(props: ZoomedDialProps) { + super(props); + this.state = { + + }; + + this.onTouchStart = this.onTouchStart.bind(this); + this.onTouchMove = this.onTouchMove.bind(this); + this.onBodyPointerDownCapture = this.onBodyPointerDownCapture.bind(this); + + this.onPointerDown = this.onPointerDown.bind(this); + this.onPointerUp = this.onPointerUp.bind(this); + this.onPointerMove = this.onPointerMove.bind(this); + this.onPointerLostCapture = this.onPointerLostCapture.bind(this); + + } + + onPointerUp(e: PointerEvent) { + + if (this.isCapturedPointer(e)) { + --this.pointersDown; + + + e.preventDefault(); + this.updateAngle(e); + this.commitAngle(); + + this.releaseCapture(e); + + } else { + --this.pointersDown; + + } + } + + + releaseCapture(e: PointerEvent) + { + let img = this.imgRef.current; + + if (img && img.style) { + img.releasePointerCapture(e.pointerId); + img.style.opacity = "" + DEFAULT_OPACITY; + // they get automaticlly released. + // for (let i = 0; i < this.capturedPointers.length; ++i) + // { + // img.releasePointerCapture (this.capturedPointers[i]); + // } + this.capturedPointers = []; + } + document.body.removeEventListener( + "pointerdown", + this.onBodyPointerDownCapture,true + ); + + this.mouseDown = false; + + } + + + onPointerLostCapture(e: PointerEvent) { + if (this.isCapturedPointer(e)) { + --this.pointersDown; + + + this.releaseCapture(e); + } + + } + + onTouchStart(e: TouchEvent) { + //must be defined to get onTouchMove + } + onTouchMove(e: TouchEvent) { + // e.preventDefault(); + e.stopPropagation(); // cancels scroll!!! + + } + capturedPointers: number[] = []; + + isExtraTouch(e: PointerEvent): boolean { + return (this.mouseDown && this.pointerType === "touch" && e.pointerType === "touch" && e.pointerId !== this.pointerId); + } + + mouseDown: boolean = false; + pointerId: number = -1; + pointerType: string = ""; + startX: number = 0; + startY: number = 0; + currentAngle: number = 0; + lastPointerAngle: number = 0; + + isValidPointer(e: PointerEvent): boolean { + if (e.pointerType === "mouse") { + return e.button === 0; + } else if (e.pointerType === "pen") { + return true; + } else if (e.pointerType === "touch") { + return true; + } + return false; + } + + defaultValue: number = 0; + + getCurrentValue(): number { + if (this.props.controlInfo == null) { + return this.defaultValue; + } else { + let uiControl = this.props.controlInfo.uiControl; + let instanceId = this.props.controlInfo.instanceId; + if (instanceId === -1) return 0; + let pedalBoardItem = this.model.pedalBoard.get()?.getItem(instanceId); + let value: number = pedalBoardItem?.getControlValue(this.props.controlInfo.uiControl.symbol) ?? 0; + this.defaultValue = value; + return value; + } + } + + angleToValue(angle: number): number { + if (this.props.controlInfo) { + if (angle > MAX_ANGLE) angle = MAX_ANGLE; + if (angle < MIN_ANGLE) angle = MIN_ANGLE; + + let r = (angle - MIN_ANGLE) / (MAX_ANGLE - MIN_ANGLE); + + let uiControl = this.props.controlInfo.uiControl; + + return r * (uiControl.max_value-uiControl.min_value)+uiControl.min_value; + } + return 0; + } + + valueToAngle(value: number): number { + if (this.props.controlInfo) { + let uiControl = this.props.controlInfo.uiControl; + let range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value); + if (range < 0) range = 0; + if (range > 1) range = 1; + return (MAX_ANGLE - MIN_ANGLE) * range + MIN_ANGLE; + } + return 0; + } + + pointerToAngle(e: PointerEvent): number { + + if (this.imgRef.current) { + let img = this.imgRef.current; + let rc = img.getBoundingClientRect(); + let x = e.clientX - (rc.left+ rc.width / 2); + let y = e.clientY - (rc.top + rc.height / 2); + + // dead zone in center of image. + + if (x*x+y*y < DEAD_ZONE_SIZE*DEAD_ZONE_SIZE) return NaN; + + let angle = -Math.atan2(-y, x) * 180 / Math.PI + 90; + + return angle; + } + return NaN; + } + captureElement?: HTMLElement; + pointersDown: number = 0; + + onPointerDown(e: PointerEvent): void { + if (!this.mouseDown && this.isValidPointer(e)) { + e.preventDefault(); + e.stopPropagation(); + + + + this.mouseDown = true; + this.pointersDown = 1; + this.pointerId = e.pointerId; + this.pointerType = e.pointerType; + this.startX = e.clientX; + this.startY = e.clientY; + this.currentAngle = this.valueToAngle(this.getCurrentValue()); + this.lastPointerAngle = this.pointerToAngle(e); + + let img = this.imgRef.current; + + if (img) { + this.captureElement = img; + document.body.addEventListener( + "pointerdown", + this.onBodyPointerDownCapture, true + ); + + img.setPointerCapture(e.pointerId); + if (img.style) { + img.style.opacity = "" + SELECTED_OPACITY; + } + } + + } else { + if (this.isExtraTouch(e)) { + ++this.pointersDown; + + } + } + } + + isCapturedPointer(e: PointerEvent): boolean { + return this.mouseDown + && e.pointerId === this.pointerId + && e.pointerType === this.pointerType; + + } + + + + onPointerMove(e: PointerEvent): void { + if (this.isCapturedPointer(e)) { + e.preventDefault(); + this.updateAngle(e) + } + } + + + commitAngle() { + let previewValue = this.angleToValue(this.currentAngle); + this.props.onSetValue(previewValue); + this.defaultValue = previewValue; + + + } + updateAngle(e: PointerEvent): void { + let angle: number = this.pointerToAngle(e); + if (!isNaN(angle)) { + if (!isNaN(this.lastPointerAngle)) { + let dAngle = angle-this.lastPointerAngle; + while (dAngle > 180) dAngle -= 360; + while (dAngle < -180) dAngle += 360; + + let scale = 1.0; + if (this.pointersDown === 2) + { + scale = FINE_RANGE_SCALE; + } else if (this.pointersDown >= 3) + { + scale = ULTRA_FINE_RANGE_SCALE; + } + + this.currentAngle += dAngle*scale; + let previewValue = this.angleToValue(this.currentAngle); + this.previewValue(previewValue); + this.props.onPreviewValue(previewValue); + } + this.lastPointerAngle = angle; + } + } + makeRotationTransform(angle: number): string + { + return "rotate(" + angle + "deg)"; + } + + getDefaultRotationTransform(): string + { + let v = this.getCurrentValue(); + let a = this.valueToAngle(v); + return this.makeRotationTransform(a); + } + previewValue(value: number): void + { + if (this.imgRef.current) + { + let img = this.imgRef.current; + let angle = this.valueToAngle(value); + img.style.transform = this.makeRotationTransform(angle); + } + + } + + + onBodyPointerDownCapture(e_: any): any { + let e = e_ as PointerEvent; + if (this.isExtraTouch(e)) { + this.captureElement!.setPointerCapture(e.pointerId); + this.capturedPointers.push(e.pointerId); + ++this.pointersDown; + } + + } + onDrag(e: SyntheticEvent) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + + + + render() { + return ( + + ); + } + } +); + +export default ZoomedDial; diff --git a/react/src/ZoomedUiControl.tsx b/react/src/ZoomedUiControl.tsx new file mode 100644 index 0000000..269002c --- /dev/null +++ b/react/src/ZoomedUiControl.tsx @@ -0,0 +1,168 @@ +// Copyright (c) 2021 Robin Davies +// +// 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 React, {SyntheticEvent} from 'react'; +import { Theme, withStyles, WithStyles,createStyles } from '@material-ui/core/styles'; +import { ZoomedControlInfo } from './PiPedalModel'; +import DialogEx from './DialogEx'; +import { TransitionProps } from '@material-ui/core/transitions/transition'; +import Slide from '@material-ui/core/Slide'; +import Typography from '@material-ui/core/Typography'; +import { PiPedalModelFactory,PiPedalModel } from './PiPedalModel'; +import ZoomedDial from './ZoomedDial'; + + +const styles = (theme: Theme) => createStyles({ +}); + + +interface ZoomedUiControlProps extends WithStyles { + dialogOpen: boolean; + onDialogClose: () => void; + onDialogClosed: () => void; + controlInfo: ZoomedControlInfo | undefined +}; + +interface ZoomedUiControlState { + value: number +}; + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { children?: React.ReactElement }, + ref: React.Ref, +) { + return ; +}); + + +const ZoomedUiControl = withStyles(styles, { withTheme: true })( + + class extends React.Component { + + model: PiPedalModel; + + constructor(props: ZoomedUiControlProps) + { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + value: this.getCurrentValue() + }; + } + + getCurrentValue(): number { + if (!this.props.controlInfo) + { + return 0; + } else { + let uiControl = this.props.controlInfo.uiControl; + let instanceId = this.props.controlInfo.instanceId; + if (instanceId === -1) return 0; + let pedalBoardItem = this.model.pedalBoard.get()?.getItem(instanceId); + let value: number = pedalBoardItem?.getControlValue(uiControl.symbol)??0; + return value; + } + } + hasControlChanged(oldProps: ZoomedUiControlProps, newProps: ZoomedUiControlProps) + { + if (oldProps.controlInfo === null) + { + return newProps.controlInfo !== null; + } else if (newProps.controlInfo === null) + { + return oldProps.controlInfo !== null; + } else { + if (newProps.controlInfo?.instanceId !== oldProps.controlInfo?.instanceId) return true; + if (newProps.controlInfo?.uiControl.symbol !== oldProps.controlInfo?.uiControl.symbol) return true; + } + return false; + + } + + componentDidUpdate(oldProps: ZoomedUiControlProps, oldState: ZoomedUiControlState) + { + if (this.hasControlChanged(oldProps,this.props)) + { + let currentValue = this.getCurrentValue(); + if (this.state.value !== currentValue) + { + this.setState({value: this.getCurrentValue()}); + } + } + } + + onDrag(e: SyntheticEvent) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + + + render() { + let displayValue = this.props.controlInfo?.uiControl.formatDisplayValue(this.state.value)??""; + return ( + { this.props.onDialogClose()}} + onAbort={()=> { this.props.onDialogClose()}} + onExited={ () => { + this.props.onDialogClosed() + } + } + > +
+ + {this.props.controlInfo?.uiControl.name??""} + + { + this.setState({value: v}); + }} + onSetValue={(v)=> { + this.setState({value: v}); + if (this.props.controlInfo) + { + this.model.setPedalBoardControlValue( + this.props.controlInfo.instanceId, + this.props.controlInfo.uiControl.symbol, + v); + } + }} + /> + + + {displayValue} + +
+ +
+ ); + } + } +); + +export default ZoomedUiControl; diff --git a/src/Storage.cpp b/src/Storage.cpp index b13c183..9be563d 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -193,7 +193,7 @@ void Storage::LoadBank(int64_t instanceId) this->bankIndex.selectedBank(instanceId); SaveBankIndex(); } - this->LoadPreset(this->bankIndex.selectedBank()); + this->LoadPreset(this->currentBank.selectedPreset()); } void Storage::LoadCurrentBank() @@ -368,8 +368,10 @@ bool Storage::LoadPreset(int64_t instanceId) { if (!currentBank.hasItem(instanceId)) return false; - currentBank.selectedPreset(instanceId); - SaveCurrentBank(); + if (instanceId != currentBank.selectedPreset()) { + currentBank.selectedPreset(instanceId); + SaveCurrentBank(); + } return true; } void Storage::saveCurrentPreset(const PedalBoard &pedalBoard)