diff --git a/Main.class b/Main.class new file mode 100644 index 0000000..6fcc676 Binary files /dev/null and b/Main.class differ diff --git a/app/App.class b/app/App.class new file mode 100644 index 0000000..e7b5490 Binary files /dev/null and b/app/App.class differ diff --git a/locale/en-US.txt b/locale/en-US.txt new file mode 100644 index 0000000..d24483f --- /dev/null +++ b/locale/en-US.txt @@ -0,0 +1,32 @@ +# Locale +locale { + id en-US + name English (United States) +} + +# Main window +app { + title { + default PVB Emulator + mixed {ctrl.number} {ctrl.filename} - {app.title.default} + number {ctrl.number} {app.title.default} + rom {ctrl.filename} - {app.title.default} + } + + file { + (menu) File + load_rom Load ROM... + exit Exit + } + +} + +# Emulation core +core { + java Java + linux-x86 Linux (32-bit) + linux-x86_64 Linux (64-bit) + native Native + windows-x86 Windows (32-bit) + windows-x86_64 Windows (64-bit) +} diff --git a/locale/en_US.txt b/locale/en_US.txt deleted file mode 100644 index 85c6689..0000000 --- a/locale/en_US.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Main window -app { - title { - default PVB Emulator - mixed {ctrl.number} {ctrl.filename} - {app.title.default} - number {ctrl.number} {app.title.default} - rom {ctrl.filename} - {app.title.default} - } -} diff --git a/makefile b/makefile index f6467e9..3ecbb74 100644 --- a/makefile +++ b/makefile @@ -67,7 +67,8 @@ core: .PHONY: desktop desktop: clean_desktop @echo " Compiling Java desktop application" - @javac -sourcepath src/desktop -d . -Xlint:unchecked src/desktop/Main.java + @javac -sourcepath src/desktop --release 10 -Xlint:unchecked \ + -d . src/desktop/Main.java # Build all native modules .PHONY: native @@ -83,17 +84,17 @@ pack: $(eval jarname = "pvbemu_`date +%Y%m%d`.jar") @echo " Bundling into $(jarname)" @jar -cfe $(jarname) Main *.class \ - locale native src util vue makefile license.txt + app locale native src util vue makefile license.txt # Delete only Java .class files .PHONY: clean_desktop clean_desktop: - @rm -r -f *.class util vue + @rm -r -f *.class app util vue # Delete everything but the .jar .PHONY: clean_most clean_most: clean_desktop - @rm -f src/desktop/native/vue_NativeVUE.h native/* + @rm -f src/desktop/native/vue_NativeVUE.h native/*.dll native/*.so diff --git a/src/desktop/Main.java b/src/desktop/Main.java index 1ea43b8..5b50c55 100644 --- a/src/desktop/Main.java +++ b/src/desktop/Main.java @@ -1,5 +1,5 @@ -import java.util.*; -import javax.swing.*; +// Project imports +import app.*; import util.*; // Desktop application primary class @@ -7,13 +7,8 @@ public class Main { // Program entry point public static void main(String[] args) { - var loc = new Localizer(); - loc.set(Util.textRead("locale/en_US.txt")); - var window = new JFrame(); - loc.add(window, "app.title.mixed"); - loc.put(window, "ctrl.filename", "wario.vb"); - loc.put(window, "ctrl.number", "1"); - System.out.println(window.getTitle()); + Util.setSystemLAF(); + new App(); } } diff --git a/src/desktop/app/App.java b/src/desktop/app/App.java new file mode 100644 index 0000000..b4dfa1a --- /dev/null +++ b/src/desktop/app/App.java @@ -0,0 +1,113 @@ +package app; + +// Java imports +import java.util.*; + +// Project imports +import util.*; + +// Top-level software state manager +public class App { + + // Instance fields + private Localizer.Locale[] locales; // Language translations + private Localizer localizer; // UI localization manager + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + public App() { + + // Instance fields + localizer = new Localizer(); + + // Additional processing + initLocales(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Public Methods // + /////////////////////////////////////////////////////////////////////////// + + // Associate a control with the localizer + public boolean addControl(Object control, Object key) { + return localizer.add(control, key); + } + + // Retrieve the currently active locale + public Localizer.Locale getLocale() { + return localizer.getLocale(); + } + + // Retrieve a list of registered locales + public 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); + } + + // Specify a new locale + public void setLocale(Localizer.Locale locale) { + localizer.setLocale(locale); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Private Methods // + /////////////////////////////////////////////////////////////////////////// + + // Load and parse all locale translations + private void initLocales() { + + // Process all locale files + var locales = new HashMap(); + for (String file : Util.listFiles("locale")) { + + // Process the file + try { + var locale = Localizer.parse(Util.textRead("locale/" + file)); + String key = locale.id.toLowerCase(); + + // Register the locale + if (locales.containsKey(key)) throw new RuntimeException( + "Locale with ID '" + locale.id + "' already registered"); + locales.put(key, locale); + + // The locale matches the one in the config + if (key.equals("en-us")) + localizer.setLocale(locale); + } + + // Could not process the file + catch (Exception e) { + System.err.println("Error parsing locale " + + file + ": " + e.getMessage()); + } + + } + + // Produce the list of registered locales + this.locales = locales.values().toArray( + new Localizer.Locale[locales.size()]); + Arrays.sort(this.locales); + + // Select a default locale + if (localizer.getLocale() != null || this.locales.length == 0) + return; + var locale = locales.get("en-us"); + localizer.setLocale(locale != null ? locale : this.locales[0]); + } + +} diff --git a/src/desktop/util/Localizer.java b/src/desktop/util/Localizer.java index bbdf995..d34f9d6 100644 --- a/src/desktop/util/Localizer.java +++ b/src/desktop/util/Localizer.java @@ -11,8 +11,8 @@ public class Localizer { // Instance fields private HashMap controls; // Control mapping - private HashMap messages; // Message dictionary - private HashMap> tags; // Control overrides + private Locale locale; // Current message store + private HashMap> tags; // Control messages @@ -26,124 +26,69 @@ public class Localizer { /////////////////////////////////////////////////////////////////////////// - // Constructors // + // Classes // /////////////////////////////////////////////////////////////////////////// - // Default constructor - public Localizer() { - controls = new HashMap(); - messages = new HashMap(); - tags = new HashMap>(); + // Locale container + public static class Locale implements Comparable { + + // Public fields + public final String id; // Unique identifier + public final String name; // Display name + + // Private fields + private HashMap messages; // Message dictionary + + // Constructor + private Locale(HashMap messages) { + id = messages.get("locale.id"); + this.messages = messages; + name = messages.get("locale.name"); + } + + // Comparator + public int compareTo(Locale o) { + return id.compareTo(o.id); + } + + // Represent this object as a string + public String toString() { + return id + " - " + name; + } + } /////////////////////////////////////////////////////////////////////////// - // Public Methods // + // Static Methods // /////////////////////////////////////////////////////////////////////////// - // Add a control to the collection - public boolean add(Object control, Object key) { - - // Error checking - if (control == null || key == null) - return false; - - // Control takes a single string - if (key instanceof String) { - if (!( - control instanceof AbstractButton || - control instanceof JFrame || - control instanceof JInternalFrame || - control instanceof JPanel || // TitledBorder - control instanceof JTextComponent - )) return false; - } - - // Control takes an array of strings - else if (key instanceof String[]) { - if (!( - JCOMBOBOX.getClass().isAssignableFrom(control.getClass()) - )) return false; - } - - // Invalid control type - else return false; - - // Add the control to the collection - controls.put(control, key); - tags .put(control, new HashMap()); - update(); - return true; - } - - // Remove all controls from being managed - public void clearControls() { - controls.clear(); - } - - // Configure a control tag - public String put(Object control, String key, String value) { - if (controls.get(control) == null || key == null) - return null; - var tags = this.tags.get(control); - key = key.toLowerCase(); - String ret = value == null ? - tags.remove(key) : - tags.put(key, value) - ; - update(); - return ret; - } - - // Configure a dictionary entry - public String put(String key, String value) { - String ret = value == null ? - messages.remove(key) : - messages.put(key, value) - ; - update(); - return ret; - } - - // Remove a control from the collection - public boolean remove(Object control) { - tags.remove(control); - return controls.remove(control) != null; - } - - // Specify a message dictionary - public void set(String text) { - - // Pre-processing - messages.clear(); - if (text == null) { - update(); - return; - } - - // Configure working variables + // Parse a text file to produce a Locale object + public static Locale parse(String text) { var chars = text.replaceAll("\\r\\n?", "\n").toCharArray(); - int col = 1; // Current character on the current line - int line = 1; // Current line in the file + var ret = new HashMap(); // Output dictionary int start = 0; // Position of first character in key or value var stack = new Stack(); // Nested key chain int state = 0; // Current parsing context // Process all characters - for (int x = 0; x < chars.length; x++, col++) { + for (int x = 0, line = 1, col = 1; x < chars.length; x++, col++) { char c = chars[x]; boolean end = x == chars.length - 1; String pos = line + ":" + col + ": "; boolean white = c == ' ' || c == '\t'; + // Processing for newline + if (c == '\n') { + col = 0; + line++; + } + // Comment if (state == -1) { - if (c != '\n' && !end) - continue; - col = 1; - state = 0; - line++; + if (c == '\n' || end) + state = 0; } // Pre-key @@ -164,11 +109,8 @@ public class Localizer { break; // The line is empty - if (c == '\n') { - col = 1; - line++; + if (c == '\n') continue; - } // Proceed to key start = x; @@ -263,9 +205,7 @@ public class Localizer { if (end) x++; String value = new String(chars, start, x - start).trim(); - col = 1; state = 0; - line++; // Open a key group if (value.equals("{")) { @@ -281,8 +221,7 @@ public class Localizer { case '{' : depth++; continue; case '}' : if (--depth == -1) throw new RuntimeException( - line + ":" + (x - start + y + 1) + - ": Unexpected '}'" + (line - 1) + ": Unexpected '}'" ); continue; } @@ -298,18 +237,118 @@ public class Localizer { // Check for duplicate keys String lkey = key.toLowerCase(); - if (messages.get(lkey) != null) throw new RuntimeException( + if (ret.get(lkey) != null) throw new RuntimeException( (line-1)+ ": Key '" + key + "' has already been defined."); // Add the pair to the dictionary - messages.put(lkey, value); + ret.put(lkey, value); stack.pop(); } } - // Update all controls + // Perform post-processing error checks + if (state != 0 || !stack.empty()) throw new RuntimeException( + "Unexpected end of input"); + if (!ret.containsKey("locale.id")) throw new RuntimeException( + "Required key not found: 'locale.id'"); + if (!ret.containsKey("locale.name")) throw new RuntimeException( + "Required key not found: 'locale.name'"); + return new Locale(ret); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + public Localizer() { + controls = new HashMap(); + tags = new HashMap>(); + } + + // Parsing constructor + public Localizer(String text) { + this(); + setLocale(parse(text)); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Public Methods // + /////////////////////////////////////////////////////////////////////////// + + // Add a control to the collection + public boolean add(Object control, Object key) { + + // Error checking + if (control == null || key == null) + return false; + + // Control takes a single string + if (key instanceof String) { + if (!( + control instanceof AbstractButton || + control instanceof JFrame || + control instanceof JInternalFrame || + control instanceof JPanel || // TitledBorder + control instanceof JTextComponent + )) return false; + } + + // Control takes an array of strings + else if (key instanceof String[]) { + if (!( + JCOMBOBOX.getClass().isAssignableFrom(control.getClass()) + )) return false; + } + + // Invalid control type + else return false; + + // Add the control to the collection + controls.put(control, key); + tags .put(control, new HashMap()); update(); + return true; + } + + // Remove all controls from being managed + public void clearControls() { + controls.clear(); + } + + // Retrieve the currently loaded locale + public Locale getLocale() { + return locale; + } + + // Configure a control tag + public String put(Object control, String key, String value) { + if (controls.get(control) == null || key == null) + return null; + var tags = this.tags.get(control); + key = key.toLowerCase(); + String ret = value == null ? + tags.remove(key) : + tags.put(key, value) + ; + update(); + return ret; + } + + // Remove a control from the collection + public boolean remove(Object control) { + tags.remove(control); + return controls.remove(control) != null; + } + + // Specify a message dictionary + public void setLocale(Locale locale) { + this.locale = locale; } @@ -320,9 +359,13 @@ public class Localizer { // Process substitutions and escapes on a message for a given key private String evaluate(Object control, String key) { - String ret = messages.get(key.toLowerCase()); - // The topmost key does not exist in the dictionary + // No locale is loaded + if (locale == null) + return key; + + // Check that the key exists + String ret = locale.messages.get(key.toLowerCase()); if (ret == null) return key; @@ -354,7 +397,7 @@ public class Localizer { String lkey = key.toLowerCase(); String value = tags.get(control).get(lkey); if (value == null) - value = messages.get(lkey); + value = locale.messages.get(lkey); if (value == null) value = "\\{" + key + "\\}"; diff --git a/src/desktop/util/Util.java b/src/desktop/util/Util.java index 0471f19..c002a18 100644 --- a/src/desktop/util/Util.java +++ b/src/desktop/util/Util.java @@ -5,7 +5,9 @@ import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; +import java.net.*; import java.nio.charset.*; +import java.nio.file.*; import java.util.*; import javax.imageio.*; import javax.swing.*; @@ -53,6 +55,23 @@ public final class Util { new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE), }; + // Filesystem state manager for the current .jar (if any) + private static final FileSystem JARFS; + + // Static initializer + static { + FileSystem fs = null; + try { + fs = FileSystems.newFileSystem( + new URI("jar:" + Util.class.getProtectionDomain() + .getCodeSource().getLocation().toString()), + new HashMap(), + Util.class.getClassLoader() + ); + } catch (Exception e) { } + JARFS = fs; + } + /////////////////////////////////////////////////////////////////////////// @@ -138,6 +157,34 @@ public final class Util { catch (Exception e) { return null; } } + // Produce a list of files contained in a directory + // If the directory is not found on disk, looks for it in the .jar + public static String[] listFiles(String path) { + + // Check for the directory on disk + var file = new File(path); + if (file.exists() && file.isDirectory()) + return file.list(); + + // Not executing out of a .jar + if (JARFS == null) + return null; + + // Check for the directory in the .jar + try { + var list = Files.list(JARFS.getPath("/" + path)).toArray(); + var ret = new String[list.length]; + for (int x = 0; x < list.length; x++) { + path = "/" + ((Path) list[x]).toString(); + ret[x] = path.substring(path.lastIndexOf("/") + 1); + } + return ret; + } catch (Exception e) { } + + // The directory was not found + return null; + } + // Produce a WindowListener with a functional interface public static WindowListener onClose(OnClose close) { return new WindowListener() { @@ -246,7 +293,7 @@ public final class Util { return split; } - // Read a text file, accounting for BOMs and guaranteeing Unix line endings + // Read a text file, accounting for BOMs public static String textRead(String filename) { // Read the file @@ -277,8 +324,8 @@ public final class Util { data = temp; } - // Produce a string with LF-style (Unix) line endings - return new String(data, set).replaceAll("\\r\\n?", "\n"); + // Produce a string + return new String(data, set); } } diff --git a/util/Localizer$1.class b/util/Localizer$1.class new file mode 100644 index 0000000..8e91022 Binary files /dev/null and b/util/Localizer$1.class differ diff --git a/util/Localizer$Locale.class b/util/Localizer$Locale.class new file mode 100644 index 0000000..888a611 Binary files /dev/null and b/util/Localizer$Locale.class differ diff --git a/util/Localizer.class b/util/Localizer.class new file mode 100644 index 0000000..87c3c79 Binary files /dev/null and b/util/Localizer.class differ diff --git a/util/Util$1.class b/util/Util$1.class new file mode 100644 index 0000000..b03d637 Binary files /dev/null and b/util/Util$1.class differ diff --git a/util/Util$2.class b/util/Util$2.class new file mode 100644 index 0000000..731113b Binary files /dev/null and b/util/Util$2.class differ diff --git a/util/Util$3.class b/util/Util$3.class new file mode 100644 index 0000000..a8f09ca Binary files /dev/null and b/util/Util$3.class differ diff --git a/util/Util$4.class b/util/Util$4.class new file mode 100644 index 0000000..436af48 Binary files /dev/null and b/util/Util$4.class differ diff --git a/util/Util$5.class b/util/Util$5.class new file mode 100644 index 0000000..6428a19 Binary files /dev/null and b/util/Util$5.class differ diff --git a/util/Util$6.class b/util/Util$6.class new file mode 100644 index 0000000..ba35529 Binary files /dev/null and b/util/Util$6.class differ diff --git a/util/Util$7.class b/util/Util$7.class new file mode 100644 index 0000000..9380e42 Binary files /dev/null and b/util/Util$7.class differ diff --git a/util/Util$8$1.class b/util/Util$8$1.class new file mode 100644 index 0000000..f60f0e1 Binary files /dev/null and b/util/Util$8$1.class differ diff --git a/util/Util$8.class b/util/Util$8.class new file mode 100644 index 0000000..00781fe Binary files /dev/null and b/util/Util$8.class differ diff --git a/util/Util$BOM.class b/util/Util$BOM.class new file mode 100644 index 0000000..9b0b6e2 Binary files /dev/null and b/util/Util$BOM.class differ diff --git a/util/Util$OnClose.class b/util/Util$OnClose.class new file mode 100644 index 0000000..a4cf0b2 Binary files /dev/null and b/util/Util$OnClose.class differ diff --git a/util/Util$OnClose2.class b/util/Util$OnClose2.class new file mode 100644 index 0000000..a23326d Binary files /dev/null and b/util/Util$OnClose2.class differ diff --git a/util/Util$OnFocus.class b/util/Util$OnFocus.class new file mode 100644 index 0000000..50fcfdd Binary files /dev/null and b/util/Util$OnFocus.class differ diff --git a/util/Util$OnKey.class b/util/Util$OnKey.class new file mode 100644 index 0000000..64a2c70 Binary files /dev/null and b/util/Util$OnKey.class differ diff --git a/util/Util$OnMouse.class b/util/Util$OnMouse.class new file mode 100644 index 0000000..697ce28 Binary files /dev/null and b/util/Util$OnMouse.class differ diff --git a/util/Util$OnResize.class b/util/Util$OnResize.class new file mode 100644 index 0000000..d9a1345 Binary files /dev/null and b/util/Util$OnResize.class differ diff --git a/util/Util.class b/util/Util.class new file mode 100644 index 0000000..530cf9d Binary files /dev/null and b/util/Util.class differ