Establishing app module, adjustments to localization support

This commit is contained in:
Guy Perfect 2020-08-01 15:42:28 -05:00
parent 85693cd130
commit 7c7a52c113
29 changed files with 359 additions and 137 deletions

BIN
Main.class Normal file

Binary file not shown.

BIN
app/App.class Normal file

Binary file not shown.

32
locale/en-US.txt Normal file
View File

@ -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)
}

View File

@ -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}
}
}

View File

@ -67,7 +67,8 @@ core:
.PHONY: desktop .PHONY: desktop
desktop: clean_desktop desktop: clean_desktop
@echo " Compiling Java desktop application" @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 # Build all native modules
.PHONY: native .PHONY: native
@ -83,17 +84,17 @@ pack:
$(eval jarname = "pvbemu_`date +%Y%m%d`.jar") $(eval jarname = "pvbemu_`date +%Y%m%d`.jar")
@echo " Bundling into $(jarname)" @echo " Bundling into $(jarname)"
@jar -cfe $(jarname) Main *.class \ @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 # Delete only Java .class files
.PHONY: clean_desktop .PHONY: clean_desktop
clean_desktop: clean_desktop:
@rm -r -f *.class util vue @rm -r -f *.class app util vue
# Delete everything but the .jar # Delete everything but the .jar
.PHONY: clean_most .PHONY: clean_most
clean_most: clean_desktop 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

View File

@ -1,5 +1,5 @@
import java.util.*; // Project imports
import javax.swing.*; import app.*;
import util.*; import util.*;
// Desktop application primary class // Desktop application primary class
@ -7,13 +7,8 @@ public class Main {
// Program entry point // Program entry point
public static void main(String[] args) { public static void main(String[] args) {
var loc = new Localizer(); Util.setSystemLAF();
loc.set(Util.textRead("locale/en_US.txt")); new App();
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());
} }
} }

113
src/desktop/app/App.java Normal file
View File

@ -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<String, Localizer.Locale>();
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]);
}
}

View File

@ -11,8 +11,8 @@ public class Localizer {
// Instance fields // Instance fields
private HashMap<Object, Object> controls; // Control mapping private HashMap<Object, Object> controls; // Control mapping
private HashMap<String, String> messages; // Message dictionary private Locale locale; // Current message store
private HashMap<Object, HashMap<String, String>> tags; // Control overrides private HashMap<Object, HashMap<String, String>> tags; // Control messages
@ -26,124 +26,69 @@ public class Localizer {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Constructors // // Classes //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Default constructor // Locale container
public Localizer() { public static class Locale implements Comparable<Locale> {
controls = new HashMap<Object, Object>();
messages = new HashMap<String, String>(); // Public fields
tags = new HashMap<Object, HashMap<String, String>>(); public final String id; // Unique identifier
public final String name; // Display name
// Private fields
private HashMap<String, String> messages; // Message dictionary
// Constructor
private Locale(HashMap<String, String> 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 // Parse a text file to produce a Locale object
public boolean add(Object control, Object key) { public static Locale parse(String text) {
// 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<String, String>());
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
var chars = text.replaceAll("\\r\\n?", "\n").toCharArray(); var chars = text.replaceAll("\\r\\n?", "\n").toCharArray();
int col = 1; // Current character on the current line var ret = new HashMap<String, String>(); // Output dictionary
int line = 1; // Current line in the file
int start = 0; // Position of first character in key or value int start = 0; // Position of first character in key or value
var stack = new Stack<String>(); // Nested key chain var stack = new Stack<String>(); // Nested key chain
int state = 0; // Current parsing context int state = 0; // Current parsing context
// Process all characters // 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]; char c = chars[x];
boolean end = x == chars.length - 1; boolean end = x == chars.length - 1;
String pos = line + ":" + col + ": "; String pos = line + ":" + col + ": ";
boolean white = c == ' ' || c == '\t'; boolean white = c == ' ' || c == '\t';
// Processing for newline
if (c == '\n') {
col = 0;
line++;
}
// Comment // Comment
if (state == -1) { if (state == -1) {
if (c != '\n' && !end) if (c == '\n' || end)
continue; state = 0;
col = 1;
state = 0;
line++;
} }
// Pre-key // Pre-key
@ -164,11 +109,8 @@ public class Localizer {
break; break;
// The line is empty // The line is empty
if (c == '\n') { if (c == '\n')
col = 1;
line++;
continue; continue;
}
// Proceed to key // Proceed to key
start = x; start = x;
@ -263,9 +205,7 @@ public class Localizer {
if (end) if (end)
x++; x++;
String value = new String(chars, start, x - start).trim(); String value = new String(chars, start, x - start).trim();
col = 1;
state = 0; state = 0;
line++;
// Open a key group // Open a key group
if (value.equals("{")) { if (value.equals("{")) {
@ -281,8 +221,7 @@ public class Localizer {
case '{' : depth++; continue; case '{' : depth++; continue;
case '}' : if (--depth == -1) case '}' : if (--depth == -1)
throw new RuntimeException( throw new RuntimeException(
line + ":" + (x - start + y + 1) + (line - 1) + ": Unexpected '}'"
": Unexpected '}'"
); );
continue; continue;
} }
@ -298,18 +237,118 @@ public class Localizer {
// Check for duplicate keys // Check for duplicate keys
String lkey = key.toLowerCase(); 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."); (line-1)+ ": Key '" + key + "' has already been defined.");
// Add the pair to the dictionary // Add the pair to the dictionary
messages.put(lkey, value); ret.put(lkey, value);
stack.pop(); 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<Object, Object>();
tags = new HashMap<Object, HashMap<String, String>>();
}
// 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<String, String>());
update(); 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 // Process substitutions and escapes on a message for a given key
private String evaluate(Object control, String 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) if (ret == null)
return key; return key;
@ -354,7 +397,7 @@ public class Localizer {
String lkey = key.toLowerCase(); String lkey = key.toLowerCase();
String value = tags.get(control).get(lkey); String value = tags.get(control).get(lkey);
if (value == null) if (value == null)
value = messages.get(lkey); value = locale.messages.get(lkey);
if (value == null) if (value == null)
value = "\\{" + key + "\\}"; value = "\\{" + key + "\\}";

View File

@ -5,7 +5,9 @@ import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.awt.image.*; import java.awt.image.*;
import java.io.*; import java.io.*;
import java.net.*;
import java.nio.charset.*; import java.nio.charset.*;
import java.nio.file.*;
import java.util.*; import java.util.*;
import javax.imageio.*; import javax.imageio.*;
import javax.swing.*; import javax.swing.*;
@ -53,6 +55,23 @@ public final class Util {
new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE), 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<String, String>(),
Util.class.getClassLoader()
);
} catch (Exception e) { }
JARFS = fs;
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -138,6 +157,34 @@ public final class Util {
catch (Exception e) { return null; } 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 // Produce a WindowListener with a functional interface
public static WindowListener onClose(OnClose close) { public static WindowListener onClose(OnClose close) {
return new WindowListener() { return new WindowListener() {
@ -246,7 +293,7 @@ public final class Util {
return split; 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) { public static String textRead(String filename) {
// Read the file // Read the file
@ -277,8 +324,8 @@ public final class Util {
data = temp; data = temp;
} }
// Produce a string with LF-style (Unix) line endings // Produce a string
return new String(data, set).replaceAll("\\r\\n?", "\n"); return new String(data, set);
} }
} }

BIN
util/Localizer$1.class Normal file

Binary file not shown.

BIN
util/Localizer$Locale.class Normal file

Binary file not shown.

BIN
util/Localizer.class Normal file

Binary file not shown.

BIN
util/Util$1.class Normal file

Binary file not shown.

BIN
util/Util$2.class Normal file

Binary file not shown.

BIN
util/Util$3.class Normal file

Binary file not shown.

BIN
util/Util$4.class Normal file

Binary file not shown.

BIN
util/Util$5.class Normal file

Binary file not shown.

BIN
util/Util$6.class Normal file

Binary file not shown.

BIN
util/Util$7.class Normal file

Binary file not shown.

BIN
util/Util$8$1.class Normal file

Binary file not shown.

BIN
util/Util$8.class Normal file

Binary file not shown.

BIN
util/Util$BOM.class Normal file

Binary file not shown.

BIN
util/Util$OnClose.class Normal file

Binary file not shown.

BIN
util/Util$OnClose2.class Normal file

Binary file not shown.

BIN
util/Util$OnFocus.class Normal file

Binary file not shown.

BIN
util/Util$OnKey.class Normal file

Binary file not shown.

BIN
util/Util$OnMouse.class Normal file

Binary file not shown.

BIN
util/Util$OnResize.class Normal file

Binary file not shown.

BIN
util/Util.class Normal file

Binary file not shown.