diff --git a/Main.class b/Main.class deleted file mode 100644 index 6fcc676..0000000 Binary files a/Main.class and /dev/null differ diff --git a/app/App.class b/app/App.class deleted file mode 100644 index e7b5490..0000000 Binary files a/app/App.class and /dev/null differ diff --git a/images/app_icon.png b/images/app_icon.png new file mode 100644 index 0000000..3ad6e8b Binary files /dev/null and b/images/app_icon.png differ diff --git a/locale/en-US.txt b/locale/en-US.txt index d24483f..4d11e2d 100644 --- a/locale/en-US.txt +++ b/locale/en-US.txt @@ -6,6 +6,7 @@ locale { # Main window app { + title { default PVB Emulator mixed {ctrl.number} {ctrl.filename} - {app.title.default} @@ -16,6 +17,7 @@ app { file { (menu) File load_rom Load ROM... + new_window New window exit Exit } @@ -30,3 +32,11 @@ core { windows-x86 Windows (32-bit) windows-x86_64 Windows (64-bit) } + +# File dialog +dialog { + load Load + load_rom Load ROM + load_rom_error Unable to load the selected ROM file. + load_rom_notvb The selected file does not appear to be a Virtual Boy ROM. +} diff --git a/makefile b/makefile index 3ecbb74..afbe3c4 100644 --- a/makefile +++ b/makefile @@ -84,7 +84,7 @@ pack: $(eval jarname = "pvbemu_`date +%Y%m%d`.jar") @echo " Bundling into $(jarname)" @jar -cfe $(jarname) Main *.class \ - app locale native src util vue makefile license.txt + app images locale native src util vue makefile license.txt # Delete only Java .class files .PHONY: clean_desktop diff --git a/src/desktop/app/App.java b/src/desktop/app/App.java index b4dfa1a..97ed65f 100644 --- a/src/desktop/app/App.java +++ b/src/desktop/app/App.java @@ -12,6 +12,7 @@ public class App { // Instance fields private Localizer.Locale[] locales; // Language translations private Localizer localizer; // UI localization manager + private ArrayList windows; // Application windows @@ -24,41 +25,55 @@ public class App { // Instance fields localizer = new Localizer(); + windows = new ArrayList(); // Additional processing initLocales(); + addWindow(); } /////////////////////////////////////////////////////////////////////////// - // Public Methods // + // Package Methods // /////////////////////////////////////////////////////////////////////////// - // Associate a control with the localizer - public boolean addControl(Object control, Object key) { - return localizer.add(control, key); + // Add a new program window + void addWindow() { + windows.add(new Window(this)); + windowsChanged(); } // Retrieve the currently active locale - public Localizer.Locale getLocale() { + Localizer.Locale getLocale() { return localizer.getLocale(); } + // Retrieve the localization manager + Localizer getLocalizer() { + return localizer; + } + // Retrieve a list of registered locales - public Localizer.Locale[] listLocales() { + Localizer.Locale[] listLocales() { var ret = new Localizer.Locale[locales.length]; System.arraycopy(locales, 0, ret, 0, locales.length); return ret; } - // Remove a control from the localizer - public boolean removeControl(Object control) { - return localizer.remove(control); + // Determine the number of a window in the collection + int numberOf(Window window) { + return windows.indexOf(window) + 1; + } + + // Remove a program window + void removeWindow(Window window) { + windows.remove(window); + windowsChanged(); } // Specify a new locale - public void setLocale(Localizer.Locale locale) { + void setLocale(Localizer.Locale locale) { localizer.setLocale(locale); } @@ -110,4 +125,11 @@ public class App { localizer.setLocale(locale != null ? locale : this.locales[0]); } + // A window has been added to or removed from the program state + private void windowsChanged() { + int count = windows.size(); + for (int x = 0; x < count; x++) + windows.get(x).windowsChanged(x + 1, count == 1); + } + } diff --git a/src/desktop/app/Window.java b/src/desktop/app/Window.java new file mode 100644 index 0000000..fbfece7 --- /dev/null +++ b/src/desktop/app/Window.java @@ -0,0 +1,218 @@ +package app; + +// Java imports +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import javax.swing.*; +import javax.swing.filechooser.*; + +// Project imports +import util.*; + +// Main application window +class Window extends JFrame { + + // Instance fields + private App app; // Containing application + private int number; // Window number within application + private boolean only; // This is the only application window + private File romFile; // Currently loaded ROM file + + // UI components + private File pwd; // Most recent working directory + private JPanel video; // Video output + + + + /////////////////////////////////////////////////////////////////////////// + // Constants // + /////////////////////////////////////////////////////////////////////////// + + // Application icon + private static final BufferedImage APPICON; + + // Static initializer + static { + APPICON = Util.imageRead("images/app_icon.png"); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + Window(App app) { + super(); + + // Configure instance fields + this.app = app; + + // Configure video pane + video = new JPanel() { + public void paintComponent(Graphics g) { + super.paintComponent(g); + onPaintVideo((Graphics2D) g, getWidth(), getHeight()); + } + }; + video.setPreferredSize(new Dimension(384, 224)); + + // Configure window + addWindowListener(Util.onClose(e->onClose())); + setContentPane(video); + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + setIconImage(APPICON); + setJMenuBar(initMenus()); + app.getLocalizer().add(this, "app.title.default"); + + // Display window + pack(); + setLocationRelativeTo(null); + setVisible(true); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Menu Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Produce the window's menu bar + private JMenuBar initMenus() { + var bar = new JMenuBar(); + var loc = app.getLocalizer(); + bar.setBorder(null); + bar.add(initMenuFile(loc)); + return bar; + } + + // Initialize the File menu + private JMenu initMenuFile(Localizer loc) { + var mnuFile = new JMenu(); + loc.add(mnuFile, "app.file.(menu)"); + + var mnuFileLoadRom = new JMenuItem(); + loc.add(mnuFileLoadRom, "app.file.load_rom"); + mnuFileLoadRom.addActionListener(e->onLoadROM()); + mnuFile.add(mnuFileLoadRom); + + mnuFile.addSeparator(); + + var mnuFileNewWindow = new JMenuItem(); + loc.add(mnuFileNewWindow, "app.file.new_window"); + mnuFileNewWindow.addActionListener(e->onNewWindow()); + mnuFile.add(mnuFileNewWindow); + + var mnuFileExit = new JMenuItem(); + loc.add(mnuFileExit, "app.file.exit"); + mnuFileExit.addActionListener(e->onClose()); + mnuFile.add(mnuFileExit); + + return mnuFile; + } + + + + /////////////////////////////////////////////////////////////////////////// + // Package Methods // + /////////////////////////////////////////////////////////////////////////// + + // A window has been added to or removed from the program state + void windowsChanged(int number, boolean only) { + this.number = number; + this.only = only; + app.getLocalizer().put(this, "ctrl.number", "" + number); + updateTitle(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Event Handlers // + /////////////////////////////////////////////////////////////////////////// + + // Window close, File -> Exit + private void onClose() { + app.removeWindow(this); + dispose(); + } + + // File -> Load ROM + private void onLoadROM() { + var loc = app.getLocalizer(); + + // Prompt the user to select a file + var dlgFile = new JFileChooser(pwd); + dlgFile.addChoosableFileFilter(new FileNameExtensionFilter( + "Virtual Boy ROMs (*.vb)", "vb")); + dlgFile.addChoosableFileFilter(new FileNameExtensionFilter( + "ISX modules (*.isx)", "isx")); + dlgFile.setAcceptAllFileFilterUsed(true); + dlgFile.setDialogTitle(loc.get("dialog.load_rom")); + int option = dlgFile.showDialog(this, loc.get("dialog.load")); + + // The user did not select a file + var file = dlgFile.getSelectedFile(); + if (option != JFileChooser.APPROVE_OPTION || file == null) + return; + + // Update the current directory + pwd = file.getParentFile(); + + // Attempt to load the ROM file + var data = Util.fileRead(file); + if (data == null) { + JOptionPane.showMessageDialog(this, + loc.get("dialog.load_rom_error"), + loc.get("dialog.load_rom"), + JOptionPane.ERROR_MESSAGE + ); + return; + } + + // Update the emulation state + romFile = file; + loc.put(this, "ctrl.filename", file.getName()); + updateTitle(); + } + + // File -> New window + private void onNewWindow() { + app.addWindow(); + } + + // Video paint + private void onPaintVideo(Graphics2D g, int width, int height) { + int scale = Math.max(1, Math.min(width / 384, height / 224)); + + g.translate( + Math.max(0, (width - scale * 384) / 2), + Math.max(0, (height - scale * 224) / 2) + ); + g.scale(scale, scale); + + g.setColor(Color.black); + g.fillRect(0, 0, 384, 224); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Private Methods // + /////////////////////////////////////////////////////////////////////////// + + // Update the window title + private void updateTitle() { + app.getLocalizer().add(this, + only ? romFile != null ? + "app.title.rom" : + "app.title.default" + : romFile != null ? + "app.title.mixed" : + "app.title.number" + ); + } + +} diff --git a/src/desktop/util/Localizer.java b/src/desktop/util/Localizer.java index d34f9d6..3a40faf 100644 --- a/src/desktop/util/Localizer.java +++ b/src/desktop/util/Localizer.java @@ -10,9 +10,8 @@ import javax.swing.text.*; public class Localizer { // Instance fields - private HashMap controls; // Control mapping - private Locale locale; // Current message store - private HashMap> tags; // Control messages + private HashMap controls; // Control mapping + private Locale locale; // Current message store @@ -29,6 +28,18 @@ public class Localizer { // Classes // /////////////////////////////////////////////////////////////////////////// + // Control settings + private static class Control { + String key; // Single-string dictionary key + String[] keys; // Multiple-string dictionary key + String tipKey; // Tooltip dictionary key + HashMap tags; // Message overrides + Control(String tipKey) { + tags = new HashMap(); + this.tipKey = tipKey; + } + } + // Locale container public static class Locale implements Comparable { @@ -265,8 +276,7 @@ public class Localizer { // Default constructor public Localizer() { - controls = new HashMap(); - tags = new HashMap>(); + controls = new HashMap(); } // Parsing constructor @@ -283,6 +293,14 @@ public class Localizer { // Add a control to the collection public boolean add(Object control, Object key) { + return add(control, key, null); + } + + // Add a control with a tooltip to the collection + public boolean add(Object control, Object key, String tipKey) { + var ctrl = controls.get(control); + if (ctrl == null) + ctrl = new Control(tipKey); // Error checking if (control == null || key == null) @@ -290,6 +308,8 @@ public class Localizer { // Control takes a single string if (key instanceof String) { + + // Type validation if (!( control instanceof AbstractButton || control instanceof JFrame || @@ -297,22 +317,31 @@ public class Localizer { control instanceof JPanel || // TitledBorder control instanceof JTextComponent )) return false; + + // Configure key + ctrl.key = (String) key; } // Control takes an array of strings else if (key instanceof String[]) { + + // Type validation if (!( JCOMBOBOX.getClass().isAssignableFrom(control.getClass()) )) return false; + + // Configure keys + String[] keys = (String[]) key; + ctrl.keys = new String[keys.length]; + System.arraycopy(keys, 0, ctrl.keys, 0, keys.length); } // Invalid control type else return false; // Add the control to the collection - controls.put(control, key); - tags .put(control, new HashMap()); - update(); + controls.put(control, ctrl); + update(control); return true; } @@ -321,6 +350,11 @@ public class Localizer { controls.clear(); } + // Evaluate the message for a given key + public String get(String key) { + return key == null ? null : evaluate(null, key); + } + // Retrieve the currently loaded locale public Locale getLocale() { return locale; @@ -328,21 +362,26 @@ public class Localizer { // Configure a control tag public String put(Object control, String key, String value) { - if (controls.get(control) == null || key == null) + var ctrl = controls.get(control); + + // Error checking + if (ctrl == null || key == null) return null; - var tags = this.tags.get(control); + + // Update the control's tags key = key.toLowerCase(); String ret = value == null ? - tags.remove(key) : - tags.put(key, value) + ctrl.tags.remove(key) : + ctrl.tags.put(key, value) ; - update(); + + // Refresh the text on the control + update(control); return ret; } // Remove a control from the collection public boolean remove(Object control) { - tags.remove(control); return controls.remove(control) != null; } @@ -358,7 +397,7 @@ public class Localizer { /////////////////////////////////////////////////////////////////////////// // Process substitutions and escapes on a message for a given key - private String evaluate(Object control, String key) { + private String evaluate(Control control, String key) { // No locale is loaded if (locale == null) @@ -395,7 +434,7 @@ public class Localizer { // Determine the substitution key = new String(chars, start, x - start); String lkey = key.toLowerCase(); - String value = tags.get(control).get(lkey); + String value = control == null ? null : control.tags.get(lkey); if (value == null) value = locale.messages.get(lkey); if (value == null) @@ -430,68 +469,66 @@ public class Localizer { // Update the text for all controls private void update() { + for (var control : controls.keySet()) + update(control); + } - // Process all controls - for (var control : controls.keySet()) { - Object key = controls.get(control); - String[] values = null; + // Update the text for a control + private void update(Object control) { + var ctrl = controls.get(control); + String[] keys = ctrl.key==null ? ctrl.keys : new String[]{ctrl.key}; + String[] values = new String[keys.length]; - // One string - if (key instanceof String) - values = new String[] { evaluate(control, (String) key) }; + // Evaluate all messages + for (int x = 0; x < keys.length; x++) + values[x] = evaluate(ctrl, keys[x]); - // Multiple strings - else { - String[] keys = (String[]) key; - values = new String[keys.length]; - for (int x = 0; x < keys.length; x++) - values[x] = evaluate(control, keys[x]); - } - - // Update the control's text - if (control instanceof AbstractButton) - ((AbstractButton) control).setText (values[0]); - if (control instanceof JFrame) - ((JFrame ) control).setTitle(values[0]); - if (control instanceof JInternalFrame) - ((JInternalFrame) control).setTitle(values[0]); - if (control instanceof JTextComponent) - ((JTextComponent) control).setText (values[0]); - - // JPanel must be wrapped in a TitledBorder - if (control instanceof JPanel) { - var border = ((JPanel) control).getBorder(); - if (border instanceof TitledBorder) - ((TitledBorder) border).setTitle(values[0]); - } - - // Replace the contents of a JComboBox without firing events - if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) { - - // The type is explicitly verified above - @SuppressWarnings("unchecked") - var box = (JComboBox) control; - - // Configure working variables - var action = box.getActionListeners(); - int index = box.getSelectedIndex(); - var item = box.getItemListeners(); - - // Remove event listeners - for (var lst : action) box.removeActionListener(lst); - for (var lst : item ) box.removeItemListener (lst); - - // Update contents - box.setModel(new DefaultComboBoxModel(values)); - box.setSelectedIndex(index); - - // Restore event listeners - for (var lst : action) box.addActionListener(lst); - for (var lst : item ) box.addItemListener (lst); - } + // Update the control's text + if (control instanceof AbstractButton) + ((AbstractButton) control).setText (values[0]); + if (control instanceof JFrame) + ((JFrame ) control).setTitle(values[0]); + if (control instanceof JInternalFrame) + ((JInternalFrame) control).setTitle(values[0]); + if (control instanceof JTextComponent) + ((JTextComponent) control).setText (values[0]); + // JPanel must be wrapped in a TitledBorder + if (control instanceof JPanel) { + var border = ((JPanel) control).getBorder(); + if (border instanceof TitledBorder) + ((TitledBorder) border).setTitle(values[0]); } + // Replace the contents of a JComboBox without firing events + if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) { + + // The type is explicitly verified above + @SuppressWarnings("unchecked") + var box = (JComboBox) control; + + // Configure working variables + var action = box.getActionListeners(); + int index = box.getSelectedIndex(); + var item = box.getItemListeners(); + + // Remove event listeners + for (var lst : action) box.removeActionListener(lst); + for (var lst : item ) box.removeItemListener (lst); + + // Update contents + box.setModel(new DefaultComboBoxModel(values)); + box.setSelectedIndex(index); + + // Restore event listeners + for (var lst : action) box.addActionListener(lst); + for (var lst : item ) box.addItemListener (lst); + } + + // Update the control's tooltip text + if (control instanceof JComponent) + ((JComponent) control).setToolTipText( + ctrl.tipKey == null ? null : evaluate(ctrl, ctrl.tipKey)); } } diff --git a/src/desktop/util/Util.java b/src/desktop/util/Util.java index c002a18..ce2c894 100644 --- a/src/desktop/util/Util.java +++ b/src/desktop/util/Util.java @@ -117,6 +117,18 @@ public final class Util { return new Color(bits, true); } + // Read a file from disk + public static byte[] fileRead(File file) { + FileInputStream stream = null; + byte[] data = null; + try { + stream = new FileInputStream(file); + data = stream.readAllBytes(); + } catch (Exception e) { } + try { stream.close(); } catch (Exception e) { } + return data; + } + // Read a file, first from disk, then from .jar public static byte[] fileRead(String filename) { InputStream stream = null; diff --git a/util/Localizer$1.class b/util/Localizer$1.class deleted file mode 100644 index 8e91022..0000000 Binary files a/util/Localizer$1.class and /dev/null differ diff --git a/util/Localizer$Locale.class b/util/Localizer$Locale.class deleted file mode 100644 index 888a611..0000000 Binary files a/util/Localizer$Locale.class and /dev/null differ diff --git a/util/Localizer.class b/util/Localizer.class deleted file mode 100644 index 87c3c79..0000000 Binary files a/util/Localizer.class and /dev/null differ diff --git a/util/Util$1.class b/util/Util$1.class deleted file mode 100644 index b03d637..0000000 Binary files a/util/Util$1.class and /dev/null differ diff --git a/util/Util$2.class b/util/Util$2.class deleted file mode 100644 index 731113b..0000000 Binary files a/util/Util$2.class and /dev/null differ diff --git a/util/Util$3.class b/util/Util$3.class deleted file mode 100644 index a8f09ca..0000000 Binary files a/util/Util$3.class and /dev/null differ diff --git a/util/Util$4.class b/util/Util$4.class deleted file mode 100644 index 436af48..0000000 Binary files a/util/Util$4.class and /dev/null differ diff --git a/util/Util$5.class b/util/Util$5.class deleted file mode 100644 index 6428a19..0000000 Binary files a/util/Util$5.class and /dev/null differ diff --git a/util/Util$6.class b/util/Util$6.class deleted file mode 100644 index ba35529..0000000 Binary files a/util/Util$6.class and /dev/null differ diff --git a/util/Util$7.class b/util/Util$7.class deleted file mode 100644 index 9380e42..0000000 Binary files a/util/Util$7.class and /dev/null differ diff --git a/util/Util$8$1.class b/util/Util$8$1.class deleted file mode 100644 index f60f0e1..0000000 Binary files a/util/Util$8$1.class and /dev/null differ diff --git a/util/Util$8.class b/util/Util$8.class deleted file mode 100644 index 00781fe..0000000 Binary files a/util/Util$8.class and /dev/null differ diff --git a/util/Util$BOM.class b/util/Util$BOM.class deleted file mode 100644 index 9b0b6e2..0000000 Binary files a/util/Util$BOM.class and /dev/null differ diff --git a/util/Util$OnClose.class b/util/Util$OnClose.class deleted file mode 100644 index a4cf0b2..0000000 Binary files a/util/Util$OnClose.class and /dev/null differ diff --git a/util/Util$OnClose2.class b/util/Util$OnClose2.class deleted file mode 100644 index a23326d..0000000 Binary files a/util/Util$OnClose2.class and /dev/null differ diff --git a/util/Util$OnFocus.class b/util/Util$OnFocus.class deleted file mode 100644 index 50fcfdd..0000000 Binary files a/util/Util$OnFocus.class and /dev/null differ diff --git a/util/Util$OnKey.class b/util/Util$OnKey.class deleted file mode 100644 index 64a2c70..0000000 Binary files a/util/Util$OnKey.class and /dev/null differ diff --git a/util/Util$OnMouse.class b/util/Util$OnMouse.class deleted file mode 100644 index 697ce28..0000000 Binary files a/util/Util$OnMouse.class and /dev/null differ diff --git a/util/Util$OnResize.class b/util/Util$OnResize.class deleted file mode 100644 index d9a1345..0000000 Binary files a/util/Util$OnResize.class and /dev/null differ diff --git a/util/Util.class b/util/Util.class deleted file mode 100644 index 530cf9d..0000000 Binary files a/util/Util.class and /dev/null differ