Establishing app module, adjustments to localization support
This commit is contained in:
parent
85693cd130
commit
7c7a52c113
Binary file not shown.
Binary file not shown.
|
@ -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)
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
}
|
9
makefile
9
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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
if (c == '\n' || end)
|
||||
state = 0;
|
||||
line++;
|
||||
}
|
||||
|
||||
// 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 + "\\}";
|
||||
|
||||
|
|
|
@ -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.
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.
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.
Binary file not shown.
Loading…
Reference in New Issue