Introducing the main window, yet more tweaks to localization support

This commit is contained in:
2020-08-01 18:28:47 -05:00
parent 7c7a52c113
commit 73c8245eff
29 changed files with 382 additions and 83 deletions
+32 -10
View File
@@ -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);
}
}
+218
View File
@@ -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
View File
@@ -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));
}
}
+12
View File
@@ -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;