Street testing fixes

This commit is contained in:
Robin Davies
2021-12-31 10:30:31 -05:00
parent d717a742b9
commit 96584fd4a7
17 changed files with 868 additions and 116 deletions
+1
View File
@@ -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
+33 -11
View File
@@ -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<AppPro
super(props);
this.model_ = PiPedalModelFactory.getInstance();
this.model_.zoomedUiControl.addOnChangedHandler(
()=> {
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<AppPro
aboutDialogOpen: false,
bankDialogOpen: false,
editBankDialogOpen: false,
zoomedControlOpen: false,
bankDisplayItems: 5
};
@@ -341,6 +357,7 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent<AppPro
}
onOpenBank(bankId: number) {
this.model_.openBank(bankId)
.catch((error) => this.model_.showAlert(error));
@@ -475,8 +492,7 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent<AppPro
window.addEventListener("beforeunload", (e) => {
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<AppPro
// ENTRY_HEIGHT*6 +K = 727 from observation.
const K = 450;
let bankEntries = Math.floor((height-K)/ENTRY_HEIGHT);
let bankEntries = Math.floor((height - K) / ENTRY_HEIGHT);
if (bankEntries < 1) bankEntries = 1;
if (bankEntries > 7) bankEntries = 7;
this.setState({ bankDisplayItems: bankEntries });
@@ -609,12 +625,11 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent<AppPro
display: "flex", flexDirection: "column", flexWrap: "nowrap",
overscrollBehavior: this.state.isDebug ? "auto" : "none"
}}
onContextMenu={(e) => {
if (!this.model_.serverVersion?.debug??false)
{
e.preventDefault(); e.stopPropagation();
}
}}
onContextMenu={(e) => {
if (!this.model_.serverVersion?.debug ?? false) {
e.preventDefault(); e.stopPropagation();
}
}}
>
<CssBaseline />
{(!this.state.tinyToolBar) ?
@@ -769,6 +784,13 @@ const App = withStyles(appStyles)(class extends ResizeResponsiveComponent<AppPro
}
/>
<ZoomedUiControl
dialogOpen={this.state.zoomedControlOpen}
controlInfo={this.state.zoomedControlInfo}
onDialogClose={()=>{ this.setState({zoomedControlOpen: false});} }
onDialogClosed={()=>{ this.model_.zoomedUiControl.set(undefined); }
}
/>
<Dialog
open={this.state.alertDialogOpen}
onClose={this.handleCloseAlert}
+1 -1
View File
@@ -169,7 +169,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
uploadAfter = await this.model.uploadBank(fileList[i], uploadAfter);
}
} catch (error) {
this.model.showAlert(error);
this.model.showAlert(error +"");
};
return uploadAfter;
}
+1 -1
View File
@@ -203,7 +203,7 @@ const FullScreenIME =
<Typography variant="body2">{this.props.caption}</Typography>
<Input key={value}
type="number"
defaultValue={control.formatValue(value)}
defaultValue={control.formatShortValue(value)}
error={this.state.error}
autoFocus
inputProps={{
+14 -1
View File
@@ -19,7 +19,7 @@
import React, {Component} from 'react';
import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
import { MonitorPortHandle, PiPedalModel, PiPedalModelFactory } from "./PiPedalModel";
import { MonitorPortHandle, PiPedalModel, State, PiPedalModelFactory } from "./PiPedalModel";
import SvgPathBuilder from './SvgPathBuilder'
//const char* model[] = {"12-TET","19-TET","24-TET", "31-TET", "53-TET"};
@@ -120,6 +120,7 @@ const GxTunerControl =
};
this.onFrequencyUpdated = this.onFrequencyUpdated.bind(this);
this.onStateChanged = this.onStateChanged.bind(this);
}
@@ -225,8 +226,19 @@ const GxTunerControl =
}
}
onStateChanged(state: State)
{
// initial or reconnect
if (state === State.Ready)
{
this.removeSubscription();
this.updateSubscription();
}
}
componentDidMount()
{
this.model.state.addOnChangedHandler(this.onStateChanged);
this.addSubscription();
}
componentDidUpdate() {
@@ -234,6 +246,7 @@ const GxTunerControl =
}
componentWillUnmount()
{
this.model.state.removeOnChangedHandler(this.onStateChanged);
this.removeSubscription();
}
+97 -8
View File
@@ -189,6 +189,12 @@ export enum PluginType {
}
export enum ControlType {
Dial,
OnOffSwitch,
Toggle,
Select
}
export class UiControl implements Deserializable<UiControl> {
deserialize(input: any): UiControl
@@ -210,9 +216,31 @@ export class UiControl implements Deserializable<UiControl> {
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<UiControl> {
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<UiControl> {
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];
+3 -8
View File
@@ -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;
+85
View File
@@ -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<string>;
alertMessage: ObservableProperty<string>;
state: ObservableProperty<State>;
visibilityState: ObservableProperty<VisibilityState>;
ui_plugins: ObservableProperty<UiPlugin[]>;
plugin_classes: ObservableProperty<PluginClass>;
jackConfiguration: ObservableProperty<JackConfiguration>;
@@ -272,6 +285,9 @@ export interface PiPedalModel {
presets: ObservableProperty<PresetIndex>;
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<WifiChannel[]>;
getAlsaDevices() : Promise<AlsaDeviceInfo[]>;
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<UiPlugin[]>
= new ObservableProperty<UiPlugin[]>([]);
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
pedalBoard: ObservableProperty<PedalBoard> = new ObservableProperty<PedalBoard>(new PedalBoard());
@@ -384,6 +405,7 @@ class PiPedalModelImpl implements PiPedalModel {
new PresetIndex()
);
zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined> = new ObservableProperty<ZoomedControlInfo | undefined>(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<number>("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);
}
};
+18 -1
View File
@@ -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)
+51 -71
View File
@@ -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<typeof styles> {
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<PluginControlProps, PluginControlState>
{
frameRef: React.RefObject<HTMLDivElement>;
imgRef: React.RefObject<HTMLImageElement>;
inputRef: React.RefObject<HTMLInputElement>;
selectRef: React.RefObject<HTMLSelectElement>;
displayValueRef: React.RefObject<HTMLDivElement>;
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<HTMLImageElement>): 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 (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: item_width, margin: 8 }}>
<div ref={this.frameRef} style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: item_width, margin: 8 }}>
{/* TITLE SECTION */}
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8,marginLeft: isSelect? 16: 0, marginRight: 0 }}>
<Typography variant="caption" display="block" noWrap style={{
@@ -697,7 +676,8 @@ const PluginControl =
) : (
<div style={{ flex: "0 1 auto" }}>
<img ref={this.imgRef} src="img/fx_dial.svg"
style={{ overscrollBehavior: "none", touchAction: "none", width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform() }}
style={{ overscrollBehavior: "none", touchAction: "none",
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform() }}
draggable="true"
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
@@ -720,7 +700,7 @@ const PluginControl =
<div>
<Input key={value}
type="number"
defaultValue={this.formatValue(control, value)}
defaultValue={control.formatShortValue(value)}
error={this.state.error}
inputProps={{
'aria-label':
+2 -2
View File
@@ -288,7 +288,7 @@ const PluginControlView =
}
return ((
<PluginControl uiControl={uiControl} value={controlValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { 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) => (
<PluginControl uiControl={plugin.getControl(controlValue.key)} value={controlValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={plugin.getControl(controlValue.key)} value={controlValue.value}
onChange={(value: number) => { this.onValueChanged(controlValue.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue.key, value) }}
requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)}
+7 -7
View File
@@ -185,7 +185,7 @@ const SplitControlView =
</div>
<div className={gridClass} >
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitTypeControl} value={typeValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitTypeControl} value={typeValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitAbControl} value={selectValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitAbControl} value={selectValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitMixControl} value={mixValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitMixControl} value={mixValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitPanLeftControl} value={panLValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitPanLeftControl} value={panLValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitVolLeftControl} value={volumeLValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitVolLeftControl} value={volumeLValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitPanRightControl} value={panRValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitPanRightControl} value={panRValue.value}
onChange={(value: number) => { 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 && (
<div style={{ flex: "0 0 auto" }}>
<PluginControl uiControl={SplitVolRightControl} value={volumeRValue.value}
<PluginControl instanceId={this.props.instanceId} uiControl={SplitVolRightControl} value={volumeRValue.value}
onChange={(value: number) => { this.onValueChanged(volumeRValue.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(volumeRValue.key, value) }}
requestIMEEdit={(uiControl, value) => { }}
+2 -1
View File
@@ -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 {
+11 -1
View File
@@ -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);
}
}
);
+369
View File
@@ -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<typeof styles> {
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<ZoomedDialProps, ZoomedDialState> {
model: PiPedalModel = PiPedalModelFactory.getInstance();
imgRef: React.RefObject<HTMLImageElement> = 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<HTMLImageElement>) {
if (this.isCapturedPointer(e)) {
--this.pointersDown;
e.preventDefault();
this.updateAngle(e);
this.commitAngle();
this.releaseCapture(e);
} else {
--this.pointersDown;
}
}
releaseCapture(e: PointerEvent<HTMLImageElement>)
{
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<HTMLImageElement>) {
if (this.isCapturedPointer(e)) {
--this.pointersDown;
this.releaseCapture(e);
}
}
onTouchStart(e: TouchEvent<HTMLImageElement>) {
//must be defined to get onTouchMove
}
onTouchMove(e: TouchEvent<HTMLImageElement>) {
// 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<HTMLImageElement>): 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<HTMLImageElement>): 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<HTMLImageElement>): 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<HTMLImageElement>): boolean {
return this.mouseDown
&& e.pointerId === this.pointerId
&& e.pointerType === this.pointerType;
}
onPointerMove(e: PointerEvent<HTMLImageElement>): 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<HTMLImageElement>): 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 (
<img ref={this.imgRef} src="img/fx_dial.svg"
style={{
overscrollBehavior: "none", touchAction: "none",
width: this.props.size, height: this.props.size, opacity: DEFAULT_OPACITY,
transform: this.getDefaultRotationTransform()
}}
draggable="true"
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
/>
);
}
}
);
export default ZoomedDial;
+168
View File
@@ -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<typeof styles> {
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<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />;
});
const ZoomedUiControl = withStyles(styles, { withTheme: true })(
class extends React.Component<ZoomedUiControlProps, ZoomedUiControlState> {
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 (
<DialogEx tag="zoomedControlDlg" open={this.props.dialogOpen}
onClose={()=> { this.props.onDialogClose()}}
onAbort={()=> { this.props.onDialogClose()}}
onExited={ () => {
this.props.onDialogClosed()
}
}
>
<div style={{ width: 300, height: 300, background: "#FFF",
display: "flex", flexFlow: "column", alignItems: "center", alignContent: "center", justifyContent: "center"
}}
onDrag={this.onDrag}
>
<Typography
display="block" variant="h6" color="textPrimary" style={{textAlign: "center"}}
>
{this.props.controlInfo?.uiControl.name??""}
</Typography>
<ZoomedDial size={200} controlInfo={this.props.controlInfo}
onPreviewValue={(v)=> {
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);
}
}}
/>
<Typography
display="block" variant="h6" color="textPrimary" style={{textAlign: "center"}}
>
{displayValue}
</Typography>
</div>
</DialogEx>
);
}
}
);
export default ZoomedUiControl;