Introducing the main window, yet more tweaks to localization support
This commit is contained in:
parent
7c7a52c113
commit
73c8245eff
BIN
Main.class
BIN
Main.class
Binary file not shown.
BIN
app/App.class
BIN
app/App.class
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 220 B |
|
@ -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.
|
||||
}
|
||||
|
|
2
makefile
2
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
|
||||
|
|
|
@ -12,6 +12,7 @@ public class App {
|
|||
// Instance fields
|
||||
private Localizer.Locale[] locales; // Language translations
|
||||
private Localizer localizer; // UI localization manager
|
||||
private ArrayList<Window> windows; // Application windows
|
||||
|
||||
|
||||
|
||||
|
@ -24,41 +25,55 @@ public class App {
|
|||
|
||||
// Instance fields
|
||||
localizer = new Localizer();
|
||||
windows = new ArrayList<Window>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,9 +10,8 @@ import javax.swing.text.*;
|
|||
public class Localizer {
|
||||
|
||||
// Instance fields
|
||||
private HashMap<Object, Object> controls; // Control mapping
|
||||
private HashMap<Object, Control> controls; // Control mapping
|
||||
private Locale locale; // Current message store
|
||||
private HashMap<Object, HashMap<String, String>> tags; // Control messages
|
||||
|
||||
|
||||
|
||||
|
@ -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<String, String> tags; // Message overrides
|
||||
Control(String tipKey) {
|
||||
tags = new HashMap<String, String>();
|
||||
this.tipKey = tipKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Locale container
|
||||
public static class Locale implements Comparable<Locale> {
|
||||
|
||||
|
@ -265,8 +276,7 @@ public class Localizer {
|
|||
|
||||
// Default constructor
|
||||
public Localizer() {
|
||||
controls = new HashMap<Object, Object>();
|
||||
tags = new HashMap<Object, HashMap<String, String>>();
|
||||
controls = new HashMap<Object, Control>();
|
||||
}
|
||||
|
||||
// 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<String, String>());
|
||||
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,24 +469,20 @@ public class Localizer {
|
|||
|
||||
// Update the text for all controls
|
||||
private void update() {
|
||||
|
||||
// Process all controls
|
||||
for (var control : controls.keySet()) {
|
||||
Object key = controls.get(control);
|
||||
String[] values = null;
|
||||
|
||||
// One string
|
||||
if (key instanceof String)
|
||||
values = new String[] { evaluate(control, (String) key) };
|
||||
|
||||
// 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]);
|
||||
for (var control : controls.keySet())
|
||||
update(control);
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// Evaluate all messages
|
||||
for (int x = 0; x < keys.length; x++)
|
||||
values[x] = evaluate(ctrl, keys[x]);
|
||||
|
||||
// Update the control's text
|
||||
if (control instanceof AbstractButton)
|
||||
((AbstractButton) control).setText (values[0]);
|
||||
|
@ -490,8 +525,10 @@ public class Localizer {
|
|||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
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.
BIN
util/Util.class
BIN
util/Util.class
Binary file not shown.
Loading…
Reference in New Issue