Memory hardening for AlsaDriver.

This commit is contained in:
Robin Davies
2024-10-05 02:14:58 -04:00
parent 46669cfda9
commit 9d4cc6e978
22 changed files with 1137 additions and 409 deletions
+10 -1
View File
@@ -20,6 +20,7 @@
import React from 'react';
import { ThemeProvider, createTheme, StyledEngineProvider, Theme } from '@mui/material/styles';
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode';
@@ -152,6 +153,8 @@ const theme = createTheme(
type AppThemeProps = {
};
const App = (class extends React.Component {
// Before the component mounts, we initialise our state
@@ -159,13 +162,19 @@ const App = (class extends React.Component {
super(props);
this.state = {
};
if (!App.virtualKeyboardHandler)
{
App.virtualKeyboardHandler = new VirtualKeyboardHandler();
}
}
static virtualKeyboardHandler?: VirtualKeyboardHandler;
render() {
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<AppThemed />
<AppThemed />
</ThemeProvider>
</StyledEngineProvider>
);
+22 -11
View File
@@ -232,10 +232,11 @@ const appStyles = (theme: Theme) => createStyles({
});
function supportsFullScreen(): boolean {
let doc: any = window.document;
let docEl: any = doc.documentElement;
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullscreen || docEl.msRequestFullScreen;
return (!!requestFullScreen);
}
@@ -244,10 +245,20 @@ function setFullScreen(value: boolean) {
let doc: any = window.document;
let docEl: any = doc.documentElement;
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
if (docEl.requestFullscren) // the latest offical api.
{
if (value)
{
window.document.documentElement.requestFullscreen({navigationUI: "show"});
} else {
window.document.exitFullscreen();
}
}
if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullscreen || docEl.msRequestFullScreen;
var cancelFullScreen = docEl.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
if (value) {
requestFullScreen.call(docEl);
}
else {
@@ -547,7 +558,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
});
}
toggleFullScreen(): void {
setFullScreen(this.state.isFullScreen);
setFullScreen(!this.state.isFullScreen);
this.setState({ isFullScreen: !this.state.isFullScreen });
}
@@ -735,7 +746,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
return (
<div style={{
colorScheme: isDarkMode() ? "dark" : "light", // affects scrollbar color
minHeight: 345, minWidth: 390,
minHeight: 300, minWidth: 300,
position: "absolute", left:0, top: 0, right:0,bottom:0,
overscrollBehavior: this.state.isDebug ? "auto" : "none"
}}
@@ -747,7 +758,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
>
<CssBaseline />
{this.state.performanceView ? (
<PerformanceView
<PerformanceView open={this.state.performanceView}
onClose={() => { this.setState({ performanceView: false }); }}
/>
) : (
@@ -1028,8 +1039,8 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
</DialogEx>
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Error ? "flex" : "none" }}
onMouseDown={preventDefault} onKeyDown={preventDefault}
<div className={classes.errorContent} style={{ zIndex: 1200, display: this.state.displayState === State.Error ? "flex" : "none" }}
onMouseDown={preventDefault} onKeyDown={preventDefault}
>
<div className={classes.errorContentMask} />
<div style={{ flex: "2 2 3px", height: 20 }} >&nbsp;</div>
@@ -1056,7 +1067,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
</div>
{/* initial load mask*/}
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Loading ? "block" : "none" }}>
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Loading ? "block" : "none",zIndex: 1201 }}>
<div className={classes.errorContentMask} />
<div className={classes.loadingBox}>
<div className={classes.loadingBoxItem}>
@@ -1069,7 +1080,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
</div>
{/* Reloading mask */}
<div className={classes.errorContent} style={{
zIndex: 501,
zIndex: 1201,
display: (
wantsReloadingScreen(this.state.displayState)
? "block" : "none"
+9 -3
View File
@@ -7,14 +7,21 @@ import { useTheme } from '@mui/styles';
const colors: string[] = colorKeys;
export enum DropdownAlignment {
SE, SW
}
interface ColorDropdownButtonProps {
currentColor?: string;
onColorChange: (color: string) => void;
dropdownAlignment: DropdownAlignment;
}
const ColorDropdownButton: React.FC<ColorDropdownButtonProps> = ({
currentColor = colors[0],
onColorChange,
dropdownAlignment = DropdownAlignment.SE
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedColor, setSelectedColor] = useState<string>(currentColor);
@@ -38,7 +45,6 @@ const ColorDropdownButton: React.FC<ColorDropdownButtonProps> = ({
setSelectedColor(color);
onColorChange(color);
};
return (
<div>
<ButtonBase
@@ -63,11 +69,11 @@ const ColorDropdownButton: React.FC<ColorDropdownButtonProps> = ({
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left')
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left'),
}}
>
<div style={{ display: "flex", flexFlow: "row wrap", width: 56 * 4 + 2 }}>
+42 -12
View File
@@ -28,29 +28,34 @@ interface DialogExProps extends DialogProps {
}
interface DialogExState {
}
export interface IDialogStackable {
getTag: ()=> string;
isOpen: ()=> boolean;
onDialogStackClose(): void;
}
class DialogStackEntry {
constructor(dialog: DialogEx)
constructor(dialog: IDialogStackable)
{
this.tag = dialog.props.tag;
this.tag = dialog.getTag();
this.dialog = dialog;
}
tag: string;
dialog: DialogEx;
dialog: IDialogStackable;
};
let dialogStack: DialogStackEntry[] = [];
function popDialogStack(): void {
export function popDialogStack(): void {
if (dialogStack.length !== 0)
{
dialogStack.splice(dialogStack.length-1,1);
}
}
function pushDialogStack(dialog: DialogEx): void {
export function pushDialogStack(dialog: IDialogStackable): void {
dialogStack.push(new DialogStackEntry(dialog));
}
@@ -66,18 +71,19 @@ export function removeDialogStackEntriesAbove(tag: string)
for (let i = dialogStack.length-1; i >= 0; --i)
{
let entry = dialogStack[i];
if (entry.tag === tag)
if (entry.dialog.getTag() === tag)
{
return;
}
popDialogStack();
if (entry.dialog.props.open && entry.dialog.props.onClose)
if (entry.dialog.isOpen())
{
entry.dialog.props.onClose({}, "backdropClick");
entry.dialog.onDialogStackClose();
}
}
}
class DialogEx extends React.Component<DialogExProps,DialogExState> {
class DialogEx extends React.Component<DialogExProps,DialogExState> implements IDialogStackable {
constructor(props: DialogExProps)
{
super(props);
@@ -93,6 +99,16 @@ class DialogEx extends React.Component<DialogExProps,DialogExState> {
hasHooks: boolean = false;
stateWasPopped: boolean = false;
getTag() {
return this.props.tag;
}
isOpen(): boolean {
return this.props.open;
}
onDialogStackClose() {
this.props.onClose?.({},"backdropClick");
}
handlePopState(ev: PopStateEvent): any
{
let evTag: DialogStackState | null = ev.state as DialogStackState | null;
@@ -169,11 +185,25 @@ class DialogEx extends React.Component<DialogExProps,DialogExState> {
if (this.props.onClose) this.props.onClose(event,reason);
}
onEnterKey() {
}
onKeyDown(evt: React.KeyboardEvent<HTMLDivElement>)
{
if (evt.key === 'Enter')
{
this.onEnterKey();
}
}
render() {
let { tag,onClose, ...extra} = this.props;
let { tag,onClose,...extra} = this.props;
return (
<Dialog fullWidth={this.props.fullWidth??false} maxWidth={this.props.fullWidth ? false: undefined} {...extra} onClose={(event,reason)=>{ this.myOnClose(event,reason);}}>
{this.props.children}
<Dialog fullWidth={this.props.fullWidth??false}
maxWidth={this.props.fullWidth ? false: undefined} {...extra}
onClose={(event,reason)=>{ this.myOnClose(event,reason);}}
onKeyDown={(evt)=>{ this.onKeyDown(evt); }}
>
{this.props.children}
</Dialog>
);
}
+78 -33
View File
@@ -26,7 +26,6 @@ import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu';
import MoreIcon from '@mui/icons-material/MoreVert';
import { PiPedalModel, PiPedalModelFactory, FileEntry } from './PiPedalModel';
import Link from '@mui/material/Link';
import { isDarkMode } from './DarkMode';
import Button from '@mui/material/Button';
import FileUploadIcon from '@mui/icons-material/FileUpload';
@@ -36,6 +35,7 @@ import FolderIcon from '@mui/icons-material/Folder';
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import IconButton from '@mui/material/IconButton';
import OldDeleteIcon from './OldDeleteIcon';
import Toolbar from '@mui/material/Toolbar';
@@ -49,7 +49,6 @@ import Typography from '@mui/material/Typography';
import DialogEx from './DialogEx';
import UploadFileDialog from './UploadFileDialog';
import OkCancelDialog from './OkCancelDialog';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
@@ -73,6 +72,7 @@ export interface FilePropertyDialogProps extends WithStyles<typeof styles> {
export interface FilePropertyDialogState {
fullScreen: boolean;
selectedFile: string;
selectedFileIsDirectory: boolean;
navDirectory: string;
hasSelection: boolean;
canDelete: boolean;
@@ -165,8 +165,9 @@ export default withStyles(styles, { withTheme: true })(
this.model = PiPedalModelFactory.getInstance();
this.state = {
fullScreen: false,
fullScreen: this.getFullScreen(),
selectedFile: props.selectedFile,
selectedFileIsDirectory: false,
navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty),
hasSelection: false,
canDelete: false,
@@ -182,6 +183,9 @@ export default withStyles(styles, { withTheme: true })(
};
this.requestScroll = true;
}
getFullScreen() {
return window.innerWidth < 450 || window.innerHeight < 450;
}
private scrollRef: HTMLDivElement | null = null;
@@ -244,7 +248,7 @@ export default withStyles(styles, { withTheme: true })(
onWindowSizeChanged(width: number, height: number): void {
this.setState({
fullScreen: height < 200
fullScreen: this.getFullScreen()
})
if (this.lastDivRef !== null) {
this.onMeasureRef(this.lastDivRef);
@@ -302,9 +306,9 @@ export default withStyles(styles, { withTheme: true })(
return hasSelection;
}
onSelectValue(selectedFile: string) {
onSelectValue(selectedFile: string, isDirectory: boolean) {
this.requestScroll = true;
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) })
this.setState({ selectedFile: selectedFile, selectedFileIsDirectory: isDirectory, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) })
}
onDoubleClickValue(selectedFile: string) {
this.openSelectedFile();
@@ -364,17 +368,17 @@ export default withStyles(styles, { withTheme: true })(
}
renderBreadcrumbs() {
if (this.state.navDirectory === "") {
return (<div style={{ height: 0 }} />);
return (<Divider />);
}
let breadcrumbs: React.ReactElement[] = [(
<Link underline="hover"
<Button variant="text"
color="inherit"
key="1" onClick={() => { this.navigate(""); }}
sx={{ display: 'flex', alignItems: 'center',cursor: "default" }}
key="h" onClick={() => { this.navigate(""); }}
style={{ textTransform: "none", flexShrink: 0 }}
startIcon={(<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />)}
>
<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />
Home
</Link>
<Typography style={{}} variant="body2" noWrap>Home</Typography>
</Button>
)
];
let directories = this.state.navDirectory.split("/");
@@ -383,25 +387,48 @@ export default withStyles(styles, { withTheme: true })(
target = pathConcat(target, directories[i]);
let myTarget = target;
breadcrumbs.push((
<Link underline="hover" key={(i + 1).toString()} style={{cursor: "default"}}
color="inherit" onClick={() => { this.navigate(myTarget) }}>
{directories[i]}
</Link>
<Typography key={"x"+(i)} variant="body2" style={{marginTop: 6, marginBottom: 6}} noWrap>/</Typography>
)
);
breadcrumbs.push((
<Button variant="text"
key={(i).toString()}
color="inherit"
onClick={() => { this.navigate(myTarget) }}
style={{ minWidth: 0,textTransform: "none" , flexShrink: 0 }}
>
<Typography variant="body2" style={{}} noWrap>{directories[i]}</Typography>
</Button>
));
}
{
let lastdirectory = directories[directories.length - 1];
breadcrumbs.push((
<Typography noWrap key={directories.length.toString()}
style={{cursor: "default"}}
<Typography key={"x"+(directories.length)} variant="body2" style={{userSelect: "none",marginTop: 6, marginBottom: 6}} noWrap>/</Typography>
)
);
breadcrumbs.push((
<div style={{flexShrink: 0,paddingLeft: 8, paddingRight: 8, paddingTop: 6, paddingBottom: 6, overflowX:"clip"}}>
<Typography key={directories.length.toString()} noWrap
style={{}} variant="body2"
color="text.secondary"> {lastdirectory}</Typography>
</div>
));
}
return (
<Breadcrumbs separator='/' aria-label="breadcrumb" style={{ marginLeft: 24 }}>
{breadcrumbs}
</Breadcrumbs>
<div>
<div aria-label="breadcrumb"
style={{ marginLeft: 16, marginBottom: 8, marginRight:8, textOverflow: "",display: "flex",flexFlow: "row wrap",alignItems: "center",justifyContent: "start" }}>
{breadcrumbs}
</div>
<Divider/>
</div>
);
}
@@ -446,12 +473,30 @@ export default withStyles(styles, { withTheme: true })(
render() {
let classes = this.props.classes;
let columnWidth = this.state.columnWidth;
let okButtonText = "Select";
if (this.state.hasSelection && this.state.selectedFileIsDirectory)
{
okButtonText = "Open";
}
return this.props.open &&
(
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="fileProperty" fullWidth maxWidth="xl" style={{ height: "90%" }}
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
<DialogEx
fullScreen={this.state.fullScreen}
onClose={() => {
this.props.onCancel();
}}
open={this.props.open} tag="fileProperty"
fullWidth maxWidth="xl"
PaperProps={
this.state.fullScreen ?
{}
:
{ style: {
minHeight: "90%",
maxHeight: "90%",
} }}
>
<DialogTitle >
<DialogTitle style={{paddingBottom: 0}} >
<Toolbar style={{ padding: 0 }}>
<IconButton
edge="start"
@@ -507,17 +552,17 @@ export default withStyles(styles, { withTheme: true })(
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onRename(); }}>Rename</MenuItem>)}
</Menu>
</Toolbar>
<div style={{ flex: "0 0 auto", marginLeft: "auto", marginRight: "auto" }}>
<div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}>
{this.renderBreadcrumbs()}
</div>
</DialogTitle>
<div style={{ flex: "0 0 auto", height: "1px", background: isDarkMode() ? "rgba(255,255,255,0.1" : "rgba(0,0,0,0.2" }}>&nbsp;</div>
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row wrap", overflowX: "hidden", overflowY: "auto" }}>
<DialogContent style={{paddingLeft: 0, paddingRight: 0, paddingTop: 0,colorScheme: (isDarkMode() ? "dark" : "light")}}>
<div
ref={(element) => this.onMeasureRef(element)}
style={{
flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16, paddingBottom: 16
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16
}}>
{
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
@@ -549,7 +594,7 @@ export default withStyles(styles, { withTheme: true })(
<ButtonBase key={value.filename}
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
onClick={() => this.onSelectValue(value.filename)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }}
onClick={() => this.onSelectValue(value.filename,value.isDirectory)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }}
>
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
@@ -569,8 +614,8 @@ export default withStyles(styles, { withTheme: true })(
)
}
</div>
</div>
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}>&nbsp;</div>
</DialogContent>
<Divider/>
<DialogActions style={{ justifyContent: "stretch" }}>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
@@ -594,7 +639,7 @@ export default withStyles(styles, { withTheme: true })(
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
>
Select
{okButtonText}
</Button>
</div>
</DialogActions>
+1 -1
View File
@@ -200,7 +200,7 @@ const FullScreenIME =
<div style={{
width: "100%", height: "100%", position: "relative",
display: "flex", flexDirection: "column", flexWrap: "nowrap",
justifyContent: "center", alignItems: "center"
justifyContent: "start", alignItems: "center", marginTop:12
}}
>
<Typography noWrap variant="body2">{this.props.caption}</Typography>
+2 -2
View File
@@ -519,8 +519,8 @@ export const MainPage =
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center", minWidth: 0,
width: "100%", height: 48, paddingLeft: 16, paddingRight: 16
}} >
<div style={{ flex: "0 0 auto", width: 80 }} >
<div style={{ display: bypassVisible ? "block" : "none", width: 80 }} >
<div style={{ flex: "0 0 auto", width: this.state.splitControlBar? undefined: 80 }} >
<div style={{ display: bypassVisible ? "block" : "none", width: this.state.splitControlBar? undefined: 80 }} >
<Switch color="secondary" checked={bypassChecked} onChange={this.handleEnableCurrentItemChanged} />
</div>
</div>
+2 -2
View File
@@ -348,9 +348,9 @@ export function getBorderColor(colorKey: string): string {
} else {
if (colorKey === "grey")
{
return list[300];
return list[500];
}
return list[200];
return list[400];
}
}
+1 -1
View File
@@ -228,7 +228,7 @@ export class SnapshotValue {
result.instanceId = item.instanceId;
result.controlValues = ControlValue.deserializeArray(item.controlValues);
result.lv2State = item.lv2State; // we can do this, because lv2State is immutable.
result.pathProperties = {... item.pathProperties}; // clone the dictionary.
result.pathProperties = {...item.pathProperties}; // clone the dictionary.
return result;
}
instanceId: number = -1;
+109 -10
View File
@@ -28,14 +28,18 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButton from '@mui/material/IconButton';
import AppBar from '@mui/material/AppBar';
import SnapshotPanel from './SnapshotPanel';
import ArrowLeftOutlined from '@mui/icons-material/ArrowLeft';
import ArrowRightOutlined from '@mui/icons-material/ArrowRight';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
//import ArrowLeftOutlined from '@mui/icons-material/ArrowLeft';
//import ArrowRightOutlined from '@mui/icons-material/ArrowRight';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import { isDarkMode } from './DarkMode';
import { BankIndex } from './Banks';
import SnapshotEditor from './SnapshotEditor';
import { Snapshot } from './Pedalboard';
import JackStatusView from './JackStatusView';
import {IDialogStackable, DialogStackState, removeDialogStackEntriesAbove, popDialogStack, pushDialogStack} from './DialogEx';
const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
@@ -63,6 +67,7 @@ const styles = (theme: Theme) => createStyles({
interface PerformanceViewProps extends WithStyles<typeof styles> {
open: boolean,
onClose: () => void,
theme: Theme
}
@@ -78,7 +83,7 @@ interface PerformanceViewState {
export const PerformanceView =
withStyles(styles, { withTheme: true })(
class extends ResizeResponsiveComponent<PerformanceViewProps, PerformanceViewState> {
class extends ResizeResponsiveComponent<PerformanceViewProps, PerformanceViewState> implements IDialogStackable {
model: PiPedalModel;
constructor(props: PerformanceViewProps) {
@@ -96,7 +101,76 @@ export const PerformanceView =
};
this.onPresetsChanged = this.onPresetsChanged.bind(this);
this.onBanksChanged = this.onBanksChanged.bind(this);
this.handlePopState = this.handlePopState.bind(this);
}
getTag() { return "performView"}
isOpen() { return this.props.open }
onDialogStackClose() {
this.props.onClose();
}
private stateWasPopped: boolean = false;
private hasHooks: boolean = false;
updateHooks() : void {
let wantHooks = this.mounted && this.props.open;
if (wantHooks !== this.hasHooks)
{
this.hasHooks = wantHooks;
if (this.hasHooks)
{
this.stateWasPopped = false;
window.addEventListener("popstate",this.handlePopState);
pushDialogStack(this);
let dialogStackState: DialogStackState = {tag: this.getTag(),previousState: window.history.state as DialogStackState | null};
window.history.replaceState(dialogStackState,"",window.location.href);
window.history.pushState(
null,
"",
"#" + this.getTag()
);
} else {
if (!this.stateWasPopped)
{
window.history.back();
}
}
}
}
handlePopState(ev: PopStateEvent): any
{
let evTag: DialogStackState | null = ev.state as DialogStackState | null;
if (evTag && evTag.tag === this.getTag()) {
removeDialogStackEntriesAbove(this.getTag());
if (!this.stateWasPopped)
{
this.stateWasPopped = true;
popDialogStack ();
if (this.props.open) {
this.props.onClose();
}
}
window.history.replaceState(evTag.previousState,"",window.location.href);
window.removeEventListener("popstate",this.handlePopState);
if (window.location.hash === "#menu") // The menu popstate is next?
{
window.history.back(); // then clear off the menu popstate too.
}
ev.stopPropagation();
}
}
onWindowSizeChanged(width: number, height: number): void {
this.setState({ wrapSelects: width < 700 });
}
@@ -107,19 +181,34 @@ export const PerformanceView =
onBanksChanged(newValue: BankIndex) {
this.setState({ banks: this.model.banks.get() })
}
private mounted: boolean = false;
componentDidMount(): void {
super.componentDidMount();
this.mounted = true;
this.model.presets.addOnChangedHandler(this.onPresetsChanged);
this.model.banks.addOnChangedHandler(this.onBanksChanged);
this.setState({
presets: this.model.presets.get(),
banks: this.model.banks.get()
});
this.updateHooks();
}
componentWillUnmount(): void {
this.model.presets.removeOnChangedHandler(this.onPresetsChanged);
this.model.banks.removeOnChangedHandler(this.onBanksChanged);
this.mounted = false;
this.updateHooks();
super.componentWillUnmount();
}
handlePresetSelectClose(event: any): void {
let value = event.currentTarget.getAttribute("data-value");
@@ -178,7 +267,7 @@ export const PerformanceView =
<div className={this.props.classes.frame} style={{ overflow: "clip", height: "100%" }} >
<AppBar id="select-plugin-dialog-title"
style={{
position: "static", flex: "0 0 auto", height: wrapSelects ? 104 : 54,
position: "static", flex: "0 0 auto", height: wrapSelects ? 108 : 58,
display: this.state.showSnapshotEditor ? "none" : undefined
}}
>
@@ -188,7 +277,10 @@ export const PerformanceView =
alignItems: "start"
}}>
<IconButton aria-label="menu" color="inherit"
onClick={() => { this.props.onClose(); }} style={{ flex: "0 0 auto" }} >
onClick={() => { this.props.onClose(); }}
style={{
position: "relative",top: 3,
flex: "0 0 auto" }} >
<ArrowBackIcon />
</IconButton>
@@ -206,12 +298,13 @@ export const PerformanceView =
<IconButton
aria-label="previous-bank"
onClick={() => { this.handlePreviousBank(); }}
size="medium"
color="inherit"
style={{
borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4
}}
>
<ArrowLeftOutlined style={{ opacity: 0.75 }} />
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
</IconButton>
<Select variant="standard"
@@ -239,10 +332,11 @@ export const PerformanceView =
<IconButton
aria-label="next-bank"
onClick={() => { this.handleNextBank(); }}
size="large"
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowRightOutlined style={{ opacity: 0.75 }} />
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
</IconButton>
</div>
@@ -254,7 +348,7 @@ export const PerformanceView =
color="inherit"
style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }}
>
<ArrowLeftOutlined style={{ opacity: 0.75 }} />
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
</IconButton>
<Select variant="standard"
className={classes.select}
@@ -288,16 +382,21 @@ export const PerformanceView =
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowRightOutlined style={{ opacity: 0.75 }} />
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
</IconButton>
</div>
</div>
</div>
</AppBar >
<div style={{ flex: "1 0 auto", display: "flex", marginTop: 16 }}>
<div style={{ flex: "1 0 auto", display: "flex", marginTop: 16,marginBottom: 20 }}>
<SnapshotPanel onEdit={(index) => { return this.handleOnEdit(index); }} />
</div>
{!this.state.showSnapshotEditor && (
<JackStatusView />)
}
</div>
{this.state.showSnapshotEditor && (
<SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex}
+3 -1
View File
@@ -653,7 +653,9 @@ const PluginControlView =
<FilePropertyDialog open={this.state.showFileDialog}
fileProperty={this.state.dialogFileProperty}
selectedFile={this.state.dialogFileValue}
onCancel={() => { this.setState({ showFileDialog: false }); }}
onCancel={() => {
this.setState({ showFileDialog: false });
}}
onOk={(fileProperty, selectedFile) => {
this.model.setPatchProperty(
+32
View File
@@ -0,0 +1,32 @@
export default class Rectangle {
top: number;
left: number;
width: number;
height: number;
constructor(top?: number, left?: number, width?: number, height?: number) {
this.top = top ? top: 0;
this.left = left ? left: 0;
this.width = width ? width: 0;
this.height = height ? height: 0;
}
get right(): number {
return this.left + this.width;
}
get bottom(): number {
return this.top + this.height;
}
set right(value: number) {
this.width = value - this.left;
}
set bottom(value: number) {
this.height = value - this.top;
}
};
+10 -5
View File
@@ -19,13 +19,14 @@
import React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import DialogEx from './DialogEx';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import { nullCast } from './Utility';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
//import TextFieldEx from './TextFieldEx';
import TextField from '@mui/material/TextField';
export interface RenameDialogProps {
@@ -113,15 +114,19 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
}
};
return (
<DialogEx tag="nameDialog" open={open} fullWidth onClose={handleClose} aria-labelledby="Rename-dialog-title"
<DialogEx tag="nameDialog" open={open} fullWidth maxWidth="sm" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{userSelect: "none"}}
>
<DialogContent>
<DialogContent style={{minHeight: 96}}>
<TextField
autoFocus
onKeyDown={handleKeyDown}
margin="dense"
variant="outlined"
inputProps={{
scrollMargin: 24,
}}
id="name"
label="Name"
type="text"
@@ -130,7 +135,7 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
inputRef={this.refText}
/>
</DialogContent>
<DialogActions>
<DialogActions style={{flexShrink: 1}}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
+5 -3
View File
@@ -38,7 +38,8 @@ import { isDarkMode } from './DarkMode';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import { Snapshot } from './Pedalboard';
import ColorDropdownButton from './ColorDropdownButton';
import ColorDropdownButton, { DropdownAlignment } from './ColorDropdownButton';
import { getBackgroundColor } from './MaterialColors';
const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
@@ -252,7 +253,7 @@ const SnapshotEditor = withStyles(appStyles)(class extends ResizeResponsiveCompo
}}
>
<CssBaseline />
<AppBar position="static" sx={{ bgcolor: isDarkMode() ? "#100020" : "#200040" }} >
<AppBar position="static" sx={{ bgcolor: isDarkMode() ? getBackgroundColor("purple") : "#200040" }} >
<Toolbar variant="dense" >
<IconButton
edge="start"
@@ -288,6 +289,7 @@ const SnapshotEditor = withStyles(appStyles)(class extends ResizeResponsiveCompo
</div>
<ColorDropdownButton
currentColor={this.state.color}
dropdownAlignment={DropdownAlignment.SW}
onColorChange={(newColor) => {
this.setState({ color: newColor });
}} />
@@ -335,7 +337,7 @@ const SnapshotEditor = withStyles(appStyles)(class extends ResizeResponsiveCompo
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
}
/>
{this.state.showStatusMonitor && (<JackStatusView />)}
{ this.state.showStatusMonitor && (<JackStatusView />)}
</div >
);
}
+9 -38
View File
@@ -25,7 +25,6 @@
import React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import ButtonBase from "@mui/material/ButtonBase"
import Typography from '@mui/material/Typography';
import DialogEx from './DialogEx';
import DialogTitle from '@mui/material/DialogTitle';
@@ -33,8 +32,9 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButton from '@mui/material/IconButton';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import { colorKeys, getBackgroundColor } from "./MaterialColors";
import { colorKeys } from "./MaterialColors";
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import ColorDropdownButton, { DropdownAlignment } from './ColorDropdownButton';
const snapshotColors = colorKeys.slice(0,100);
const NBSP = "\u00A0";
@@ -111,7 +111,7 @@ export default class SnapshotPropertiesDialog extends ResizeResponsiveComponent<
let okButtonText = this.props.editing ? "OK" : "Save";
return (
<DialogEx maxWidth={false} tag="snapshotProps" open={this.props.open} onClose={handleClose}
<DialogEx maxWidth="sm" fullWidth={true} tag="snapshotProps" open={this.props.open} onClose={handleClose}
style={{ userSelect: "none" }} fullScreen={this.state.compactVertical}
>
<DialogTitle>
@@ -126,7 +126,7 @@ export default class SnapshotPropertiesDialog extends ResizeResponsiveComponent<
</div>
</DialogTitle>
<DialogContent>
<DialogContent style={{paddingBottom: 0}}>
<TextField variant="standard" style={{ flexGrow: 1, flexBasis: 1 }}
autoComplete="off"
@@ -144,40 +144,11 @@ export default class SnapshotPropertiesDialog extends ResizeResponsiveComponent<
}}
/>
<Typography variant="caption" color="textSecondary">Color</Typography>
<div style={{ margin: 4,display: "flex", flexFlow: "row wrap", gap: 8 }}>
{
snapshotColors.map((colorKey, index) => {
let selected = this.state.color === colorKey;
let color = getBackgroundColor(colorKey);
return (
<ButtonBase
key={"colorTag" + index}
style={{
width: 48, height: 48,
borderRadius: 8,
borderStyle: "solid",
borderColor: selected ? color : "transparent",
borderWidth: 2,
}}
onClick={()=> {
this.setState({color: colorKey});
}}
>
<div style={{
width: 40, height: 40,
borderRadius: 4,
margin: 2,
background: color
}}>
</div>
</ButtonBase>
);
})
}
</div>
<ColorDropdownButton aria-label="color" currentColor={this.state.color} dropdownAlignment={DropdownAlignment.SE}
onColorChange={(newcolor) => {
this.setState({color: newcolor});
}} />
</DialogContent>
<DialogActions>
<Button variant="dialogSecondary" onClick={handleClose} >
+184
View File
@@ -0,0 +1,184 @@
import React from 'react';
import TextField, { TextFieldProps } from '@mui/material/TextField';
import { withStyles, WithStyles } from '@mui/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import Modal from '@mui/material/Modal';
const styles = (theme: Theme) => createStyles({
});
export interface TextFieldExPropsBase extends WithStyles<typeof styles> {
label?: string;
containerStyle?: React.CSSProperties;
autoFocus?: boolean;
inputRef?: React.Ref<any>;
theme: Theme;
};
export type TextFieldExProps = TextFieldExPropsBase & TextFieldProps;
export interface TextFieldExState {
androidHosted: boolean;
composingText: string;
displayText: string;
showIme: boolean;
imeValue: string;
};
const TextFieldEx = withStyles(styles, { withTheme: true })(
class extends React.Component<TextFieldExProps, TextFieldExState> {
constructor(props: TextFieldExProps) {
super(props);
this.state = {
composingText: "",
displayText: "",
androidHosted: true,
showIme: false,
imeValue: ""
};
this.handleClick = this.handleClick.bind(this);
this.handleImeCompositionStart = this.handleImeCompositionStart.bind(this);
this.handleCompositionEnd = this.handleCompositionEnd.bind(this);
this.handleImeCompositionUpdate = this.handleImeCompositionUpdate.bind(this);
this.handleImeCompositionEnd = this.handleImeCompositionEnd.bind(this);
}
private getUseImeMask() {
return this.state.androidHosted && window.innerHeight < 4500;
}
private originalInputHref: HTMLInputElement | undefined = undefined;
private imeInputHref: HTMLInputElement | undefined = undefined;
handleInputRef(input: HTMLInputElement) {
if (input) {
this.originalInputHref = input;
if (this.getUseImeMask()) {
input.addEventListener('click', this.handleClick);
input.addEventListener('input', this.handleInput);
}
} else {
if (this.originalInputHref) {
this.originalInputHref.removeEventListener('click', this.handleClick);
this.originalInputHref.removeEventListener('input', this.handleInput);
this.originalInputHref = undefined;
}
}
}
handleClick(event: FocusEvent) {
(event.target as any).blur();
event.preventDefault();
event.stopPropagation();
this.setState({
imeValue: (event.target as any).value as string,
showIme: true
});
return false;
}
handleImeCompositionStart(event: any) {
// this.setState({
// composingText: '' ,
// imeValue: '', //event.target.value as string,
// showIme: true
// });
}
handleImeCompositionUpdate(event: CompositionEvent) {
this.setState({ composingText: event.data });
};
handleCompositionEnd = (event: CompositionEvent) => {
this.setState(prevState => ({
displayText: prevState.displayText + event.data,
composingText: '',
}));
};
dismissIme() {
if (this.originalInputHref && this.imeInputHref)
{
let value = this.imeInputHref.value;
this.originalInputHref.value = this.imeInputHref.value;
this.originalInputHref.blur();
this.imeInputHref.blur();
this.setState({
showIme: false, imeValue: value
});
}
}
handleImeCompositionEnd()
{
this.dismissIme();
}
handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!this.state.composingText) {
this.setState({ displayText: target.value });
}
};
render() {
let { autoFocus, containerStyle, label, ...extra } = this.props;
return (
<div style={this.props.containerStyle}>
<TextField {...extra} autoFocus={autoFocus} tabIndex={-1} inputRef={(input) => this.handleInputRef(input)} />
<p>Display Text: {this.state.imeValue}</p>
<p>Composing Text: {this.state.composingText}</p>
{this.state.showIme && (
<Modal open={this.state.showIme}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
background: this.props.theme.palette.background.default,
display: "flex", flexFlow: "row nowrap",justifyContent: "center"
}}>
<TextField variant="standard"
inputRef={(element: HTMLInputElement)=>{
if (this.imeInputHref) {
this.imeInputHref.removeEventListener("compositionend",this.handleImeCompositionEnd);
this.imeInputHref.removeEventListener("compositionupdate",this.handleImeCompositionUpdate);
this.imeInputHref.removeEventListener("compositionstart",this.handleImeCompositionStart);
}
this.imeInputHref = element;
if (this.imeInputHref)
{
this.imeInputHref.addEventListener("compositionend",this.handleImeCompositionEnd);
this.imeInputHref.addEventListener("compositionupdate",this.handleImeCompositionUpdate);
this.imeInputHref.addEventListener("compositionstart",this.handleImeCompositionStart);
}
}}
style={{width: 200,marginTop: 16}}
autoFocus
value={this.state.imeValue}
onChange={(event: any) => {
this.setState({ imeValue: event.target.value as string });
}}
onBlur={() => {
this.dismissIme();
}}
onKeyDown={(event)=> {
if (event.key === "Enter")
{
this.setState({showIme: false});
}
}}
/>
</div>
</Modal>
)}
</div>
);
}
});
export default TextFieldEx;
+4 -3
View File
@@ -230,16 +230,15 @@ const ToobFrequencyResponseView =
currentPath: string = "";
majorGridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode {
return (<line x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.75" opacity="1" />);
return (<line key={"l" + this.nextKey++} x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.75" opacity="1" />);
}
gridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode {
return (<line x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.25" opacity="1" />);
return (<line key={"l" + this.nextKey++} x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.25" opacity="1" />);
}
grid(): React.ReactNode[] {
let result: React.ReactNode[] = [];
for (var db = Math.ceil(this.dbMax / this.dbTickSpacing) * this.dbTickSpacing; db < this.dbMin; db += this.dbTickSpacing) {
var y = (db - this.dbMin) / (this.dbMax - this.dbMin) * (this.yMax - this.yMin);
if (db === 0) {
@@ -274,8 +273,10 @@ const ToobFrequencyResponseView =
dbMin: number = 5;
dbMax: number = -35;
private nextKey: number = 0;
render() {
// deliberately reversed to flip up and down.
this.nextKey = 0;
this.dbMax = this.state.minDb;
this.dbMin = this.state.maxDb;
+150
View File
@@ -0,0 +1,150 @@
// Copyright (c) 2024 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.
// converts android handling of the virtual keyboard to use pan instead of zoom.
import Rectangle from './Rectangle';
export default class VirtualKeyboardHandler
{
enabled: boolean = false;
originalPosition: Rectangle
constructor()
{
let enabled = this.isChromeMobile();
if ("virtualKeyboard" in navigator) {
this.virtualKeyboard = navigator["virtualKeyboard"];
if (this.virtualKeyboard)
{
alert("Has Virtual keyboard!");
this.virtualKeyboard.addEventListener("geometryChange",this.handleGeometryChange);
}
}
if ('visualViewport' in window) {
window.visualViewport?.addEventListener('resize', this.handleVisualViewportResize.bind(this));
} else {
enabled = false;
}
this.enabled = enabled;
this.originalPosition = new Rectangle(0,0, window.innerWidth,window.innerHeight)
}
private handleVisualViewportResize(event: Event): void {
const viewport = event.target as VisualViewport;
if (viewport.width !== this.originalPosition.width)
{
// a flip, not a virtual keyboard event.
this.originalPosition = new Rectangle(0,0,viewport.width,viewport.height);
this.resetContentPosition();
}
const heightDifference = this.originalPosition.height - viewport.height;
if (Math.abs(heightDifference) < 150)
{
// fullscren !fullscreen
this.originalPosition = new Rectangle(0,0,viewport.width,viewport.height);
this.resetContentPosition();
}
let viewportRect = new Rectangle(viewport.offsetLeft,viewport.offsetTop,viewport.width,viewport.height);
if (heightDifference > 0) {
// Keyboard is likely visible
this.panContent(viewportRect);
} else {
// Keyboard is likely hidden
this.resetContentPosition();
}
}
panContent(viewportRect: Rectangle)
{
let focusObject = this.getFocusedInputElement();
if (focusObject) {
let clientRect_ = focusObject.getBoundingClientRect();
let clientRect = new Rectangle(clientRect_.left,clientRect_.top,clientRect_.width,clientRect_.height);
// center the client in the available height.
let yOffset = viewportRect.top + viewportRect.height/2 - (clientRect.top + clientRect.height/2);
if (yOffset < 0) {
yOffset = 0;
}
let transform = `translateY(-${yOffset}px)`;
document.body.style.transform = transform;
document.body.style.height = `${this.originalPosition.height}px`;
document.body.style.overflow = 'hidden';
}
}
resetContentPosition() {
document.body.style.transform = "";
document.body.style.height = "";
document.body.style.overflow = "hidden";
}
getFocusedInputElement(): HTMLInputElement | null {
const element = document.activeElement;
return element instanceof HTMLInputElement ? element : null;
}
isChromeMobile(): boolean {
const userAgent = navigator.userAgent.toLowerCase();
const vendor = navigator.vendor.toLowerCase();
const isAndroid = /android/.test(userAgent);
const isChrome = /chrome/.test(userAgent) && /google inc/.test(vendor);
const isMobile = /mobile/.test(userAgent);
// Additional check for Chrome's navigator.userAgentData (if available)
const userAgentData = (navigator as any).userAgentData;
const isMobileDevice = userAgentData?.mobile ?? false;
// Check if it's Chrome
if (isChrome) {
// Check if it's on Android or explicitly marked as mobile
if (isAndroid || isMobile || isMobileDevice) {
return true;
}
}
return false;
}
private handleGeometryChange(event: any) {
}
private virtualKeyboard: any;
componentDidMount(): void {
}
componentWillUnmount(): void {
if (this.virtualKeyboard)
{
this.virtualKeyboard.removeEventListener("geometryChange",this.handleGeometryChange);
}
}
}