Establishing app module, adjustments to localization support

This commit is contained in:
2020-08-01 15:42:28 -05:00
parent 85693cd130
commit 7c7a52c113
29 changed files with 359 additions and 137 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+32
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)
}
-9
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}
}
}
+5 -4
View File
@@ -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
+4 -9
View File
@@ -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();
}
}
+113
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]);
}
}
+155 -112
View File
@@ -11,8 +11,8 @@ public class Localizer {
// Instance fields
private HashMap<Object, Object> controls; // Control mapping
private HashMap<String, String> messages; // Message dictionary
private HashMap<Object, HashMap<String, String>> tags; // Control overrides
private Locale locale; // Current message store
private HashMap<Object, HashMap<String, String>> tags; // Control messages
@@ -26,124 +26,69 @@ public class Localizer {
///////////////////////////////////////////////////////////////////////////
// Constructors //
// Classes //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public Localizer() {
controls = new HashMap<Object, Object>();
messages = new HashMap<String, String>();
tags = new HashMap<Object, HashMap<String, String>>();
// Locale container
public static class Locale implements Comparable<Locale> {
// Public fields
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
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();
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<String, String>(); // Output dictionary
int start = 0; // Position of first character in key or value
var stack = new Stack<String>(); // 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<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();
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 + "\\}";
+50 -3
View File
@@ -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<String, String>(),
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);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.