Street testing fixes
This commit is contained in:
@@ -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
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -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
@@ -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':
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user