diff --git a/src/main/java/sh/ball/gui/controller/GeneralController.java b/src/main/java/sh/ball/gui/controller/GeneralController.java index 91e247b2..0e2790a7 100644 --- a/src/main/java/sh/ball/gui/controller/GeneralController.java +++ b/src/main/java/sh/ball/gui/controller/GeneralController.java @@ -3,9 +3,11 @@ package sh.ball.gui.controller; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; +import javafx.scene.input.KeyCode; import javafx.scene.shape.SVGPath; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; @@ -19,10 +21,12 @@ import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.*; import static sh.ball.gui.Gui.audioPlayer; +import static sh.ball.gui.Gui.main; public class GeneralController implements Initializable, SubController { @@ -61,6 +65,14 @@ public class GeneralController implements Initializable, SubController { private Slider micVolumeSlider; @FXML private SVGPath micVolumeMidi; + @FXML + private TextField createFileTextField; + @FXML + private ComboBox createFileComboBox; + @FXML + private Button createFileButton; + @FXML + private Button deleteFileButton; public void setRecordResult(String result) { recordLabel.setText(result); @@ -216,6 +228,38 @@ public class GeneralController implements Initializable, SubController { }); editFileButton.setOnAction(e -> mainController.openCodeEditor()); + deleteFileButton.setOnAction(e -> mainController.deleteCurrentFile()); + + createFileComboBox.setItems(FXCollections.observableList(List.of(".lua", ".svg", ".obj", ".txt"))); + createFileComboBox.setValue(".lua"); + + try { + Map defaultFileMap = Map.of( + ".lua", getClass().getResourceAsStream("/lua/demo.lua").readAllBytes(), + ".svg", getClass().getResourceAsStream("/svg/demo.svg").readAllBytes(), + ".obj", getClass().getResourceAsStream("/models/cube.obj").readAllBytes(), + ".txt", "hello".getBytes(StandardCharsets.UTF_8) + ); + + createFileButton.setOnAction(e -> { + try { + String fileName = createFileTextField.getText() + createFileComboBox.getValue(); + byte[] fileData = defaultFileMap.get(createFileComboBox.getValue()); + mainController.createFile(fileName, fileData); + createFileTextField.setText(""); + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + + createFileTextField.setOnKeyPressed(event -> { + if (event.getCode() == KeyCode.ENTER) { + createFileButton.getOnAction().handle(null); + } + }); } @Override @@ -233,7 +277,7 @@ public class GeneralController implements Initializable, SubController { public void updateFrequency(double leftFrequency, double rightFrequency) { Platform.runLater(() -> - frequencyLabel.setText(String.format("L/R Frequency:\n%d Hz / %d Hz", Math.round(leftFrequency), Math.round(rightFrequency))) + frequencyLabel.setText(String.format("L/R Frequency: %5d Hz\t / %5d Hz", Math.round(leftFrequency), Math.round(rightFrequency))) ); } diff --git a/src/main/java/sh/ball/gui/controller/MainController.java b/src/main/java/sh/ball/gui/controller/MainController.java index f9457388..91ae5576 100644 --- a/src/main/java/sh/ball/gui/controller/MainController.java +++ b/src/main/java/sh/ball/gui/controller/MainController.java @@ -53,6 +53,7 @@ import sh.ball.engine.ObjectServer; import sh.ball.engine.ObjectSet; import sh.ball.gui.Gui; import sh.ball.oscilloscope.ByteWebSocketServer; +import sh.ball.parser.FileParser; import sh.ball.parser.lua.LuaParser; import sh.ball.parser.obj.ObjFrameSettings; import sh.ball.parser.obj.ObjParser; @@ -96,11 +97,10 @@ public class MainController implements Initializable, FrequencyListener, MidiLis // frames private static final InputStream DEFAULT_OBJ = MainController.class.getResourceAsStream("/models/cube.obj"); private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private final LuaParser luaParser = new LuaParser(); private final Set unsavedFileNames = new LinkedHashSet<>(); private List openFiles = new ArrayList<>(); private List frameSourcePaths = new ArrayList<>(); - private List> sampleSources = new ArrayList<>(); + private List>> sampleParsers = new ArrayList<>(); private List>> frameSources = new ArrayList<>(); private FrameProducer> producer; private int currentFrameSource; @@ -190,7 +190,7 @@ public class MainController implements Initializable, FrequencyListener, MidiLis openFiles.add(baos.toByteArray()); frameSources.add(frames); - sampleSources.add(null); + sampleParsers.add(null); frameSourcePaths.add("cube.obj"); currentFrameSource = 0; this.producer = new FrameProducer<>(audioPlayer, frames); @@ -518,7 +518,11 @@ public class MainController implements Initializable, FrequencyListener, MidiLis } public void setLuaVariable(String variableName, Object value) { - luaParser.setVariable(variableName, value); + sampleParsers.forEach(sampleParser -> { + if (sampleParser instanceof LuaParser luaParser) { + luaParser.setVariable(variableName, value); + } + }); } public void setSoftwareOscilloscopeAction(Runnable openBrowser) { @@ -620,17 +624,42 @@ public class MainController implements Initializable, FrequencyListener, MidiLis new Thread(analyser).start(); } + private void disableSources() { + frameSources.stream().filter(Objects::nonNull).forEach(FrameSource::disable); + sampleParsers.stream().filter(Objects::nonNull).forEach(parser -> { + try { + parser.get().disable(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + // changes the FrameProducer e.g. could be changing from a 3D object to an // SVG. The old FrameProducer is stopped and a new one created and initialised // with the same settings that the original had. private void changeFrameSource(int index) { + if (frameSources.size() == 0) { + generalController.setFrameSourceName("No file open!"); + generalController.updateFrameLabels(); + audioPlayer.removeSampleSource(); + audioPlayer.addFrame(List.of(new Vector2())); + return; + } index = Math.max(0, Math.min(index, frameSources.size() - 1)); currentFrameSource = index; - frameSources.stream().filter(Objects::nonNull).forEach(FrameSource::disable); - sampleSources.stream().filter(Objects::nonNull).forEach(FrameSource::disable); + disableSources(); FrameSource> frames = frameSources.get(index); - FrameSource samples = sampleSources.get(index); + FrameSource samples = null; + try { + FileParser> sampleParser = sampleParsers.get(index); + if (sampleParser != null ) { + samples = sampleParser.get(); + } + } catch (Exception e) { + e.printStackTrace(); + } if (frames != null) { frames.enable(); audioPlayer.removeSampleSource(); @@ -676,6 +705,56 @@ public class MainController implements Initializable, FrequencyListener, MidiLis }); } + private void createFile(String name, byte[] fileData, List>> sampleParsers, List>> frameSources, List frameSourcePaths, List openFiles) throws Exception { + if (frameSourcePaths.contains(name)) { + throw new IOException("File already exists with this name"); + } + if (LuaParser.isLuaFile(name)) { + LuaParser luaParser = new LuaParser(); + luaParser.setScriptFromInputStream(new ByteArrayInputStream(fileData)); + luaParser.parse(); + sampleParsers.add(luaParser); + luaController.updateLuaVariables(); + frameSources.add(null); + } else { + frameSources.add(ParserFactory.getParser(name, fileData).parse()); + sampleParsers.add(null); + } + frameSourcePaths.add(name); + openFiles.add(fileData); + Platform.runLater(() -> { + generalController.showMultiFileTooltip(frameSources.size() > 1); + if (generalController.framesPlaying() && frameSources.size() == 1) { + generalController.disablePlayback(); + } + }); + } + + public void createFile(String name, byte[] fileData) throws Exception { + createFile(name, fileData, sampleParsers, frameSources, frameSourcePaths, openFiles); + changeFrameSource(frameSources.size() - 1); + unsavedFileNames.add(name); + setUnsavedFileWarning(); + } + + public void deleteCurrentFile() { + if (frameSources.isEmpty()) { + return; + } + disableSources(); + sampleParsers.remove(currentFrameSource); + frameSources.remove(currentFrameSource); + String name = frameSourcePaths.get(currentFrameSource); + unsavedFileNames.remove(name); + setUnsavedFileWarning(); + frameSourcePaths.remove(currentFrameSource); + openFiles.remove(currentFrameSource); + if (currentFrameSource >= frameSources.size()) { + currentFrameSource--; + } + changeFrameSource(currentFrameSource); + } + void updateFileData(byte[] file, String name) { int index = frameSourcePaths.indexOf(name); if (index == -1) { @@ -685,16 +764,18 @@ public class MainController implements Initializable, FrequencyListener, MidiLis openFiles.set(index, file); try { if (LuaParser.isLuaFile(name)) { - luaParser.setScriptFromInputStream(new ByteArrayInputStream(file)); - FrameSource sampleSource = sampleSources.get(index); - sampleSources.set(index, luaParser.parse()); - sampleSource.disable(); + FileParser> sampleParser = sampleParsers.get(index); + if (sampleParser instanceof LuaParser luaParser) { + luaParser.setScriptFromInputStream(new ByteArrayInputStream(file)); + } + sampleParser.parse().disable(); + sampleParsers.set(index, sampleParser); frameSources.set(index, null); } else { FrameSource> frameSource = frameSources.get(index); frameSources.set(index, ParserFactory.getParser(name, file).parse()); frameSource.disable(); - sampleSources.set(index, null); + sampleParsers.set(index, null); } unsavedFileNames.add(name); @@ -711,7 +792,7 @@ public class MainController implements Initializable, FrequencyListener, MidiLis void setUnsavedFileWarning() { Platform.runLater(() -> { if (!unsavedFileNames.isEmpty()) { - String warning = "(unsaved file changes: " + String.join(", ", unsavedFileNames) + ")"; + String warning = "(unsaved files: " + String.join(", ", unsavedFileNames) + ")"; updateTitle(warning, openProjectPath); } else { updateTitle(null, openProjectPath); @@ -720,7 +801,7 @@ public class MainController implements Initializable, FrequencyListener, MidiLis } void updateFiles(List files, List names, int startingFrameSource) throws Exception { - List> newSampleSources = new ArrayList<>(); + List>> newSampleParsers = new ArrayList<>(); List>> newFrameSources = new ArrayList<>(); List newFrameSourcePaths = new ArrayList<>(); List newOpenFiles = new ArrayList<>(); @@ -735,17 +816,8 @@ public class MainController implements Initializable, FrequencyListener, MidiLis for (int i = 0; i < files.size(); i++) { try { - if (LuaParser.isLuaFile(names.get(i))) { - luaParser.setScriptFromInputStream(new ByteArrayInputStream(files.get(i))); - newSampleSources.add(luaParser.parse()); - newFrameSources.add(null); - } else { - newFrameSources.add(ParserFactory.getParser(names.get(i), files.get(i)).parse()); - newSampleSources.add(null); - } - newFrameSourcePaths.add(names.get(i)); - newOpenFiles.add(files.get(i)); - } catch (IOException | IllegalArgumentException e) { + createFile(names.get(i), files.get(i), newSampleParsers, newFrameSources, newFrameSourcePaths, newOpenFiles); + } catch (Exception e) { Platform.runLater(() -> { generalController.setFrameSourceName(e.getMessage()); generalController.updateFrameLabels(); @@ -753,21 +825,14 @@ public class MainController implements Initializable, FrequencyListener, MidiLis } } - Platform.runLater(() -> { - if (newFrameSources.size() > 0) { - generalController.showMultiFileTooltip(newFrameSources.size() > 1); - if (generalController.framesPlaying() && newFrameSources.size() == 1) { - generalController.disablePlayback(); - } - frameSources.stream().filter(Objects::nonNull).forEach(FrameSource::disable); - sampleSources.stream().filter(Objects::nonNull).forEach(FrameSource::disable); - sampleSources = newSampleSources; - frameSources = newFrameSources; - frameSourcePaths = newFrameSourcePaths; - openFiles = newOpenFiles; - changeFrameSource(startingFrameSource); - } - }); + if (newFrameSources.size() > 0) { + disableSources(); + sampleParsers = newSampleParsers; + frameSources = newFrameSources; + frameSourcePaths = newFrameSourcePaths; + openFiles = newOpenFiles; + changeFrameSource(startingFrameSource); + } } // used so that the Controller has access to the stage, allowing it to open @@ -1218,6 +1283,8 @@ public class MainController implements Initializable, FrequencyListener, MidiLis updateFiles(files, fileNames, 0); } + luaController.updateLuaVariables(); + openProjectPath = projectFileName; updateTitle(null, projectFileName); } catch (Exception e) { @@ -1297,7 +1364,11 @@ public class MainController implements Initializable, FrequencyListener, MidiLis } public void resetLuaStep() { - luaParser.resetStep(); + sampleParsers.forEach(sampleParser -> { + if (sampleParser instanceof LuaParser luaParser) { + luaParser.resetStep(); + } + }); } private record PrintableSlider(Slider slider) { diff --git a/src/main/java/sh/ball/parser/FileParser.java b/src/main/java/sh/ball/parser/FileParser.java index 316d20e1..bc2b5fa8 100644 --- a/src/main/java/sh/ball/parser/FileParser.java +++ b/src/main/java/sh/ball/parser/FileParser.java @@ -1,10 +1,10 @@ package sh.ball.parser; -import java.io.IOException; -import javax.xml.parsers.ParserConfigurationException; - import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; + public abstract class FileParser { public abstract String getFileExtension(); @@ -21,4 +21,6 @@ public abstract class FileParser { } public abstract T parse() throws Exception; + + public abstract T get() throws Exception; } diff --git a/src/main/java/sh/ball/parser/lua/LuaParser.java b/src/main/java/sh/ball/parser/lua/LuaParser.java index e4e92f28..0724de89 100644 --- a/src/main/java/sh/ball/parser/lua/LuaParser.java +++ b/src/main/java/sh/ball/parser/lua/LuaParser.java @@ -35,6 +35,11 @@ public class LuaParser extends FileParser> { return sampleSource; } + @Override + public FrameSource get() { + return sampleSource; + } + public static boolean isLuaFile(String path) { return path.matches(".*\\.lua"); } diff --git a/src/main/java/sh/ball/parser/obj/ObjParser.java b/src/main/java/sh/ball/parser/obj/ObjParser.java index 55a8fad4..9aef250b 100644 --- a/src/main/java/sh/ball/parser/obj/ObjParser.java +++ b/src/main/java/sh/ball/parser/obj/ObjParser.java @@ -55,6 +55,11 @@ public class ObjParser extends FileParser>> { return new ObjFrameSource(object, camera); } + @Override + public FrameSource> get() throws IOException { + return parse(); + } + // If camera position arguments haven't been specified, automatically work out the position of // the camera based on the size of the object in the camera's view. public void setFocalLength(double focalLength) { diff --git a/src/main/java/sh/ball/parser/svg/SvgParser.java b/src/main/java/sh/ball/parser/svg/SvgParser.java index cb42295f..87672ee4 100644 --- a/src/main/java/sh/ball/parser/svg/SvgParser.java +++ b/src/main/java/sh/ball/parser/svg/SvgParser.java @@ -190,6 +190,11 @@ public class SvgParser extends FileParser>> { } } + @Override + public FrameSource> get() throws IOException, ParserConfigurationException, SAXException { + return parse(); + } + /* Given a character, will return the glyph associated with it. * Assumes that the .svg loaded has character glyphs. */ public List parseGlyphsWithUnicode(char unicode) { diff --git a/src/main/java/sh/ball/parser/txt/TextParser.java b/src/main/java/sh/ball/parser/txt/TextParser.java index 4a151b64..e832fb8b 100644 --- a/src/main/java/sh/ball/parser/txt/TextParser.java +++ b/src/main/java/sh/ball/parser/txt/TextParser.java @@ -83,6 +83,11 @@ public class TextParser extends FileParser>> { return new ShapeFrameSource(Shape.flip(Shape.normalize(shapes))); } + @Override + public FrameSource> get() throws Exception { + return parse(); + } + public static boolean isTxtFile(String path) { return path.matches(".*\\.txt"); } diff --git a/src/main/resources/fxml/general.fxml b/src/main/resources/fxml/general.fxml index 2a0a9f4a..0474ca38 100644 --- a/src/main/resources/fxml/general.fxml +++ b/src/main/resources/fxml/general.fxml @@ -1,25 +1,31 @@ + + - + -