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 }}>
+
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
+ }}>
{
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
- this.state.files.map(
- (value: string, index: number) => {
- let displayValue = value;
- if (displayValue === "")
- {
+ this.state.fileEntries.map(
+ (value: FileEntry, index: number) => {
+ let isDefault = this.props.fileProperty.directory.startsWith("default");
+ let displayValue = value.filename;
+ if (displayValue === "") {
displayValue = "
";
} else {
- displayValue = this.fileNameOnly(value);
+ if (isDefault)
+ {
+ displayValue = pathFileName(displayValue);
+ } else {
+ displayValue = pathFileNameOnly(displayValue);
+ }
}
- let selected = value === this.state.selectedFile;
+ let selected = value.filename === this.state.selectedFile;
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
+ if (isDarkMode()) {
+ selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
+ }
let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined;
- if (selected)
- {
- scrollRef = (element)=> { this.onScrollRef(element)};
+ if (selected) {
+ scrollRef = (element) => { this.onScrollRef(element) };
}
return (
- this.onSelectValue(value)} onDoubleClick={()=> {this.onDoubleClickValue(value);}}
+ onClick={() => this.onSelectValue(value.filename)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }}
>
-
-
{displayValue}
+ {value.filename === "" ?
+ (
)
+ : (
+ value.isDirectory ? (
+
+ ) : (
+
+ ))}
+
{displayValue}
);
@@ -333,25 +572,26 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent
this.handleDelete()} >
+ disabled={!this.state.hasSelection || this.state.selectedFile === ""}
+ onClick={() => this.handleDelete()} >
}
- onClick={()=> { this.setState({openUploadFileDialog: true})}}
+ onClick={() => { this.setState({ openUploadFileDialog: true }) }}
>
Upload
-
@@ -364,22 +604,160 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent { this.requestFiles();}}
+ uploadPage={
+ "uploadUserFile?directory=" + encodeURIComponent(
+ pathConcat(this.props.fileProperty.directory,this.state.navDirectory)
+ )
+ + "&ext="
+ + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
+ }
+ onUploaded={() => { this.requestFiles(this.state.navDirectory); }}
fileProperty={this.props.fileProperty}
/>
this.handleConfirmDelete()}
- onClose={()=>{
- this.setState({openConfirmDeleteDialog: false});
+ onOk={() => this.handleConfirmDelete()}
+ onClose={() => {
+ this.setState({ openConfirmDeleteDialog: false });
}}
/>
+ {
+ this.state.newFolderDialogOpen && (
+ { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }}
+ onClose={() => { this.setState({ newFolderDialogOpen: false }); }}
+ acceptActionName="OK"
+ />
+ )
+ }
+ {
+ this.state.renameDialogOpen && (
+ { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
+ onClose={() => { this.setState({ renameDialogOpen: false }); }}
+ acceptActionName="OK"
+ />
+
+ )
+ }
+ {
+ this.state.moveDialogOpen && (
+ (
+ {this.setState({moveDialogOpen: false});}}
+ onOk={
+ (path) => {
+ this.setState({moveDialogOpen: false});
+ this.setDefaultPath(path);
+ this.onExecuteMove(path);
+ }
+ }
+
+ />
+ )
+ )
+ }
);
}
-}
\ No newline at end of file
+ openSelectedFile(): void {
+ if (this.isDirectory(this.state.selectedFile)) {
+ let directoryName = pathFileNameOnly(this.state.selectedFile);
+ let navDirectory = pathConcat(this.state.navDirectory, directoryName);
+ this.requestFiles(navDirectory);
+ this.setState({ navDirectory: navDirectory });
+ } else {
+ this.props.onOk(this.props.fileProperty, this.state.selectedFile);
+ }
+ }
+
+ private renameDefaultName(): string {
+ let name = this.state.selectedFile;
+ if (name === "") return "";
+ if (this.isDirectory(name))
+ {
+ return pathFileName(name);
+ } else {
+ return pathFileNameOnly(name);
+ }
+ }
+ private onMove(): void {
+ this.setState({ moveDialogOpen: true });
+ }
+ private onExecuteMove(newDirectory: string)
+ {
+ let fileName = pathFileName(this.state.selectedFile);
+ let oldFilePath = pathConcat(this.state.navDirectory,fileName);
+ let newFilePath = pathConcat(newDirectory,fileName);
+
+ this.model.renameFilePropertyFile(oldFilePath,newFilePath,this.props.fileProperty)
+ .then(()=>{
+ this.requestFiles(this.state.navDirectory);
+ })
+ .catch((e)=>{
+ this.model.showAlert(e.toString());
+ });
+
+ }
+
+ private onRename(): void {
+ this.setState({ renameDialogOpen: true });
+ }
+ private onExecuteRename(newName: string) {
+ let newPath: string = "";
+ let oldPath: string = "";
+ if (this.isDirectory(this.state.selectedFile))
+ {
+ let oldName = pathFileName(this.state.selectedFile);
+ if (oldName === newName) return;
+ oldPath = pathConcat(this.state.navDirectory,oldName);
+ newPath = pathConcat(this.state.navDirectory,newName);;
+ } else {
+ let oldName = pathFileNameOnly(this.state.selectedFile);
+ if (oldName === newName) return;
+ let extension = pathExtension(this.state.selectedFile);
+ oldPath = pathConcat(this.state.navDirectory,oldName+extension);
+ newPath = pathConcat(this.state.navDirectory,newName + extension);
+ }
+ this.model.renameFilePropertyFile(oldPath,newPath,this.props.fileProperty)
+ .then((newPath)=>{
+ this.setState({selectedFile: newPath});
+ this.requestFiles(this.state.navDirectory);
+ this.requestScroll = true
+
+ })
+ .catch((e) =>{
+ this.model.showAlert(e.toString());
+ });
+ }
+
+ private onNewFolder(): void {
+ this.setState({ newFolderDialogOpen: true });
+ }
+
+ private onExecuteNewFolder(newName: string) {
+ this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory,newName), this.props.fileProperty)
+ .then((newPath) => {
+ this.setState({selectedFile: newPath});
+ this.requestFiles(this.state.navDirectory);
+ this.requestScroll = true
+ })
+ .catch((e) => {
+ this.model.showAlert(e.toString());
+ }
+ );
+ }
+});
\ No newline at end of file
diff --git a/react/src/FilePropertyDirectorySelectDialog.tsx b/react/src/FilePropertyDirectorySelectDialog.tsx
new file mode 100644
index 0000000..ec468c3
--- /dev/null
+++ b/react/src/FilePropertyDirectorySelectDialog.tsx
@@ -0,0 +1,347 @@
+// 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.
+
+import React from 'react';
+import Button from '@mui/material/Button';
+import DialogEx from './DialogEx';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import ResizeResponsiveComponent from './ResizeResponsiveComponent';
+import ArrowRightIcon from '@mui/icons-material/ArrowRight';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
+import { UiFileProperty } from './Lv2Plugin';
+import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
+import ButtonBase from '@mui/material/ButtonBase';
+import Typography from '@mui/material/Typography'
+import Divider from '@mui/material/Divider';
+import FolderIcon from '@mui/icons-material/Folder';
+import HomeIcon from '@mui/icons-material/Home';
+import { isDarkMode } from './DarkMode';
+import IconButton from '@mui/material/IconButton';
+
+
+function pathConcat(l: string, r: string): string
+{
+ if (l.length === 0) return r;
+ if (r.length === 0) return l;
+ return l +'/' + r;
+}
+
+class DirectoryTree {
+ name: string = "";
+ path: string = "";
+ expanded: boolean = false;
+ children: DirectoryTree[] = [];
+
+ constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) {
+ this.name = tree.directoryName;
+ if (parentTree) {
+ this.path = pathConcat(parentTree.path,tree.directoryName);
+ } else {
+ this.name = "Home";
+ this.path = tree.directoryName;
+ this.expanded = true;
+ }
+ for (var treeChild of tree.children) {
+ this.children.push(new DirectoryTree(treeChild, this));
+ }
+ }
+ canExpand(): boolean {
+ return this.children.length !== 0;
+ }
+ expand(path: string): void {
+ this.expanded = true;
+
+ let currentNode: DirectoryTree = this;
+ for (var segment of path.split('/')) {
+ let found = false;
+ for (var child of currentNode.children) {
+ if (child.name === segment) {
+ found = true;
+ if (child.children.length !== 0) {
+ child.expanded = true;
+ }
+ currentNode = child;
+ break;
+ }
+ }
+ if (!found) {
+ break;
+ }
+ }
+ }
+ private static remove_(directoryTree: DirectoryTree, path: string): boolean
+ {
+ for (let i = 0; i < directoryTree.children.length; ++i)
+ {
+ let child = directoryTree.children[i];
+ if (child.path === path)
+ {
+ directoryTree.children.splice(i,1)
+ return true;
+ }
+ if (DirectoryTree.remove_(child,path))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ remove(path: string): boolean
+ {
+ return DirectoryTree.remove_(this,path);
+ }
+ find(path: string): DirectoryTree | null {
+ let currentNode: DirectoryTree = this;
+ for (var segment of path.split('/')) {
+ let found = false;
+ for (var child of currentNode.children) {
+ if (child.name === segment) {
+ found = true;
+ currentNode = child;
+ break;
+ }
+ }
+ if (!found) {
+ return null;
+ }
+ }
+ return currentNode;
+
+ }
+}
+
+export interface FilePropertyDirectorySelectDialogProps {
+ open: boolean,
+ uiFileProperty: UiFileProperty;
+ defaultPath: string,
+ excludeDirectory: string,
+ onOk: (path: string) => void,
+ onClose: () => void
+};
+
+export interface FilePropertyDirectorySelectDialogState {
+ fullScreen: boolean;
+ selectedPath: string;
+ directoryTree?: DirectoryTree;
+ hasSelection: boolean;
+ directoryTreeInvalidatecount: number;
+
+};
+
+export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveComponent {
+
+ private model: PiPedalModel;
+ private refSelected: React.Ref;
+
+ constructor(props: FilePropertyDirectorySelectDialogProps) {
+ super(props);
+ this.model = PiPedalModelFactory.getInstance();
+ this.state = {
+ fullScreen: false,
+ selectedPath: props.defaultPath,
+ directoryTree: undefined,
+ hasSelection: false,
+ directoryTreeInvalidatecount: 0
+
+ };
+ this.refSelected = React.createRef();
+
+ this.requestDirectoryTree();
+ }
+ mounted: boolean = false;
+
+
+
+ onWindowSizeChanged(width: number, height: number): void {
+ this.setState({ fullScreen: height < 200 })
+ }
+
+
+ componentDidMount() {
+ super.componentDidMount();
+ this.mounted = true;
+ }
+ componentWillUnmount() {
+ super.componentWillUnmount();
+ this.mounted = false;
+ }
+
+ private requestScroll = true;
+ requestDirectoryTree() {
+ if (!this.props.open) return;
+
+ this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty)
+ .then((filePropertyDirectoryTree) => {
+ let myTree = new DirectoryTree(filePropertyDirectoryTree);
+ if (this.props.excludeDirectory)
+ {
+ myTree.remove(this.props.excludeDirectory);
+ }
+ myTree.expand(this.state.selectedPath);
+
+ let hasSelection = myTree.find(this.state.selectedPath) != null;
+ this.setState({ directoryTree: myTree, hasSelection: hasSelection });
+ this.requestScroll = true;
+ })
+ .catch((e) => {
+ this.model.showAlert(e.toString());
+ this.setState({ hasSelection: false });
+ });
+
+ }
+
+ componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void {
+ super.componentDidUpdate?.(prevProps, prevState, snapshot);
+
+ if (prevProps.open !== this.props.open) {
+ if (this.props.open) {
+ this.setState({ selectedPath: this.props.defaultPath, hasSelection: false });
+ this.requestDirectoryTree();
+ this.requestScroll = true;
+ }
+ }
+ }
+
+
+ onOK() {
+ if (this.state.hasSelection) {
+ this.props.onOk(this.state.selectedPath);
+ }
+ }
+ onClose() {
+ this.props.onClose();
+ }
+ private onToggleExpand(directoryTree: DirectoryTree)
+ {
+ directoryTree.expanded = !directoryTree.expanded;
+ this.setState({directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount+1});
+ }
+ private onTreeClick(directoryTree: DirectoryTree) {
+ if (!this.state.directoryTree) return;
+ this.state.directoryTree.expand(directoryTree.path);
+ this.setState({ selectedPath: directoryTree.path, hasSelection: true, directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 });
+ }
+ private renderTree_(directoryTree: DirectoryTree) {
+ let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined;
+ let selected = directoryTree.path === this.state.selectedPath;
+ if (selected) {
+ ref = (element) => {
+ if (this.requestScroll) {
+ this.requestScroll = false;
+ }
+
+ }
+ }
+ let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
+ if (isDarkMode()) {
+ selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
+ }
+ let showToggle = directoryTree.canExpand() && directoryTree.path.length !== 0;
+ return (
+
+
+
{ this.onToggleExpand(directoryTree); }}
+ style={{opacity: showToggle ? 1: 0}}
+ disabled={!showToggle}
+ >
+ {
+ directoryTree.expanded &&
+ (
+
+ )
+ }
+ {
+ !directoryTree.expanded &&
+ (
+
+ )
+ }
+
+
{ this.onTreeClick(directoryTree); }}>
+
+
+ {
+ directoryTree.path === "" ? (
+
+ ) : (
+
+ )
+ }
+
+ {directoryTree.name === "" ? "Home" : directoryTree.name}
+
+
+
+ {directoryTree.expanded
+ && directoryTree.canExpand()
+ && (
+
+ {
+ directoryTree.children.map(
+ (child, index) => {
+ return this.renderTree_(child);
+ }
+ )
+ }
+
+ )
+ }
+
+ );
+ }
+ private renderTree() {
+ if (!this.state.directoryTree) { return undefined; }
+ return this.renderTree_(this.state.directoryTree);
+ }
+ render() {
+
+
+ return (
+ { this.onClose(); }} aria-labelledby="Rename-dialog-title"
+ fullScreen={this.state.fullScreen}
+ style={{ userSelect: "none" }}
+ PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }}
+ >
+
+ Select folder
+
+
+
+
+ {
+ this.renderTree()
+ }
+
+
+
+
+ { this.onClose(); }} color="primary">
+ Cancel
+
+ { this.onOK(); }} color="secondary" disabled={!this.state.hasSelection} >
+ OK
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/react/src/FilePropertyDirectoryTree.tsx b/react/src/FilePropertyDirectoryTree.tsx
new file mode 100644
index 0000000..4a9191b
--- /dev/null
+++ b/react/src/FilePropertyDirectoryTree.tsx
@@ -0,0 +1,39 @@
+// 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.
+
+
+export default class FilePropertyDirectoryTree {
+ deserialize(input: any) : FilePropertyDirectoryTree {
+ this.directoryName = input.directoryName;
+ this.children = FilePropertyDirectoryTree.deserialize_array(input.children);
+ return this;
+ }
+ static deserialize_array(input: any): FilePropertyDirectoryTree[] {
+ let result: FilePropertyDirectoryTree[] = [];
+ for (let i = 0; i < input.length; ++i)
+ {
+ result[i] = new FilePropertyDirectoryTree().deserialize(input[i]);
+ }
+ return result;
+ }
+
+
+ directoryName: string = "";
+ children: FilePropertyDirectoryTree[] = [];
+}
\ No newline at end of file
diff --git a/react/src/JackServerSettingsDialog.tsx b/react/src/JackServerSettingsDialog.tsx
index 34b3fec..38c811b 100644
--- a/react/src/JackServerSettingsDialog.tsx
+++ b/react/src/JackServerSettingsDialog.tsx
@@ -362,10 +362,10 @@ const JackServerSettingsDialog = withStyles(styles)(
-
+
Cancel
- this.handleApply()} color="primary" disabled={
+ this.handleApply()} disabled={
(!this.state.alsaDevices) || !this.state.jackServerSettings.valid}>
OK
diff --git a/react/src/ListSelectDialog.tsx b/react/src/ListSelectDialog.tsx
index 0124104..03f145d 100644
--- a/react/src/ListSelectDialog.tsx
+++ b/react/src/ListSelectDialog.tsx
@@ -106,7 +106,7 @@ export default class ListSelectDialog extends ResizeResponsiveComponent
- this.props.onClose()} color="primary">
+ this.props.onClose()} color="primary">
Cancel
diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx
index 5c7c93b..fea41e4 100644
--- a/react/src/LoadPluginDialog.tsx
+++ b/react/src/LoadPluginDialog.tsx
@@ -814,8 +814,8 @@ export const LoadPluginDialog =