Introducing the main window, yet more tweaks to localization support

This commit is contained in:
Guy Perfect 2020-08-01 18:28:47 -05:00
parent 7c7a52c113
commit 73c8245eff
29 changed files with 382 additions and 83 deletions

Binary file not shown.

Binary file not shown.

BIN
images/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

View File

@ -6,6 +6,7 @@ locale {
# Main window # Main window
app { app {
title { title {
default PVB Emulator default PVB Emulator
mixed {ctrl.number} {ctrl.filename} - {app.title.default} mixed {ctrl.number} {ctrl.filename} - {app.title.default}
@ -16,6 +17,7 @@ app {
file { file {
(menu) File (menu) File
load_rom Load ROM... load_rom Load ROM...
new_window New window
exit Exit exit Exit
} }
@ -30,3 +32,11 @@ core {
windows-x86 Windows (32-bit) windows-x86 Windows (32-bit)
windows-x86_64 Windows (64-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.
}

View File

@ -84,7 +84,7 @@ pack:
$(eval jarname = "pvbemu_`date +%Y%m%d`.jar") $(eval jarname = "pvbemu_`date +%Y%m%d`.jar")
@echo " Bundling into $(jarname)" @echo " Bundling into $(jarname)"
@jar -cfe $(jarname) Main *.class \ @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 # Delete only Java .class files
.PHONY: clean_desktop .PHONY: clean_desktop

View File

@ -12,6 +12,7 @@ public class App {
// Instance fields // Instance fields
private Localizer.Locale[] locales; // Language translations private Localizer.Locale[] locales; // Language translations
private Localizer localizer; // UI localization manager private Localizer localizer; // UI localization manager
private ArrayList<Window> windows; // Application windows
@ -24,41 +25,55 @@ public class App {
// Instance fields // Instance fields
localizer = new Localizer(); localizer = new Localizer();
windows = new ArrayList<Window>();
// Additional processing // Additional processing
initLocales(); initLocales();
addWindow();
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Public Methods // // Package Methods //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Associate a control with the localizer // Add a new program window
public boolean addControl(Object control, Object key) { void addWindow() {
return localizer.add(control, key); windows.add(new Window(this));
windowsChanged();
} }
// Retrieve the currently active locale // Retrieve the currently active locale
public Localizer.Locale getLocale() { Localizer.Locale getLocale() {
return localizer.getLocale(); return localizer.getLocale();
} }
// Retrieve the localization manager
Localizer getLocalizer() {
return localizer;
}
// Retrieve a list of registered locales // Retrieve a list of registered locales
public Localizer.Locale[] listLocales() { Localizer.Locale[] listLocales() {
var ret = new Localizer.Locale[locales.length]; var ret = new Localizer.Locale[locales.length];
System.arraycopy(locales, 0, ret, 0, locales.length); System.arraycopy(locales, 0, ret, 0, locales.length);
return ret; return ret;
} }
// Remove a control from the localizer // Determine the number of a window in the collection
public boolean removeControl(Object control) { int numberOf(Window window) {
return localizer.remove(control); return windows.indexOf(window) + 1;
}
// Remove a program window
void removeWindow(Window window) {
windows.remove(window);
windowsChanged();
} }
// Specify a new locale // Specify a new locale
public void setLocale(Localizer.Locale locale) { void setLocale(Localizer.Locale locale) {
localizer.setLocale(locale); localizer.setLocale(locale);
} }
@ -110,4 +125,11 @@ public class App {
localizer.setLocale(locale != null ? locale : this.locales[0]); 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
src/desktop/app/Window.java Normal file
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"
);
}
}

View File

@ -10,9 +10,8 @@ import javax.swing.text.*;
public class Localizer { public class Localizer {
// Instance fields // Instance fields
private HashMap<Object, Object> controls; // Control mapping private HashMap<Object, Control> controls; // Control mapping
private Locale locale; // Current message store private Locale locale; // Current message store
private HashMap<Object, HashMap<String, String>> tags; // Control messages
@ -29,6 +28,18 @@ public class Localizer {
// Classes // // 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 // Locale container
public static class Locale implements Comparable<Locale> { public static class Locale implements Comparable<Locale> {
@ -265,8 +276,7 @@ public class Localizer {
// Default constructor // Default constructor
public Localizer() { public Localizer() {
controls = new HashMap<Object, Object>(); controls = new HashMap<Object, Control>();
tags = new HashMap<Object, HashMap<String, String>>();
} }
// Parsing constructor // Parsing constructor
@ -283,6 +293,14 @@ public class Localizer {
// Add a control to the collection // Add a control to the collection
public boolean add(Object control, Object key) { 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 // Error checking
if (control == null || key == null) if (control == null || key == null)
@ -290,6 +308,8 @@ public class Localizer {
// Control takes a single string // Control takes a single string
if (key instanceof String) { if (key instanceof String) {
// Type validation
if (!( if (!(
control instanceof AbstractButton || control instanceof AbstractButton ||
control instanceof JFrame || control instanceof JFrame ||
@ -297,22 +317,31 @@ public class Localizer {
control instanceof JPanel || // TitledBorder control instanceof JPanel || // TitledBorder
control instanceof JTextComponent control instanceof JTextComponent
)) return false; )) return false;
// Configure key
ctrl.key = (String) key;
} }
// Control takes an array of strings // Control takes an array of strings
else if (key instanceof String[]) { else if (key instanceof String[]) {
// Type validation
if (!( if (!(
JCOMBOBOX.getClass().isAssignableFrom(control.getClass()) JCOMBOBOX.getClass().isAssignableFrom(control.getClass())
)) return false; )) 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 // Invalid control type
else return false; else return false;
// Add the control to the collection // Add the control to the collection
controls.put(control, key); controls.put(control, ctrl);
tags .put(control, new HashMap<String, String>()); update(control);
update();
return true; return true;
} }
@ -321,6 +350,11 @@ public class Localizer {
controls.clear(); 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 // Retrieve the currently loaded locale
public Locale getLocale() { public Locale getLocale() {
return locale; return locale;
@ -328,21 +362,26 @@ public class Localizer {
// Configure a control tag // Configure a control tag
public String put(Object control, String key, String value) { 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; return null;
var tags = this.tags.get(control);
// Update the control's tags
key = key.toLowerCase(); key = key.toLowerCase();
String ret = value == null ? String ret = value == null ?
tags.remove(key) : ctrl.tags.remove(key) :
tags.put(key, value) ctrl.tags.put(key, value)
; ;
update();
// Refresh the text on the control
update(control);
return ret; return ret;
} }
// Remove a control from the collection // Remove a control from the collection
public boolean remove(Object control) { public boolean remove(Object control) {
tags.remove(control);
return controls.remove(control) != null; return controls.remove(control) != null;
} }
@ -358,7 +397,7 @@ public class Localizer {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Process substitutions and escapes on a message for a given key // 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 // No locale is loaded
if (locale == null) if (locale == null)
@ -395,7 +434,7 @@ public class Localizer {
// Determine the substitution // Determine the substitution
key = new String(chars, start, x - start); key = new String(chars, start, x - start);
String lkey = key.toLowerCase(); String lkey = key.toLowerCase();
String value = tags.get(control).get(lkey); String value = control == null ? null : control.tags.get(lkey);
if (value == null) if (value == null)
value = locale.messages.get(lkey); value = locale.messages.get(lkey);
if (value == null) if (value == null)
@ -430,68 +469,66 @@ public class Localizer {
// Update the text for all controls // Update the text for all controls
private void update() { private void update() {
for (var control : controls.keySet())
update(control);
}
// Process all controls // Update the text for a control
for (var control : controls.keySet()) { private void update(Object control) {
Object key = controls.get(control); var ctrl = controls.get(control);
String[] values = null; String[] keys = ctrl.key==null ? ctrl.keys : new String[]{ctrl.key};
String[] values = new String[keys.length];
// One string // Evaluate all messages
if (key instanceof String) for (int x = 0; x < keys.length; x++)
values = new String[] { evaluate(control, (String) key) }; values[x] = evaluate(ctrl, keys[x]);
// Multiple strings // Update the control's text
else { if (control instanceof AbstractButton)
String[] keys = (String[]) key; ((AbstractButton) control).setText (values[0]);
values = new String[keys.length]; if (control instanceof JFrame)
for (int x = 0; x < keys.length; x++) ((JFrame ) control).setTitle(values[0]);
values[x] = evaluate(control, keys[x]); if (control instanceof JInternalFrame)
} ((JInternalFrame) control).setTitle(values[0]);
if (control instanceof JTextComponent)
// Update the control's text ((JTextComponent) control).setText (values[0]);
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);
}
// 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));
} }
} }

View File

@ -117,6 +117,18 @@ public final class Util {
return new Color(bits, true); 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 // Read a file, first from disk, then from .jar
public static byte[] fileRead(String filename) { public static byte[] fileRead(String filename) {
InputStream stream = null; 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.

Binary file not shown.