Introducing the main window, yet more tweaks to localization support
This commit is contained in:
+32
-10
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+109
-72
@@ -10,9 +10,8 @@ import javax.swing.text.*;
|
||||
public class Localizer {
|
||||
|
||||
// Instance fields
|
||||
private HashMap<Object, Object> controls; // Control mapping
|
||||
private Locale locale; // Current message store
|
||||
private HashMap<Object, HashMap<String, String>> tags; // Control messages
|
||||
private HashMap<Object, Control> controls; // Control mapping
|
||||
private Locale locale; // Current message store
|
||||
|
||||
|
||||
|
||||
@@ -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,68 +469,66 @@ public class Localizer {
|
||||
|
||||
// Update the text for all controls
|
||||
private void update() {
|
||||
for (var control : controls.keySet())
|
||||
update(control);
|
||||
}
|
||||
|
||||
// Process all controls
|
||||
for (var control : controls.keySet()) {
|
||||
Object key = controls.get(control);
|
||||
String[] values = null;
|
||||
// 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];
|
||||
|
||||
// One string
|
||||
if (key instanceof String)
|
||||
values = new String[] { evaluate(control, (String) key) };
|
||||
// Evaluate all messages
|
||||
for (int x = 0; x < keys.length; x++)
|
||||
values[x] = evaluate(ctrl, keys[x]);
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
// Update the control's text
|
||||
if (control instanceof AbstractButton)
|
||||
((AbstractButton) control).setText (values[0]);
|
||||
if (control instanceof JFrame)
|
||||
((JFrame ) control).setTitle(values[0]);
|
||||
if (control instanceof JInternalFrame)
|
||||
((JInternalFrame) control).setTitle(values[0]);
|
||||
if (control instanceof JTextComponent)
|
||||
((JTextComponent) control).setText (values[0]);
|
||||
|
||||
// JPanel must be wrapped in a TitledBorder
|
||||
if (control instanceof JPanel) {
|
||||
var border = ((JPanel) control).getBorder();
|
||||
if (border instanceof TitledBorder)
|
||||
((TitledBorder) border).setTitle(values[0]);
|
||||
}
|
||||
|
||||
// Replace the contents of a JComboBox without firing events
|
||||
if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) {
|
||||
|
||||
// The type is explicitly verified above
|
||||
@SuppressWarnings("unchecked")
|
||||
var box = (JComboBox<String>) control;
|
||||
|
||||
// Configure working variables
|
||||
var action = box.getActionListeners();
|
||||
int index = box.getSelectedIndex();
|
||||
var item = box.getItemListeners();
|
||||
|
||||
// Remove event listeners
|
||||
for (var lst : action) box.removeActionListener(lst);
|
||||
for (var lst : item ) box.removeItemListener (lst);
|
||||
|
||||
// Update contents
|
||||
box.setModel(new DefaultComboBoxModel<String>(values));
|
||||
box.setSelectedIndex(index);
|
||||
|
||||
// Restore event listeners
|
||||
for (var lst : action) box.addActionListener(lst);
|
||||
for (var lst : item ) box.addItemListener (lst);
|
||||
}
|
||||
// Update the control's text
|
||||
if (control instanceof AbstractButton)
|
||||
((AbstractButton) control).setText (values[0]);
|
||||
if (control instanceof JFrame)
|
||||
((JFrame ) control).setTitle(values[0]);
|
||||
if (control instanceof JInternalFrame)
|
||||
((JInternalFrame) control).setTitle(values[0]);
|
||||
if (control instanceof JTextComponent)
|
||||
((JTextComponent) control).setText (values[0]);
|
||||
|
||||
// JPanel must be wrapped in a TitledBorder
|
||||
if (control instanceof JPanel) {
|
||||
var border = ((JPanel) control).getBorder();
|
||||
if (border instanceof TitledBorder)
|
||||
((TitledBorder) border).setTitle(values[0]);
|
||||
}
|
||||
|
||||
// Replace the contents of a JComboBox without firing events
|
||||
if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) {
|
||||
|
||||
// The type is explicitly verified above
|
||||
@SuppressWarnings("unchecked")
|
||||
var box = (JComboBox<String>) control;
|
||||
|
||||
// Configure working variables
|
||||
var action = box.getActionListeners();
|
||||
int index = box.getSelectedIndex();
|
||||
var item = box.getItemListeners();
|
||||
|
||||
// Remove event listeners
|
||||
for (var lst : action) box.removeActionListener(lst);
|
||||
for (var lst : item ) box.removeItemListener (lst);
|
||||
|
||||
// Update contents
|
||||
box.setModel(new DefaultComboBoxModel<String>(values));
|
||||
box.setSelectedIndex(index);
|
||||
|
||||
// Restore event listeners
|
||||
for (var lst : action) box.addActionListener(lst);
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user