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
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.
}

View File

@ -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

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
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 {
// 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));
}
}

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;

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.