diff --git a/src/main/java/jace/Emulator.java b/src/main/java/jace/Emulator.java index c34a18b..10c5587 100644 --- a/src/main/java/jace/Emulator.java +++ b/src/main/java/jace/Emulator.java @@ -18,8 +18,8 @@ */ package jace; -import jace.apple2e.Apple2e; import jace.config.Configuration; +import jace.apple2e.Apple2e; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/src/main/java/jace/EmulatorUILogic.java b/src/main/java/jace/EmulatorUILogic.java index a5d9710..543eef2 100644 --- a/src/main/java/jace/EmulatorUILogic.java +++ b/src/main/java/jace/EmulatorUILogic.java @@ -18,10 +18,10 @@ */ package jace; -import com.sun.javafx.tk.quantum.OverlayWarning; import jace.apple2e.MOS65C02; import jace.apple2e.RAM128k; import jace.apple2e.SoftSwitches; +import jace.config.ConfigurableField; import jace.config.ConfigurationUIController; import jace.config.InvokableAction; import jace.config.Reconfigurable; @@ -82,6 +82,12 @@ public class EmulatorUILogic implements Reconfigurable { }; } + @ConfigurableField( + category = "General", + name = "Show Drives" + ) + public boolean showDrives = false; + public static void updateCPURegisters(MOS65C02 cpu) { // DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel(); // debuggerPanel.valueA.setText(Integer.toHexString(cpu.A)); @@ -246,7 +252,7 @@ public class EmulatorUILogic implements Reconfigurable { name = "Toggle Debug", category = "debug", description = "Show/hide the debug panel", - alternatives = "Show Debug;Hide Debug", + alternatives = "Show Debug;Hide Debug;Inspect", defaultKeyMapping = "ctrl+shift+d") public static void toggleDebugPanel() { // AbstractEmulatorFrame frame = Emulator.getFrame(); @@ -262,13 +268,14 @@ public class EmulatorUILogic implements Reconfigurable { name = "Toggle fullscreen", category = "general", description = "Activate/deactivate fullscreen mode", - alternatives = "fullscreen,maximize", + alternatives = "fullscreen;maximize", defaultKeyMapping = "ctrl+shift+f") public static void toggleFullscreen() { Platform.runLater(() -> { Stage stage = JaceApplication.getApplication().primaryStage; stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH); stage.setFullScreen(!stage.isFullScreen()); + JaceApplication.getApplication().controller.setAspectRatioEnabled(stage.isFullScreen()); }); } @@ -276,7 +283,7 @@ public class EmulatorUILogic implements Reconfigurable { name = "Save Raw Screenshot", category = "general", description = "Save raw (RAM) format of visible screen", - alternatives = "screendump, raw screenshot", + alternatives = "screendump;raw screenshot", defaultKeyMapping = "ctrl+shift+z") public static void saveScreenshotRaw() throws FileNotFoundException, IOException { SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); @@ -317,7 +324,7 @@ public class EmulatorUILogic implements Reconfigurable { name = "Save Screenshot", category = "general", description = "Save image of visible screen", - alternatives = "Save image,save framebuffer,screenshot", + alternatives = "Save image;save framebuffer;screenshot", defaultKeyMapping = "ctrl+shift+s") public static void saveScreenshot() throws IOException { FileChooser select = new FileChooser(); @@ -346,7 +353,7 @@ public class EmulatorUILogic implements Reconfigurable { name = "Configuration", category = "general", description = "Edit emulator configuraion", - alternatives = "Reconfigure,Preferences,Settings", + alternatives = "Reconfigure;Preferences;Settings;Config", defaultKeyMapping = {"f4", "ctrl+shift+c"}) public static void showConfig() { FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/Configuration.fxml")); @@ -368,7 +375,7 @@ public class EmulatorUILogic implements Reconfigurable { name = "Open IDE", category = "development", description = "Open new IDE window for Basic/Assembly/Plasma coding", - alternatives = "dev,development,acme,assembler,editor", + alternatives = "IDE;dev;development;acme;assembler;editor", defaultKeyMapping = {"ctrl+shift+i"}) public static void showIDE() { FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/editor.fxml")); @@ -392,44 +399,61 @@ public class EmulatorUILogic implements Reconfigurable { name = "Resize window", category = "general", description = "Resize the screen to 1x/1.5x/2x/3x video size", - alternatives = "Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;", + alternatives = "Aspect;Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;", defaultKeyMapping = {"ctrl+shift+a"}) public static void scaleIntegerRatio() { Platform.runLater(() -> { - JaceApplication.getApplication().primaryStage.setFullScreen(false); + if (JaceApplication.getApplication() == null + || JaceApplication.getApplication().primaryStage == null) { + return; + } + Stage stage = JaceApplication.getApplication().primaryStage; size++; if (size > 3) { size = 0; } - int width = 0, height = 0; - switch (size) { - case 0: // 1x - width = 560; - height = 384; - break; - case 1: // 1.5x - width = 840; - height = 576; - break; - case 2: // 2x - width = 560*2; - height = 384*2; - break; - case 3: // 3x (retina) 2880x1800 - width = 560*3; - height = 384*3; - break; - default: // 2x - width = 560*2; - height = 384*2; + if (stage.isFullScreen()) { + JaceApplication.getApplication().controller.toggleAspectRatio(); + } else { + int width = 0, height = 0; + switch (size) { + case 0: // 1x + width = 560; + height = 384; + break; + case 1: // 1.5x + width = 840; + height = 576; + break; + case 2: // 2x + width = 560 * 2; + height = 384 * 2; + break; + case 3: // 3x (retina) 2880x1800 + width = 560 * 3; + height = 384 * 3; + break; + default: // 2x + width = 560 * 2; + height = 384 * 2; + } + double vgap = stage.getScene().getY(); + double hgap = stage.getScene().getX(); + stage.setWidth(hgap * 2 + width); + stage.setHeight(vgap + height); } - Stage stage = JaceApplication.getApplication().primaryStage; - double vgap = stage.getScene().getY(); - double hgap = stage.getScene().getX(); - stage.setWidth(hgap*2 + width); - stage.setHeight(vgap + height); }); } + + @InvokableAction( + name = "About", + category = "general", + description = "Display about window", + alternatives = "info;credits", + defaultKeyMapping = {"ctrl+shift+."}) + public static void showAboutWindow() { + //TODO: Implement + } public static boolean confirm(String message) { // return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message); diff --git a/src/main/java/jace/JaceApplication.java b/src/main/java/jace/JaceApplication.java index f8703c2..c74097a 100644 --- a/src/main/java/jace/JaceApplication.java +++ b/src/main/java/jace/JaceApplication.java @@ -54,14 +54,15 @@ public class JaceApplication extends Application { } primaryStage.show(); - Emulator emulator = new Emulator(getParameters().getRaw()); - javafx.application.Platform.runLater(() -> { + new Thread(() -> { + new Emulator(getParameters().getRaw()); + reconnectUIHooks(); + EmulatorUILogic.scaleIntegerRatio(); while (Emulator.computer.getVideo() == null || Emulator.computer.getVideo().getFrameBuffer() == null) { Thread.yield(); } - controller.connectComputer(Emulator.computer, primaryStage); bootWatchdog(); - }); + }).start(); primaryStage.setOnCloseRequest(event -> { Emulator.computer.deactivate(); Platform.exit(); @@ -69,6 +70,10 @@ public class JaceApplication extends Application { }); } + public void reconnectUIHooks() { + controller.connectComputer(Emulator.computer, primaryStage); + } + public static JaceApplication getApplication() { return singleton; } diff --git a/src/main/java/jace/JaceUIController.java b/src/main/java/jace/JaceUIController.java index 7b5c3c1..c10243b 100644 --- a/src/main/java/jace/JaceUIController.java +++ b/src/main/java/jace/JaceUIController.java @@ -6,10 +6,10 @@ package jace; import com.sun.glass.ui.Application; -import jace.cheat.MetaCheat; import jace.core.Card; import jace.core.Computer; -import jace.core.Keyboard; +import jace.core.Motherboard; +import jace.core.Utility; import jace.library.MediaCache; import jace.library.MediaConsumer; import jace.library.MediaConsumerParent; @@ -30,10 +30,22 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import javafx.animation.FadeTransition; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.binding.NumberBinding; +import javafx.beans.binding.When; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.Slider; import javafx.scene.effect.DropShadow; import javafx.scene.image.ImageView; import javafx.scene.input.DragEvent; @@ -43,11 +55,14 @@ import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.Stage; +import javafx.util.Duration; +import javafx.util.StringConverter; /** * @@ -70,29 +85,197 @@ public class JaceUIController { @FXML private ImageView appleScreen; + @FXML + private BorderPane controlOverlay; + + @FXML + private Slider speedSlider; + + @FXML + private AnchorPane menuButtonPane; + + @FXML + private Button menuButton; + Computer computer; + private final BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false); + @FXML void initialize() { assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'JaceUI.fxml'."; assert stackPane != null : "fx:id=\"stackPane\" was not injected: check your FXML file 'JaceUI.fxml'."; assert notificationBox != null : "fx:id=\"notificationBox\" was not injected: check your FXML file 'JaceUI.fxml'."; assert appleScreen != null : "fx:id=\"appleScreen\" was not injected: check your FXML file 'JaceUI.fxml'."; - appleScreen.fitWidthProperty().bind(rootPane.widthProperty()); + controlOverlay.setVisible(false); + menuButtonPane.setVisible(false); + NumberBinding aspectCorrectedWidth = rootPane.heightProperty().multiply(3.0).divide(2.0); + NumberBinding width = new When( + aspectRatioCorrectionEnabled.and(aspectCorrectedWidth.lessThan(rootPane.widthProperty())) + ).then(aspectCorrectedWidth).otherwise(rootPane.widthProperty()); + appleScreen.fitWidthProperty().bind(width); appleScreen.fitHeightProperty().bind(rootPane.heightProperty()); + appleScreen.setVisible(false); rootPane.setOnDragEntered(this::processDragEnteredEvent); rootPane.setOnDragExited(this::processDragExitedEvent); + rootPane.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null))); + rootPane.setOnMouseMoved(this::showMenuButton); + rootPane.setOnMouseExited(this::hideControlOverlay); + menuButton.setOnMouseClicked(this::showControlOverlay); + controlOverlay.setOnMouseClicked(this::hideControlOverlay); + delayTimer.getKeyFrames().add(new KeyFrame(Duration.millis(3000), evt -> { + hideControlOverlay(null); + })); + } + + private void showMenuButton(MouseEvent evt) { + if (!evt.isPrimaryButtonDown() && !evt.isSecondaryButtonDown() && !controlOverlay.isVisible()) { + resetMenuButtonTimer(); + if (!menuButtonPane.isVisible()) { + menuButtonPane.setVisible(true); + FadeTransition ft = new FadeTransition(Duration.millis(500), menuButtonPane); + ft.setFromValue(0.0); + ft.setToValue(1.0); + ft.play(); + } + } + } + + Timeline delayTimer = new Timeline(); + private void resetMenuButtonTimer() { + delayTimer.playFromStart(); + } + + private void showControlOverlay(MouseEvent evt) { + if (!evt.isPrimaryButtonDown() && !evt.isSecondaryButtonDown()) { + delayTimer.stop(); + menuButtonPane.setVisible(false); + controlOverlay.setVisible(true); + FadeTransition ft = new FadeTransition(Duration.millis(500), controlOverlay); + ft.setFromValue(0.0); + ft.setToValue(1.0); + ft.play(); + } + } + + private void hideControlOverlay(MouseEvent evt) { + if (menuButtonPane.isVisible()) { + FadeTransition ft1 = new FadeTransition(Duration.millis(500), menuButtonPane); + ft1.setFromValue(1.0); + ft1.setToValue(0.0); + ft1.setOnFinished(evt1 -> menuButtonPane.setVisible(false)); + ft1.play(); + } + if (controlOverlay.isVisible()) { + FadeTransition ft2 = new FadeTransition(Duration.millis(500), controlOverlay); + ft2.setFromValue(1.0); + ft2.setToValue(0.0); + ft2.setOnFinished(evt1 -> controlOverlay.setVisible(false)); + ft2.play(); + } + } + + private double convertSpeedToRatio(Double setting) { + if (setting < 1.0) { + return 0.5; + } else if (setting == 1.0) { + return 1.0; + } else if (setting >= 10) { + return Double.MAX_VALUE; + } else { + double val = Math.pow(2.0, (setting - 1.0) / 1.5); + val = Math.floor(val * 2.0) / 2.0; + if (val > 2.0) { + val = Math.floor(val); + } + return val; + } + } + + private void connectControls(Stage primaryStage) { + connectButtons(controlOverlay); + if (computer.getKeyboard() != null) { + EventHandler keyboardHandler = computer.getKeyboard().getListener(); + primaryStage.setOnShowing(evt -> computer.getKeyboard().resetState()); + rootPane.setOnKeyPressed(keyboardHandler); + rootPane.setOnKeyReleased(keyboardHandler); + rootPane.setFocusTraversable(true); + } + speedSlider.setValue(1.0); + speedSlider.setMinorTickCount(0); + speedSlider.setMajorTickUnit(1); + speedSlider.setLabelFormatter(new StringConverter() { + @Override + public String toString(Double val) { + if (val < 1.0) { + return "Half"; + } else if (val >= 10.0) { + return "∞"; + } + double v = convertSpeedToRatio(val); + if (v != Math.floor(v)) { + return String.valueOf(v) + "x"; + } else { + return String.valueOf((int) v) + "x"; + } + } + + @Override + public Double fromString(String string) { + return 1.0; + } + }); + speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue())); + } + + private void connectButtons(Node n) { + if (n instanceof Button) { + Button button = (Button) n; + Runnable action = Utility.getNamedInvokableAction(button.getText()); + button.setOnMouseClicked(evt -> action.run()); + } else if (n instanceof Parent) { + for (Node child : ((Parent) n).getChildrenUnmodifiable()) { + connectButtons(child); + } + } + } + + private void setSpeed(double speed) { + double speedRatio = convertSpeedToRatio(speed); + if (speedRatio > 100.0) { + Emulator.computer.getMotherboard().maxspeed = true; + Motherboard.cpuPerClock = 3; + } else { + if (speedRatio > 25) { + Motherboard.cpuPerClock = 2; + } else { + Motherboard.cpuPerClock = 1; + } + Emulator.computer.getMotherboard().maxspeed = false; + Emulator.computer.getMotherboard().speedRatio = (int) (speedRatio * 100); + } + Emulator.computer.getMotherboard().reconfigure(); + } + + public void toggleAspectRatio() { + setAspectRatioEnabled(aspectRatioCorrectionEnabled.not().get()); + } + + public void setAspectRatioEnabled(boolean enabled) { + aspectRatioCorrectionEnabled.set(enabled); } public void connectComputer(Computer computer, Stage primaryStage) { + if (computer == null) { + return; + } this.computer = computer; - appleScreen.setImage(computer.getVideo().getFrameBuffer()); - EventHandler keyboardHandler = computer.getKeyboard().getListener(); - primaryStage.setOnShowing(evt -> computer.getKeyboard().resetState()); - rootPane.setFocusTraversable(true); - rootPane.setOnKeyPressed(keyboardHandler); - rootPane.setOnKeyReleased(keyboardHandler); - rootPane.requestFocus(); + Platform.runLater(() -> { + connectControls(primaryStage); + appleScreen.setImage(computer.getVideo().getFrameBuffer()); + appleScreen.setVisible(true); + rootPane.requestFocus(); + }); } private void processDragEnteredEvent(DragEvent evt) { @@ -245,24 +428,25 @@ public class JaceUIController { public void removeMouseListener(EventHandler handler) { appleScreen.removeEventHandler(MouseEvent.ANY, handler); } - + Label currentNotification = null; + public void displayNotification(String message) { Label oldNotification = currentNotification; Label notification = new Label(message); currentNotification = notification; notification.setEffect(new DropShadow(2.0, Color.BLACK)); notification.setTextFill(Color.WHITE); - notification.setBackground(new Background(new BackgroundFill(Color.rgb(0,0,80, 0.7), new CornerRadii(5.0), new Insets(-5.0)))); - Application.invokeLater(() -> { + notification.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 80, 0.7), new CornerRadii(5.0), new Insets(-5.0)))); + Application.invokeLater(() -> { stackPane.getChildren().remove(oldNotification); stackPane.getChildren().add(notification); }); - - notificationExecutor.schedule(()->{ - Application.invokeLater(() -> { + + notificationExecutor.schedule(() -> { + Application.invokeLater(() -> { stackPane.getChildren().remove(notification); - }); + }); }, 4, TimeUnit.SECONDS); } } diff --git a/src/main/java/jace/apple2e/Apple2e.java b/src/main/java/jace/apple2e/Apple2e.java index 3164b7b..95e6f4b 100644 --- a/src/main/java/jace/apple2e/Apple2e.java +++ b/src/main/java/jace/apple2e/Apple2e.java @@ -19,6 +19,8 @@ package jace.apple2e; import jace.Emulator; +import jace.JaceApplication; +import jace.apple2e.softswitch.VideoSoftSwitch; import jace.cheat.Cheats; import jace.config.ClassSelection; import jace.config.ConfigurableField; @@ -40,21 +42,22 @@ import jace.hardware.massStorage.CardMassStorage; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** - * Apple2e is a computer with a 65c02 CPU, 128k of bankswitched ram, - * double-hires graphics, and up to seven peripheral I/O cards installed. Pause - * and resume are implemented by the Motherboard class. This class provides - * overall configuration of the computer, but the actual operation of the - * computer and its timing characteristics are managed in the Motherboard class. + * Apple2e is a computer with a 65c02 CPU, 128k of bankswitched ram, double-hires graphics, and up to seven peripheral + * I/O cards installed. Pause and resume are implemented by the Motherboard class. This class provides overall + * configuration of the computer, but the actual operation of the computer and its timing characteristics are managed in + * the Motherboard class. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ @@ -121,7 +124,7 @@ public class Apple2e extends Computer { return "Computer (Apple //e)"; } - private void reinitMotherboard() { + protected void reinitMotherboard() { if (motherboard != null && motherboard.isRunning()) { motherboard.suspend(); } @@ -134,16 +137,25 @@ public class Apple2e extends Computer { public void coldStart() { pause(); reinitMotherboard(); + RAM128k ram = (RAM128k) getMemory(); + ram.initMemoryPattern(ram.mainMemory); + ram.initMemoryPattern(ram.getAuxMemory()); for (SoftSwitches s : SoftSwitches.values()) { s.getSwitch().reset(); } getMemory().configureActiveMemory(); getVideo().configureVideoMode(); - for (Optional c : getMemory().getAllCards()) { - c.ifPresent(Card::reset); + try { + for (Optional c : getMemory().getAllCards()) { + c.ifPresent(Card::reset); + waitForVBL(); + } + } catch (InterruptedException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } finally { + getCpu().resume(); + reboot(); } - reboot(); - resume(); } public void reboot() { @@ -158,7 +170,9 @@ public class Apple2e extends Computer { public void warmStart() { boolean restart = pause(); for (SoftSwitches s : SoftSwitches.values()) { - s.getSwitch().reset(); + if (!(s.getSwitch() instanceof VideoSoftSwitch)) { + s.getSwitch().reset(); + } } getMemory().configureActiveMemory(); getVideo().configureVideoMode(); @@ -194,11 +208,11 @@ public class Apple2e extends Computer { @Override public final void reconfigure() { boolean restart = pause(); - + if (Utility.isHeadlessMode()) { joy1enabled = false; joy2enabled = false; - + } super.reconfigure(); @@ -216,9 +230,7 @@ public class Apple2e extends Computer { if (getMemory() == null) { try { currentMemory = (RAM128k) ramCard.getValue().getConstructor(Computer.class).newInstance(this); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException ex) { - Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); - } catch (IllegalArgumentException | InvocationTargetException ex) { + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException ex) { Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); } try { @@ -291,6 +303,9 @@ public class Apple2e extends Computer { getVideo().configureVideoMode(); getVideo().reconfigure(); Emulator.resizeVideo(); + if (JaceApplication.getApplication() != null) { + JaceApplication.getApplication().reconnectUIHooks(); + } getVideo().resume(); } catch (InstantiationException | IllegalAccessException ex) { Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); @@ -377,6 +392,35 @@ public class Apple2e extends Computer { // } private List hints = new ArrayList<>(); + List vblCallbacks = Collections.synchronizedList(new ArrayList<>()); + + public void waitForVBL() throws InterruptedException { + waitForVBL(0); + } + + public void waitForVBL(int count) throws InterruptedException { + Semaphore s = new Semaphore(0); + onNextVBL(s::release); + s.acquire(); + if (count > 1) { + waitForVBL(count - 1); + } + } + + public void onNextVBL(Runnable r) { + vblCallbacks.add(r); + } + + @Override + public void notifyVBLStateChanged(boolean state) { + super.notifyVBLStateChanged(state); + if (state) { + while (vblCallbacks != null && !vblCallbacks.isEmpty()) { + vblCallbacks.remove(0).run(); + } + } + } + ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1); Runnable drawHints = () -> { if (getCpu().getProgramCounter() >> 8 != 0x0c6) { @@ -438,10 +482,10 @@ public class Apple2e extends Computer { private void enableHints() { if (hints.isEmpty()) { - hints.add(getMemory().observe(RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{ - animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS); - animationSchedule = - animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS); + hints.add(getMemory().observe(RAMEvent.TYPE.EXECUTE, 0x0FB63, (e) -> { + animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS); + animationSchedule + = animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS); })); // Latch to the PRODOS SYNTAX CHECK parser /* @@ -475,4 +519,4 @@ public class Apple2e extends Computer { public String getShortName() { return "computer"; } -} \ No newline at end of file +} diff --git a/src/main/java/jace/apple2e/VideoNTSC.java b/src/main/java/jace/apple2e/VideoNTSC.java index 37c651f..172d497 100644 --- a/src/main/java/jace/apple2e/VideoNTSC.java +++ b/src/main/java/jace/apple2e/VideoNTSC.java @@ -57,14 +57,14 @@ public class VideoNTSC extends VideoDHGR { public boolean enableVideo7 = true; // Scanline represents 560 bits, divided up into 28-bit words int[] scanline = new int[20]; - static int[] divBy28 = new int[560]; + static public int[] divBy28 = new int[560]; static { for (int i = 0; i < 560; i++) { divBy28[i] = i / 28; } } - boolean[] colorActive = new boolean[80]; + protected boolean[] colorActive = new boolean[80]; int rowStart = 0; public VideoNTSC(Computer computer) { @@ -89,7 +89,7 @@ public class VideoNTSC extends VideoDHGR { static int currentMode = -1; @InvokableAction(name = "Toggle video mode", category = "video", - alternatives = "mode,color,b&w,monochrome", + alternatives = "Gfx mode;color;b&w;monochrome", defaultKeyMapping = {"ctrl+shift+g"}) public static void changeVideoMode() { VideoNTSC thiss = (VideoNTSC) Emulator.computer.video; diff --git a/src/main/java/jace/cheat/Cheats.java b/src/main/java/jace/cheat/Cheats.java index fb8f34c..27db3fc 100644 --- a/src/main/java/jace/cheat/Cheats.java +++ b/src/main/java/jace/cheat/Cheats.java @@ -41,7 +41,7 @@ public abstract class Cheats extends Device { super(computer); } - @InvokableAction(name = "Toggle Cheats", alternatives = "cheat", defaultKeyMapping = "ctrl+shift+m") + @InvokableAction(name = "Toggle Cheats", alternatives = "cheat;Plug-in", defaultKeyMapping = "ctrl+shift+m") public void toggleCheats() { cheatsActive = !cheatsActive; if (cheatsActive) { @@ -97,7 +97,7 @@ public abstract class Cheats extends Device { super.detach(); } - abstract void registerListeners(); + public abstract void registerListeners(); protected void unregisterListeners() { listeners.stream().forEach((l) -> { diff --git a/src/main/java/jace/cheat/MetaCheat.java b/src/main/java/jace/cheat/MetaCheat.java index 384b019..162f51c 100644 --- a/src/main/java/jace/cheat/MetaCheat.java +++ b/src/main/java/jace/cheat/MetaCheat.java @@ -134,7 +134,7 @@ public class MetaCheat extends Cheats { } @Override - void registerListeners() { + public void registerListeners() { } public void addCheat(DynamicCheat cheat) { diff --git a/src/main/java/jace/cheat/MontezumasRevengeCheats.java b/src/main/java/jace/cheat/MontezumasRevengeCheats.java index abd04b7..ecd9c32 100644 --- a/src/main/java/jace/cheat/MontezumasRevengeCheats.java +++ b/src/main/java/jace/cheat/MontezumasRevengeCheats.java @@ -70,7 +70,7 @@ public class MontezumasRevengeCheats extends Cheats { }; @Override - void registerListeners() { + public void registerListeners() { RAM memory = Emulator.computer.memory; if (repulsiveHack) { addCheat(RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518); diff --git a/src/main/java/jace/config/Configuration.java b/src/main/java/jace/config/Configuration.java index 16fd22e..c45119d 100644 --- a/src/main/java/jace/config/Configuration.java +++ b/src/main/java/jace/config/Configuration.java @@ -49,6 +49,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import javafx.collections.ObservableList; import javafx.scene.control.TreeItem; import javafx.scene.image.Image; @@ -255,6 +256,12 @@ public class Configuration implements Reconfigurable { getChangedIcon().ifPresent(this::setGraphic); } } + + public Stream getTreeAsStream() { + return Stream.concat( + Stream.of(this), + children.stream().flatMap(ConfigNode::getTreeAsStream)); + } } public static ConfigNode BASE; public static EmulatorUILogic ui = Emulator.logic; @@ -301,7 +308,7 @@ public class Configuration implements Reconfigurable { node.setRawFieldValue(f.getName(), (Serializable) o); } continue; - } + } if (o == null) { continue; } @@ -494,7 +501,9 @@ public class Configuration implements Reconfigurable { newRoot.getChildren().stream().forEach((child) -> { String childName = child.toString(); ConfigNode oldChild = oldRoot.findChild(childName); - if (oldChild == null) {oldChild = oldRoot.findChild(child.id);} + if (oldChild == null) { + oldChild = oldRoot.findChild(child.id); + } // System.out.println("Applying settings for " + childName); applyConfigTree(child, oldChild); }); diff --git a/src/main/java/jace/core/Computer.java b/src/main/java/jace/core/Computer.java index 4c99ea1..cbce122 100644 --- a/src/main/java/jace/core/Computer.java +++ b/src/main/java/jace/core/Computer.java @@ -128,10 +128,18 @@ public abstract class Computer implements Reconfigurable { } public void deactivate() { - cpu.suspend(); - motherboard.suspend(); - video.suspend(); - mixer.detach(); + if (cpu != null) { + cpu.suspend(); + } + if (motherboard != null) { + motherboard.suspend(); + } + if (video != null) { + video.suspend(); + } + if (mixer != null) { + mixer.detach(); + } } @InvokableAction( @@ -161,7 +169,7 @@ public abstract class Computer implements Reconfigurable { name = "Warm boot", description = "Process user-initatiated reboot (ctrl+apple+reset)", category = "general", - alternatives = "reboot;reset;three-finger-salute", + alternatives = "reboot;reset;three-finger-salute;restart", defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"}) public void invokeWarmStart() { warmStart(); @@ -185,7 +193,7 @@ public abstract class Computer implements Reconfigurable { return result; } - @InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume", defaultKeyMapping = {"meta+shift+pause", "alt+shift+pause"}) + @InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume;play", defaultKeyMapping = {"meta+shift+pause", "alt+shift+pause"}) public void resume() { doResume(); getRunningProperty().set(true); diff --git a/src/main/java/jace/core/Keyboard.java b/src/main/java/jace/core/Keyboard.java index e8c8fa5..e7cfb5c 100644 --- a/src/main/java/jace/core/Keyboard.java +++ b/src/main/java/jace/core/Keyboard.java @@ -18,6 +18,7 @@ */ package jace.core; +import jace.Emulator; import jace.apple2e.SoftSwitches; import jace.config.InvokableAction; import jace.config.Reconfigurable; @@ -68,6 +69,7 @@ public class Keyboard implements Reconfigurable { return "kbd"; } static byte currentKey = 0; + public boolean shiftPressed = false; public static void clearStrobe() { currentKey = (byte) (currentKey & 0x07f); @@ -102,6 +104,7 @@ public class Keyboard implements Reconfigurable { registerKeyHandler(new KeyHandler(code) { @Override public boolean handleKeyUp(KeyEvent e) { + Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown(); if (action == null || !action.notifyOnRelease()) { return false; } @@ -125,6 +128,7 @@ public class Keyboard implements Reconfigurable { @Override public boolean handleKeyDown(KeyEvent e) { // System.out.println("Key down: "+method.toString()); + Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown(); Object returnValue = null; try { if (method.getParameterCount() > 0) { @@ -248,6 +252,7 @@ public class Keyboard implements Reconfigurable { default: } + Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown(); if (e.isShiftDown()) { c = fixShiftedChar(c); } diff --git a/src/main/java/jace/core/TimedDevice.java b/src/main/java/jace/core/TimedDevice.java index e2be07d..5aed2de 100644 --- a/src/main/java/jace/core/TimedDevice.java +++ b/src/main/java/jace/core/TimedDevice.java @@ -36,7 +36,8 @@ public abstract class TimedDevice extends Device { super(computer); setSpeed(cyclesPerSecond); } - @ConfigurableField(name = "Speed", description = "(in hertz)") + @ConfigurableField(name = "Speed", description = "(Percentage)") + public int speedRatio = 100; public long cyclesPerSecond = defaultCyclesPerSecond(); @ConfigurableField(name = "Max speed") public boolean maxspeed = false; @@ -170,6 +171,7 @@ public abstract class TimedDevice extends Device { @Override public void reconfigure() { + cyclesPerSecond = defaultCyclesPerSecond() * speedRatio / 100; if (cyclesPerSecond == 0) { cyclesPerSecond = defaultCyclesPerSecond(); } diff --git a/src/main/java/jace/core/Utility.java b/src/main/java/jace/core/Utility.java index e933542..1cc9162 100644 --- a/src/main/java/jace/core/Utility.java +++ b/src/main/java/jace/core/Utility.java @@ -18,6 +18,8 @@ */ package jace.core; +import jace.config.Configuration; +import jace.config.InvokableAction; import java.io.File; import java.io.InputStream; import java.lang.reflect.Field; @@ -36,15 +38,18 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.stream.Stream; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.effect.DropShadow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.paint.Color; + /** * This is a set of helper functions which do not belong anywhere else. * Functions vary from introspection, discovery, and string/pattern matching. @@ -52,7 +57,9 @@ import javafx.scene.paint.Color; * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class Utility { + static Reflections reflections = new Reflections("jace"); + public static Set findAllSubclasses(Class clazz) { return reflections.getSubTypesOf(clazz); } @@ -65,11 +72,11 @@ public class Utility { * * @param s * @param t - * @return Distance (higher is better) + * @return Distance (lower means a closer match, zero is identical) */ public static int levenshteinDistance(String s, String t) { if (s == null || t == null || s.length() == 0 || t.length() == 0) { - return -1; + return Integer.MAX_VALUE; } s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", ""); @@ -95,8 +102,19 @@ public class Utility { } } } - return Math.max(m, n) - dist[m][n]; + return dist[m][n]; } + + /** + * Normalize distance based on longest string + * @param s + * @param t + * @return Similarity ranking, higher is better + */ + public static int adjustedLevenshteinDistance(String s, String t) { + return Math.max(s.length(), t.length()) - levenshteinDistance(s, t); + } + /** * Compare strings based on a tally of similar patterns found, using a fixed @@ -107,7 +125,7 @@ public class Utility { * @param c1 * @param c2 * @param width Search window size - * @return Overall similarity score (higher is beter) + * @return Overall similarity score (higher is better) */ public static double rankMatch(String c1, String c2, int width) { double score = 0; @@ -135,6 +153,7 @@ public class Utility { } private static boolean isHeadless = false; + public static void setHeadlessMode(boolean headless) { isHeadless = headless; } @@ -142,7 +161,7 @@ public class Utility { public static boolean isHeadlessMode() { return isHeadless; } - + public static Optional loadIcon(String filename) { if (isHeadless) { return Optional.empty(); @@ -181,6 +200,20 @@ public class Utility { return Optional.of(label); } + public static void confirm(String title, String message, Runnable accept) { + Platform.runLater(() -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setContentText(message); + confirm.setTitle(title); + Optional response = confirm.showAndWait(); + response.ifPresent(b -> { + if (b.getButtonData().isDefaultButton()) { + (new Thread(accept)).start(); + } + }); + }); + } + // public static void runModalProcess(String title, final Runnable runnable) { //// final JDialog frame = new JDialog(Emulator.getFrame()); // final JProgressBar progressBar = new JProgressBar(); @@ -201,7 +234,6 @@ public class Utility { // frame.dispose(); // }).start(); // } - public static class RankingComparator implements Comparator { String match; @@ -215,8 +247,8 @@ public class Utility { @Override public int compare(String o1, String o2) { - double s1 = levenshteinDistance(match, o1); - double s2 = levenshteinDistance(match, o2); + double s1 = adjustedLevenshteinDistance(match, o1); + double s2 = adjustedLevenshteinDistance(match, o2); if (s2 == s1) { s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2); s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2); @@ -256,7 +288,7 @@ public class Utility { // System.out.println(match + "->" + c + ":" + l + " -- "+ m2 + "," + m3 + "," + "(" + (m2 + m3) + ")"); // } // double score = rankMatch(match, candidates.get(0), 2); - double score = levenshteinDistance(match, candidates.get(0)); + double score = adjustedLevenshteinDistance(match, candidates.get(0)); if (score > 1) { return candidates.get(0); } @@ -450,4 +482,47 @@ public class Utility { } return setChild(object, paths[paths.length - 1], value, hex); } + + static Map allActions = null; + + public static Map getAllInvokableActions() { + if (allActions == null) { + allActions = new HashMap<>(); + Configuration.BASE.getTreeAsStream().forEach((Configuration.ConfigNode node) -> { + for (Method m : node.subject.getClass().getMethods()) { + if (m.isAnnotationPresent(InvokableAction.class)) { + allActions.put(m.getAnnotation(InvokableAction.class), () -> { + try { + m.invoke(node.subject); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); + } + }); + } + } + }); + } + return allActions; + } + + public static Runnable getNamedInvokableAction(String action) { + Map actions = getAllInvokableActions(); + List actionsList = new ArrayList(actions.keySet()); + actionsList.sort((a,b) -> Integer.compare(getActionNameMatch(action, a), getActionNameMatch(action, b))); +// for (InvokableAction a : actionsList) { +// String actionName = a.alternatives() == null ? a.name() : (a.name() + ";" + a.alternatives()); +// System.out.println("Score for " + action + " evaluating " + a.name() + ": " + getActionNameMatch(action, a)); +// } + return actions.get(actionsList.get(0)); + } + + private static int getActionNameMatch(String str, InvokableAction action) { + int nameMatch = levenshteinDistance(str, action.name()); + if (action.alternatives() != null) { + for (String alt : action.alternatives().split(";")) { + nameMatch = Math.min(nameMatch, levenshteinDistance(str, alt)); + } + } + return nameMatch; + } } diff --git a/src/main/java/jace/core/Video.java b/src/main/java/jace/core/Video.java index 1867a06..695bdae 100644 --- a/src/main/java/jace/core/Video.java +++ b/src/main/java/jace/core/Video.java @@ -132,13 +132,15 @@ public abstract class Video extends Device { public static int MIN_SCREEN_REFRESH = 15; Runnable redrawScreen = () -> { - if (computer.getRunningProperty().get()) { - visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0); + if (visible != null && video != null) { +// if (computer.getRunningProperty().get()) { + screenDirty = false; + visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0); +// } } }; public void redraw() { - screenDirty = false; javafx.application.Platform.runLater(redrawScreen); } diff --git a/src/main/resources/fxml/JaceUI.fxml b/src/main/resources/fxml/JaceUI.fxml index 11b09e6..d6580bf 100644 --- a/src/main/resources/fxml/JaceUI.fxml +++ b/src/main/resources/fxml/JaceUI.fxml @@ -1,19 +1,188 @@ - - - - - - - + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/styles/icons/aspect.png b/src/main/resources/styles/icons/aspect.png new file mode 100644 index 0000000..67c025d Binary files /dev/null and b/src/main/resources/styles/icons/aspect.png differ diff --git a/src/main/resources/styles/icons/brun.png b/src/main/resources/styles/icons/brun.png new file mode 100644 index 0000000..217fcc8 Binary files /dev/null and b/src/main/resources/styles/icons/brun.png differ diff --git a/src/main/resources/styles/icons/config.png b/src/main/resources/styles/icons/config.png new file mode 100644 index 0000000..34008f9 Binary files /dev/null and b/src/main/resources/styles/icons/config.png differ diff --git a/src/main/resources/styles/icons/fast.png b/src/main/resources/styles/icons/fast.png new file mode 100644 index 0000000..7219fca Binary files /dev/null and b/src/main/resources/styles/icons/fast.png differ diff --git a/src/main/resources/styles/icons/fullscreen.png b/src/main/resources/styles/icons/fullscreen.png new file mode 100644 index 0000000..783f3e3 Binary files /dev/null and b/src/main/resources/styles/icons/fullscreen.png differ diff --git a/src/main/resources/styles/icons/ide.png b/src/main/resources/styles/icons/ide.png new file mode 100644 index 0000000..435b620 Binary files /dev/null and b/src/main/resources/styles/icons/ide.png differ diff --git a/src/main/resources/styles/icons/info.png b/src/main/resources/styles/icons/info.png new file mode 100644 index 0000000..444affe Binary files /dev/null and b/src/main/resources/styles/icons/info.png differ diff --git a/src/main/resources/styles/icons/inspect.png b/src/main/resources/styles/icons/inspect.png new file mode 100644 index 0000000..5d3fa9a Binary files /dev/null and b/src/main/resources/styles/icons/inspect.png differ diff --git a/src/main/resources/styles/icons/paste.png b/src/main/resources/styles/icons/paste.png new file mode 100644 index 0000000..d7e1386 Binary files /dev/null and b/src/main/resources/styles/icons/paste.png differ diff --git a/src/main/resources/styles/icons/play.png b/src/main/resources/styles/icons/play.png new file mode 100644 index 0000000..8795cc9 Binary files /dev/null and b/src/main/resources/styles/icons/play.png differ diff --git a/src/main/resources/styles/icons/plugin.png b/src/main/resources/styles/icons/plugin.png new file mode 100644 index 0000000..f6a01ef Binary files /dev/null and b/src/main/resources/styles/icons/plugin.png differ diff --git a/src/main/resources/styles/icons/reboot.png b/src/main/resources/styles/icons/reboot.png new file mode 100644 index 0000000..93f228a Binary files /dev/null and b/src/main/resources/styles/icons/reboot.png differ diff --git a/src/main/resources/styles/icons/rewind.png b/src/main/resources/styles/icons/rewind.png new file mode 100644 index 0000000..0cdbf51 Binary files /dev/null and b/src/main/resources/styles/icons/rewind.png differ diff --git a/src/main/resources/styles/icons/screenshot.png b/src/main/resources/styles/icons/screenshot.png new file mode 100644 index 0000000..3a41145 Binary files /dev/null and b/src/main/resources/styles/icons/screenshot.png differ diff --git a/src/main/resources/styles/icons/slow.png b/src/main/resources/styles/icons/slow.png new file mode 100644 index 0000000..8cd36bc Binary files /dev/null and b/src/main/resources/styles/icons/slow.png differ diff --git a/src/main/resources/styles/icons/sound.png b/src/main/resources/styles/icons/sound.png new file mode 100644 index 0000000..c8e743d Binary files /dev/null and b/src/main/resources/styles/icons/sound.png differ diff --git a/src/main/resources/styles/style.css b/src/main/resources/styles/style.css index 884c262..4b40374 100644 --- a/src/main/resources/styles/style.css +++ b/src/main/resources/styles/style.css @@ -3,7 +3,7 @@ } .setting-row { - -fx-padding: 5 0 0 4; + -fx-padding: 5 0 0 4; } .setting-label, .setting-keyboard-shortcut { @@ -19,3 +19,30 @@ -fx-font-size: 9pt; -fx-font-family: "Courier New"; } + +.menuButton { + -fx-font-size:16pt; + -fx-border-radius: 10px; + -fx-background-radius: 10px; +} + +.menuButton, .uiActionButton, .uiSpeedSlider ImageView, .uiSpeedSlider Slider, .uiSpeedSlider AnchorPane { + -fx-background-color: rgba(0, 0, 0, 0.75); + -fx-text-fill: #a0FFa0 +} + +.uiActionButton ImageView, .uiSpeedSlider ImageView { + -fx-effect: dropshadow(gaussian , rgba(128,255,128,0.75) , 2,1.0,0,0); +} + +.uiSpeedSlider AnchorPane { + -fx-padding: 0 5 0 5 +} + +.uiSpeedSlider Slider { + -fx-padding: 18 0 10 0 +} + +.uiSpeedSlider Slider NumberAxis { + -fx-tick-label-fill: #80ff80 +} \ No newline at end of file