Introducing utility library, implementing Localizer
This commit is contained in:
parent
99dbc9f0cb
commit
5ee08a4d93
|
@ -0,0 +1,6 @@
|
|||
app {
|
||||
title {
|
||||
empty PVB Emulator
|
||||
loaded {app.filename} - {app.title.empty}
|
||||
}
|
||||
}
|
12
makefile
12
makefile
|
@ -19,7 +19,7 @@ default:
|
|||
@echo
|
||||
@echo "Usage: make <recipe>"
|
||||
@echo " Package recipes:"
|
||||
@echo " build Produces a .jar and deletes all intermediate files"
|
||||
@echo " bundle Produces a .jar and deletes all intermediate files"
|
||||
@echo " clean Deletes all output files"
|
||||
@echo " core Check the native core library for style errors"
|
||||
@echo " desktop Compiles the Java desktop application"
|
||||
|
@ -39,8 +39,8 @@ default:
|
|||
###############################################################################
|
||||
|
||||
# Perform a full build process
|
||||
.PHONY: build
|
||||
build:
|
||||
.PHONY: bundle
|
||||
bundle:
|
||||
@make -s core
|
||||
@make -s desktop
|
||||
@make -s native
|
||||
|
@ -67,7 +67,7 @@ core:
|
|||
.PHONY: desktop
|
||||
desktop: clean_desktop
|
||||
@echo " Compiling Java desktop application"
|
||||
@javac -sourcepath src/desktop -d . src/desktop/Main.java
|
||||
@javac -sourcepath src/desktop -d . -Xlint:unchecked src/desktop/Main.java
|
||||
|
||||
# Build all native modules
|
||||
.PHONY: native
|
||||
|
@ -83,12 +83,12 @@ pack:
|
|||
$(eval jarname = "pvbemu_`date +%Y%m%d`.jar")
|
||||
@echo " Bundling into $(jarname)"
|
||||
@jar -cfe $(jarname) Main *.class \
|
||||
locale native src vue makefile license.txt
|
||||
locale native src util vue makefile license.txt
|
||||
|
||||
# Delete only Java .class files
|
||||
.PHONY: clean_desktop
|
||||
clean_desktop:
|
||||
@rm -r -f *.class vue
|
||||
@rm -r -f *.class util vue
|
||||
|
||||
# Delete everything but the .jar
|
||||
.PHONY: clean_most
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
import util.*;
|
||||
|
||||
// Desktop application primary class
|
||||
public class Main {
|
||||
|
||||
// Program entry point
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello, world!");
|
||||
var loc = new Localizer();
|
||||
loc.set(Util.textRead("locale/en_US.txt"));
|
||||
loc.put("app.filename", "wario.vb");
|
||||
var window = new JFrame();
|
||||
loc.add(window, "app.title.loaded");
|
||||
window.setVisible(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,585 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.locks.*;
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
// Background timer using either a realtime clock or audio for timing
|
||||
public class FrameTimer {
|
||||
|
||||
// Instance fields
|
||||
private Callback callback; // Event handler
|
||||
private double rate; // Frame rate in seconds
|
||||
private int source; // Timing source
|
||||
private int state; // Operation state
|
||||
|
||||
// Audio fields
|
||||
private int block; // Current byte buffer index
|
||||
private byte[][] blocks; // Reusable byte buffers
|
||||
private double buffer; // Buffer length in seconds
|
||||
private int channels; // Number channels
|
||||
private double delay; // Delay in seconds
|
||||
private int frames; // Audio frames per timer frame
|
||||
private SourceDataLine line; // Output line
|
||||
private int sampleRate; // Rate in samples per second
|
||||
private boolean written; // Samples have been written this frame
|
||||
private LinkedBlockingQueue<byte[]> queue; // Sample queue
|
||||
|
||||
// Threading fields
|
||||
private Thread audio; // Background audio processing
|
||||
private CyclicBarrier cycAudio; // Synchronization with audio thread
|
||||
private CyclicBarrier cycTimer; // Synchronization with timer thread
|
||||
private ReentrantLock lock; // Allows callback to control the timer
|
||||
private Thread timer; // Background timer processing
|
||||
|
||||
// Type for timer event handler
|
||||
public interface Callback {
|
||||
void onFrame(FrameTimer source);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constants //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Sources
|
||||
public static final int CLOCK = 0;
|
||||
public static final int AUDIO = 1;
|
||||
|
||||
// States
|
||||
public static final int STOPPED = 0;
|
||||
public static final int RUNNING = 1;
|
||||
public static final int PAUSED = 2;
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public FrameTimer() {
|
||||
buffer = 0.05;
|
||||
cycAudio = new CyclicBarrier(2);
|
||||
cycTimer = new CyclicBarrier(2);
|
||||
channels = 2;
|
||||
delay = 0.1;
|
||||
lock = new ReentrantLock();
|
||||
queue = new LinkedBlockingQueue<byte[]>();
|
||||
rate = 1;
|
||||
sampleRate = 44100;
|
||||
state = STOPPED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Retrieve the audio buffer size
|
||||
public synchronized double getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Retrieve the number of audio channels
|
||||
public synchronized int getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
// Retrieve the audio delay
|
||||
public synchronized double getDelay() {
|
||||
return delay;
|
||||
}
|
||||
|
||||
// Calculate the number of audio frames per timer frame
|
||||
public synchronized int getFrames() {
|
||||
return audioFrames(rate);
|
||||
}
|
||||
|
||||
// Retrieve the frame rate
|
||||
public synchronized double getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
// Retrieve the audio sampling rate
|
||||
public synchronized int getSampleRate() {
|
||||
return sampleRate;
|
||||
}
|
||||
|
||||
// Retrieve the timing source
|
||||
public synchronized int getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
// Retrieve the operation state
|
||||
public synchronized int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Pause the timer
|
||||
public synchronized boolean pause() {
|
||||
boolean isTimer = Thread.currentThread() == timer;
|
||||
|
||||
// The timer thread already owns the lock
|
||||
if (isTimer && lock.isHeldByCurrentThread()) {
|
||||
if (state == RUNNING)
|
||||
state = PAUSED;
|
||||
return state == PAUSED;
|
||||
}
|
||||
|
||||
// Obtain the lock
|
||||
lock.lock();
|
||||
|
||||
// Invalid state
|
||||
if (state != RUNNING) {
|
||||
lock.unlock();
|
||||
return state == PAUSED;
|
||||
}
|
||||
|
||||
// Configure state
|
||||
state = PAUSED;
|
||||
|
||||
// Pause timer thread
|
||||
if (!isTimer) try {
|
||||
timer.interrupt();
|
||||
lock.unlock();
|
||||
cycTimer.await();
|
||||
} catch (Exception e) { }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resume running the paused timer
|
||||
public synchronized boolean resume() {
|
||||
boolean isTimer = Thread.currentThread() == timer;
|
||||
|
||||
// The timer thread already owns the lock
|
||||
if (isTimer && lock.isHeldByCurrentThread()) {
|
||||
if (state == PAUSED)
|
||||
state = RUNNING;
|
||||
return state == RUNNING;
|
||||
}
|
||||
|
||||
// Obtain the lock
|
||||
lock.lock();
|
||||
|
||||
// Invalid state
|
||||
if (state != PAUSED) {
|
||||
lock.unlock();
|
||||
return state == RUNNING;
|
||||
}
|
||||
|
||||
// Configure state
|
||||
state = RUNNING;
|
||||
|
||||
// Resume timer thread
|
||||
if (!isTimer) try {
|
||||
lock.unlock();
|
||||
cycTimer.await();
|
||||
} catch (Exception e) { }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify the audio buffer length
|
||||
public synchronized double setBuffer(double buffer) {
|
||||
return this.buffer = state == STOPPED && buffer > 0 ?
|
||||
buffer : this.buffer;
|
||||
}
|
||||
|
||||
// Specify the callback handler
|
||||
public synchronized boolean setCallback(Callback callback) {
|
||||
if (state != STOPPED)
|
||||
return false;
|
||||
this.callback = callback;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify the number of audio channels
|
||||
public synchronized int setChannels(int channels) {
|
||||
return this.channels = state == STOPPED && (channels - 1 & ~1) != 0 ?
|
||||
channels : this.channels;
|
||||
}
|
||||
|
||||
// Specify the audio delay
|
||||
public synchronized double setDelay(double delay) {
|
||||
return this.delay = state == STOPPED && delay > 0 ? delay : this.delay;
|
||||
}
|
||||
|
||||
// Specify the frame rate
|
||||
public synchronized double setRate(double rate) {
|
||||
return this.rate = state == STOPPED && rate > 0 ? rate : this.rate;
|
||||
}
|
||||
|
||||
// Specify the audio sampling rate
|
||||
public synchronized int setSampleRate(int sampleRate) {
|
||||
return this.sampleRate = state == STOPPED && sampleRate > 0 ?
|
||||
sampleRate : this.sampleRate;
|
||||
}
|
||||
|
||||
// Begin timer operations
|
||||
public synchronized boolean start(int source) {
|
||||
|
||||
// Error checking
|
||||
if (source != CLOCK && source != AUDIO || state != STOPPED ||
|
||||
callback == null || Thread.currentThread() == timer ||
|
||||
source == AUDIO && buffer > delay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Audio processing
|
||||
if (source == AUDIO) try {
|
||||
|
||||
// Open the output line
|
||||
AudioFormat fmt =
|
||||
new AudioFormat(sampleRate, 16, channels, true, false);
|
||||
line = AudioSystem.getSourceDataLine(fmt);
|
||||
line.open(fmt);
|
||||
|
||||
// Configure audio fields
|
||||
frames = audioFrames(rate);
|
||||
block = 0;
|
||||
blocks = new byte[(int) Math.ceil(delay / rate)]
|
||||
[frames * channels * 2];
|
||||
}
|
||||
|
||||
// Could not open the audio line
|
||||
catch (Exception e) {
|
||||
line = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure state
|
||||
this.source = source;
|
||||
state = RUNNING;
|
||||
|
||||
// Spawn and start timer thread
|
||||
cycTimer.reset();
|
||||
timer = new Thread(()->timer());
|
||||
timer.setDaemon(true);
|
||||
timer.start();
|
||||
|
||||
// Spawn and start audio thread
|
||||
if (source == AUDIO) {
|
||||
cycAudio.reset();
|
||||
audio = new Thread(()->audio());
|
||||
audio.setDaemon(true);
|
||||
audio.start();
|
||||
}
|
||||
|
||||
// Synchronize with timer thread
|
||||
try { cycTimer.await(); } catch (Exception e) { }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// End timer operations
|
||||
public synchronized boolean stop() {
|
||||
boolean isTimer = Thread.currentThread() == timer;
|
||||
|
||||
// The timer thread already owns the lock
|
||||
if (isTimer && lock.isHeldByCurrentThread())
|
||||
return true;
|
||||
|
||||
// Obtain the lock
|
||||
lock.lock();
|
||||
|
||||
// Invalid state
|
||||
if (state == STOPPED) {
|
||||
lock.unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Configure state
|
||||
boolean paused = state == PAUSED;
|
||||
state = STOPPED;
|
||||
|
||||
// Stop timer thread
|
||||
if (!isTimer) try {
|
||||
if (!paused) timer.interrupt();
|
||||
lock.unlock();
|
||||
if ( paused) cycTimer.await();
|
||||
cycTimer.await();
|
||||
} catch (Exception e) { }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write audio samples as bytes to output
|
||||
public boolean write(byte[] samples, int offset) {
|
||||
int size = frames * channels * 2;
|
||||
|
||||
// Error checking
|
||||
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
||||
samples == null || offset < 0 || samples.length < offset + size)
|
||||
return false;
|
||||
|
||||
// Add a new sample block to the queue
|
||||
block = (block + 1) % blocks.length;
|
||||
byte[] block = blocks[this.block];
|
||||
System.arraycopy(samples, offset, block, 0, size);
|
||||
queue.offer(block);
|
||||
return written = true;
|
||||
}
|
||||
|
||||
// Write audio samples as shorts to output
|
||||
public boolean write(short[] samples, int offset) {
|
||||
int size = frames * channels;
|
||||
|
||||
// Error checking
|
||||
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
||||
samples == null || offset < 0 || samples.length < offset + size)
|
||||
return false;
|
||||
|
||||
// Encode the samples as bytes
|
||||
block = (block + 1) % blocks.length;
|
||||
byte[] block = blocks[this.block];
|
||||
for (int src = 0, dest = 0; src < size; src++) {
|
||||
short sample = samples[offset + src];
|
||||
block[dest++] = (byte) sample;
|
||||
block[dest++] = (byte) (sample >> 8);
|
||||
}
|
||||
queue.offer(block);
|
||||
return written = true;
|
||||
}
|
||||
|
||||
// Write audio samples as floats to output (range -1 to +1)
|
||||
public boolean write(float[] samples, int offset) {
|
||||
int size = frames * channels;
|
||||
|
||||
// Error checking
|
||||
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
||||
samples == null || offset < 0 || samples.length < offset + size)
|
||||
return false;
|
||||
|
||||
// Encode the samples as bytes
|
||||
block = (block + 1) % blocks.length;
|
||||
byte[] block = blocks[this.block];
|
||||
for (int src = 0, dest = 0; src < size; src++) {
|
||||
short sample = (short) Math.round(32767 *
|
||||
Math.min(1, Math.max(-1, samples[offset + src])) );
|
||||
block[dest++] = (byte) sample;
|
||||
block[dest++] = (byte) (sample >> 8);
|
||||
}
|
||||
queue.offer(block);
|
||||
return written = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Private Methds //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Calculate the number of audio sampling frames in some number of seconds
|
||||
private int audioFrames(double seconds) {
|
||||
return Math.max(1, (int) Math.round(seconds * sampleRate));
|
||||
}
|
||||
|
||||
// Handler for pause operations -- invoked by timer thread
|
||||
private long onPause(long reference, boolean isCallback) {
|
||||
|
||||
// Track the current time
|
||||
if (source == CLOCK)
|
||||
reference = System.nanoTime() - reference;
|
||||
|
||||
// Pause audio thread
|
||||
else try {
|
||||
audio.interrupt();
|
||||
line.stop();
|
||||
cycAudio.await();
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Synchronization
|
||||
try {
|
||||
if (!isCallback)
|
||||
cycTimer.await(); // Synchronize with invoking thread
|
||||
else lock.unlock();
|
||||
cycTimer.await(); // Wait for resume() or stop()
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Calculate a new reference time
|
||||
if (source == CLOCK)
|
||||
reference = System.nanoTime() - reference;
|
||||
|
||||
// Unpause audio thread
|
||||
else try { cycAudio.await(); } catch (Exception e) { }
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
// Handler for stop operations -- invoked by timer thread
|
||||
private int onStop(boolean paused, boolean isCallback) {
|
||||
|
||||
// Stop the audio thread
|
||||
if (source == AUDIO) try {
|
||||
audio.interrupt();
|
||||
line.stop();
|
||||
cycAudio.await();
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Synchronization
|
||||
try {
|
||||
if (!isCallback || paused)
|
||||
cycTimer.await(); // Synchronize with invoking thread
|
||||
else lock.unlock();
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Cleanup
|
||||
blocks = null;
|
||||
timer = null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Thread Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Audio thread entry point
|
||||
private void audio() {
|
||||
|
||||
// Synchronize with timer thread
|
||||
try { cycAudio.await(); } catch (Exception e) { }
|
||||
|
||||
// Initialize working variables
|
||||
byte[] block = new byte[audioFrames(delay ) * channels * 2];
|
||||
byte[] buffer = new byte[audioFrames(this.buffer) * channels * 2];
|
||||
int blockPos = 0;
|
||||
int bufferPos = -buffer.length; // Less than 0 means not full
|
||||
|
||||
// Audio processing
|
||||
line.start();
|
||||
for (;;) {
|
||||
|
||||
// Fill the buffer with samples, blocking until full
|
||||
while (bufferPos < 0) {
|
||||
|
||||
// Load bytes from the current block
|
||||
if (blockPos < block.length) {
|
||||
int size = Math.min(block.length - blockPos, -bufferPos);
|
||||
System.arraycopy(block, blockPos, buffer,
|
||||
bufferPos + buffer.length, size);
|
||||
blockPos += size;
|
||||
bufferPos += size;
|
||||
}
|
||||
|
||||
// Fetch a new sample block, blocking until one is available
|
||||
else try {
|
||||
block = queue.take();
|
||||
blockPos = 0;
|
||||
}
|
||||
|
||||
// The timer state has changed
|
||||
catch (Exception e) {
|
||||
audio.interrupt(); // take() clears interrupt status
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Send samples to the output, blocking until sent
|
||||
if (!audio.isInterrupted()) {
|
||||
bufferPos +=
|
||||
line.write(buffer, bufferPos, buffer.length - bufferPos);
|
||||
if (bufferPos == buffer.length)
|
||||
bufferPos = -buffer.length;
|
||||
}
|
||||
|
||||
// Check for changes to the timer state
|
||||
if (state == RUNNING)
|
||||
continue;
|
||||
Thread.interrupted();
|
||||
|
||||
// Timer has paused
|
||||
if (state == PAUSED) try {
|
||||
cycAudio.await(); // Synchronize with timer thread
|
||||
cycAudio.await(); // Wait for resume() or stop()
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Timer has stopped
|
||||
if (state == STOPPED) {
|
||||
try { line.close(); } catch (Exception e) { }
|
||||
try { cycAudio.await(); } catch (Exception e) { }
|
||||
audio = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Resume playback
|
||||
line.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Timer thread entry point
|
||||
private int timer() {
|
||||
|
||||
// Synchronize with other threads
|
||||
try {
|
||||
if (source == AUDIO)
|
||||
cycAudio.await(); // Synchronize with audio thread
|
||||
cycTimer.await(); // Synchronize with parent thread
|
||||
} catch (Exception e) { }
|
||||
|
||||
// Initialize working variables
|
||||
byte[] empty = new byte[frames * channels * 2];
|
||||
long reference = source == AUDIO ? 0 : System.nanoTime();
|
||||
long target = source == AUDIO ? audioFrames(rate) :
|
||||
Math.max(1, (long) Math.round(rate * 1000000000L));
|
||||
|
||||
// Timer processing
|
||||
for (boolean first = true;; first = false) {
|
||||
long current = source == CLOCK ? System.nanoTime() :
|
||||
line.getLongFramePosition();
|
||||
long remain = target - current + reference;
|
||||
|
||||
// Processing on all frames but the first
|
||||
if (!first) {
|
||||
|
||||
// Wait until the next frame interval
|
||||
if (remain > 0) try {
|
||||
if (source == AUDIO)
|
||||
remain = remain * 1000000000L / sampleRate;
|
||||
remain = Math.max(1000000, remain);
|
||||
Thread.sleep(remain / 1000000, (int) (remain % 1000000));
|
||||
continue;
|
||||
} catch (Exception e) { timer.interrupt(); }
|
||||
|
||||
// Another thread configured the timer state
|
||||
if (Thread.interrupted()) {
|
||||
if (state == PAUSED)
|
||||
reference = onPause(reference, false);
|
||||
if (state == STOPPED)
|
||||
return onStop(false, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Invoke the event callback
|
||||
written = false;
|
||||
callback.onFrame(this);
|
||||
if (source == AUDIO && !written)
|
||||
queue.offer(empty);
|
||||
|
||||
// The callback configured the timer state
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
boolean paused = state == PAUSED;
|
||||
if (state == PAUSED)
|
||||
reference = onPause(reference, true);
|
||||
if (state == STOPPED)
|
||||
return onStop(paused, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track processed frame time
|
||||
reference = current + remain;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.io.*;
|
||||
|
||||
// Little-endian implementation of DataInputStream
|
||||
public class LEDataInputStream extends FilterInputStream {
|
||||
|
||||
// Instance fields
|
||||
private DataInputStream data; // Data-decoding stream
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public LEDataInputStream(InputStream stream) {
|
||||
super(stream = stream instanceof LEDataInputStream ?
|
||||
((LEDataInputStream) stream).in : stream);
|
||||
data = stream instanceof DataInputStream ?
|
||||
(DataInputStream) stream : new DataInputStream(stream);
|
||||
}
|
||||
|
||||
// Byte array constructor
|
||||
public LEDataInputStream(byte[] data) {
|
||||
this(new ByteArrayInputStream(data));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Reads a boolean from the underlying input stream
|
||||
public boolean readBoolean() throws IOException {
|
||||
return data.readByte() != 0;
|
||||
}
|
||||
|
||||
// Reads an array of booleans from the underlying input stream
|
||||
public boolean[] readBooleans(int count) throws IOException {
|
||||
return readBooleans(new boolean[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of booleans from the underlying input stream
|
||||
public boolean[] readBooleans(boolean[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readBoolean();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a byte from the underlying input stream
|
||||
public byte readByte() throws IOException {
|
||||
return data.readByte();
|
||||
}
|
||||
|
||||
// Reads an array of bytes from the underlying input stream
|
||||
public byte[] readBytes(int count) throws IOException {
|
||||
return readBytes(new byte[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of bytes from the underlying input stream
|
||||
public byte[] readBytes(byte[] v, int offset, int length)
|
||||
throws IOException {
|
||||
data.read(v, offset, length);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a character from the underlying input stream
|
||||
public char readChar() throws IOException {
|
||||
return (char) Short.reverseBytes(data.readShort());
|
||||
}
|
||||
|
||||
// Reads an array of characters from the underlying input stream
|
||||
public char[] readChars(int count) throws IOException {
|
||||
return readChars(new char[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of characters from the underlying input stream
|
||||
public char[] readChars(char[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readChar();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a double from the underlying input stream
|
||||
public double readDouble() throws IOException {
|
||||
return readDouble(false);
|
||||
}
|
||||
|
||||
// Reads a double from the underlying input stream
|
||||
public double readDouble(boolean finite) throws IOException {
|
||||
long bits = readLong();
|
||||
double ret = Double.longBitsToDouble(bits);
|
||||
if (finite && !Double.isFinite(ret)) throw new RuntimeException(
|
||||
String.format("Non-finite double value 0x%016X", bits));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Reads an array of doubles from the underlying input stream
|
||||
public double[] readDoubles(int count) throws IOException {
|
||||
return readDoubles(new double[count], 0, count, false);
|
||||
}
|
||||
|
||||
// Reads an array of doubles from the underlying input stream
|
||||
public double[] readDoubles(int count, boolean finite) throws IOException {
|
||||
return readDoubles(new double[count], 0, count, finite);
|
||||
}
|
||||
|
||||
// Reads an array of doubles from the underlying input stream
|
||||
public double[] readDoubles(double[] v, int offset, int length)
|
||||
throws IOException {
|
||||
return readDoubles(v, offset, length, false);
|
||||
}
|
||||
|
||||
// Reads an array of doubles from the underlying input stream
|
||||
public double[] readDoubles(double[] v, int offset, int length,
|
||||
boolean finite) throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readDouble(finite);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a float from the underlying input stream
|
||||
public float readFloat() throws IOException {
|
||||
return readFloat(false);
|
||||
}
|
||||
|
||||
// Reads a float from the underlying input stream
|
||||
public float readFloat(boolean finite) throws IOException {
|
||||
int bits = readInt();
|
||||
float ret = Float.intBitsToFloat(bits);
|
||||
if (finite && !Float.isFinite(ret)) throw new RuntimeException(
|
||||
String.format("Non-finite float value 0x%08X", bits));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Reads an array of floats from the underlying input stream
|
||||
public float[] readFloats(int count) throws IOException {
|
||||
return readFloats(new float[count], 0, count, false);
|
||||
}
|
||||
|
||||
// Reads an array of floats from the underlying input stream
|
||||
public float[] readFloats(int count, boolean finite) throws IOException {
|
||||
return readFloats(new float[count], 0, count, finite);
|
||||
}
|
||||
|
||||
// Reads an array of floats from the underlying input stream
|
||||
public float[] readFloats(float[] v, int offset, int length)
|
||||
throws IOException {
|
||||
return readFloats(v, offset, length, false);
|
||||
}
|
||||
|
||||
// Reads an array of floats from the underlying input stream
|
||||
public float[] readFloats(float[] v, int offset, int length,
|
||||
boolean finite) throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readFloat(finite);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads an int from the underlying input stream
|
||||
public int readInt() throws IOException {
|
||||
return Integer.reverseBytes(data.readInt());
|
||||
}
|
||||
|
||||
// Reads an array of ints from the underlying input stream
|
||||
public int[] readInts(int count) throws IOException {
|
||||
return readInts(new int[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of ints from the underlying input stream
|
||||
public int[] readInts(int[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readInt();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a long from the underlying input stream
|
||||
public long readLong() throws IOException {
|
||||
return Long.reverseBytes(data.readLong());
|
||||
}
|
||||
|
||||
// Reads an array of longs from the underlying input stream
|
||||
public long[] readLongs(int count) throws IOException {
|
||||
return readLongs(new long[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of longs from the underlying input stream
|
||||
public long[] readLongs(long[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readLong();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a 24-bit integer from the underlying input stream
|
||||
public int readMiddle() throws IOException {
|
||||
int v = readShort() & 0xFFFF;
|
||||
return readByte() << 16 | v;
|
||||
}
|
||||
|
||||
// Reads an array of 24-bit integers from the underlying input stream
|
||||
public int[] readMiddles(int count) throws IOException {
|
||||
return readMiddles(new int[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of 24-bit integers from the underlying input stream
|
||||
public int[] readMiddles(int[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readMiddle();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads a short from the underlying input stream
|
||||
public short readShort() throws IOException {
|
||||
return Short.reverseBytes(data.readShort());
|
||||
}
|
||||
|
||||
// Reads an array of shorts from the underlying input stream
|
||||
public short[] readShorts(int count) throws IOException {
|
||||
return readShorts(new short[count], 0, count);
|
||||
}
|
||||
|
||||
// Reads an array of shorts from the underlying input stream
|
||||
public short[] readShorts(short[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
v[offset + x] = readShort();
|
||||
return v;
|
||||
}
|
||||
|
||||
// Reads from the stream in a modified UTF-8 string
|
||||
public String readUTF() throws IOException {
|
||||
return data.readUTF();
|
||||
}
|
||||
|
||||
// Advances forward in the stream some number of bytes
|
||||
public int skipBytes(int n) throws IOException {
|
||||
return data.skipBytes(n);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
|
||||
// Little-endian implementation of DataOutputStream
|
||||
public class LEDataOutputStream extends FilterOutputStream {
|
||||
|
||||
// Instance fields
|
||||
private DataOutputStream data; // Data-encoding stream
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public LEDataOutputStream() {
|
||||
this(new ByteArrayOutputStream());
|
||||
}
|
||||
|
||||
// Stream constructor
|
||||
public LEDataOutputStream(OutputStream stream) {
|
||||
super(stream = stream instanceof LEDataOutputStream ?
|
||||
((LEDataOutputStream) stream).out : stream);
|
||||
data = stream instanceof DataOutputStream ?
|
||||
(DataOutputStream) stream : new DataOutputStream(stream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Returns the current value of the underlying buffer
|
||||
public int size() {
|
||||
return out instanceof ByteArrayOutputStream ?
|
||||
((ByteArrayOutputStream) out).size() : -1;
|
||||
}
|
||||
|
||||
// Produce a byte array containing this stream's data
|
||||
public byte[] toByteArray() throws IOException {
|
||||
return out instanceof ByteArrayOutputStream ?
|
||||
((ByteArrayOutputStream) out).toByteArray() : null;
|
||||
}
|
||||
|
||||
// Writes a boolean to the underlying output stream as a 1-byte value
|
||||
public void writeBoolean(boolean v) throws IOException {
|
||||
data.writeBoolean(v);
|
||||
}
|
||||
|
||||
// Writes an array of booleans to the underlying output stream
|
||||
public void writeBooleans(boolean[] v) throws IOException {
|
||||
writeBooleans(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of booleans to the underlying output stream
|
||||
public void writeBooleans(boolean[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeBoolean(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes out a byte to the underlying output stream as a 1-byte value
|
||||
public void writeByte(int v) throws IOException {
|
||||
data.writeByte(v);
|
||||
}
|
||||
|
||||
// Writes out the string to the underlying output stream
|
||||
public void writeBytes(String s) throws IOException {
|
||||
data.writeBytes(s);
|
||||
}
|
||||
|
||||
// Writes an array of bytes to the underlying output stream
|
||||
public void writeBytes(byte[] v) throws IOException {
|
||||
writeBytes(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of bytes to the underlying output stream
|
||||
public void writeBytes(byte[] v, int offset, int length)
|
||||
throws IOException {
|
||||
data.write(v, offset, length);
|
||||
}
|
||||
|
||||
// Writes a character to the underlying output stream as a 2-byte value
|
||||
public void writeChar(int v) throws IOException {
|
||||
data.writeChar(Short.reverseBytes((short) v));
|
||||
}
|
||||
|
||||
// Writes a string to the underlying output stream
|
||||
public void writeChars(String s) throws IOException {
|
||||
writeChars(s.toCharArray());
|
||||
}
|
||||
|
||||
// Writes an array of characters to the underlying output stream
|
||||
public void writeChars(char[] v) throws IOException {
|
||||
writeChars(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of characters to the underlying output stream
|
||||
public void writeChars(char[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeChar(v[offset + x]);
|
||||
}
|
||||
|
||||
// Converts the double argument to a long using doubleToLongBits
|
||||
public void writeDouble(double v) throws IOException {
|
||||
data.writeLong(Long.reverseBytes(Double.doubleToLongBits(v)));
|
||||
}
|
||||
|
||||
// Writes an array of doubles to the underlying output stream
|
||||
public void writeDoubles(double[] v) throws IOException {
|
||||
writeDoubles(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of doubles to the underlying output stream
|
||||
public void writeDoubles(double[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeDouble(v[offset + x]);
|
||||
}
|
||||
|
||||
// Converts the float argument to an int using floatToIntBits
|
||||
public void writeFloat(float v) throws IOException {
|
||||
data.writeInt(Integer.reverseBytes(Float.floatToIntBits(v)));
|
||||
}
|
||||
|
||||
// Writes an array of floats to the underlying output stream
|
||||
public void writeFloats(float[] v) throws IOException {
|
||||
writeFloats(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of floats to the underlying output stream
|
||||
public void writeFloats(float[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeFloat(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes an int to the underlying output stream as four bytes
|
||||
public void writeInt(int v) throws IOException {
|
||||
data.writeInt(Integer.reverseBytes(v));
|
||||
}
|
||||
|
||||
// Writes an array of ints to the underlying output stream
|
||||
public void writeInts(int[] v) throws IOException {
|
||||
writeInts(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of ints to the underlying output stream
|
||||
public void writeInts(int[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeInt(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes an int to the underlying output stream as three bytes
|
||||
public void writeMiddle(int v) throws IOException {
|
||||
writeShort((short) v );
|
||||
writeByte ((byte ) (v >> 16));
|
||||
}
|
||||
|
||||
// Writes an array of 24-bit integers to the underlying output stream
|
||||
public void writeMiddles(int[] v) throws IOException {
|
||||
writeMiddles(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of 24-bit integers to the underlying output stream
|
||||
public void writeMiddles(int[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeMiddle(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes a long to the underlying output stream as eight bytes
|
||||
public void writeLong(long v) throws IOException {
|
||||
data.writeLong(Long.reverseBytes(v));
|
||||
}
|
||||
|
||||
// Writes an array of longs to the underlying output stream
|
||||
public void writeLong(long[] v) throws IOException {
|
||||
writeLongs(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of longs to the underlying output stream
|
||||
public void writeLongs(long[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeLong(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes a short to the underlying output stream as two bytes
|
||||
public void writeShort(int v) throws IOException {
|
||||
data.writeShort(Short.reverseBytes((short) v));
|
||||
}
|
||||
|
||||
// Writes an array of shorts to the underlying output stream
|
||||
public void writeShorts(short[] v) throws IOException {
|
||||
writeShorts(v, 0, v.length);
|
||||
}
|
||||
|
||||
// Writes an array of shorts to the underlying output stream
|
||||
public void writeShorts(short[] v, int offset, int length)
|
||||
throws IOException {
|
||||
for (int x = 0; x < length; x++)
|
||||
writeShort(v[offset + x]);
|
||||
}
|
||||
|
||||
// Writes a string to the underlying output stream using modified UTF-8
|
||||
public void writeUTF(String str) throws IOException {
|
||||
data.writeUTF(str);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,433 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.*;
|
||||
import javax.swing.text.*;
|
||||
|
||||
// UI display text manager
|
||||
public class Localizer {
|
||||
|
||||
// Instance fields
|
||||
private HashMap<Object, Object> controls; // Control mapping
|
||||
private HashMap<String, String> messages; // Message dictionary
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constants //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Class reference for JComboBox<String>
|
||||
JComboBox<String> JCOMBOBOX = new JComboBox<String>();
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public Localizer() {
|
||||
controls = new HashMap<Object, Object>();
|
||||
messages = new HashMap<String, String>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Add a control to the collection
|
||||
public boolean add(Object control, Object key) {
|
||||
|
||||
// Error checking
|
||||
if (control == null || key == null)
|
||||
return false;
|
||||
|
||||
// Control takes a single string
|
||||
if (key instanceof String) {
|
||||
if (!(
|
||||
control instanceof AbstractButton ||
|
||||
control instanceof JFrame ||
|
||||
control instanceof JInternalFrame ||
|
||||
control instanceof JPanel || // TitledBorder
|
||||
control instanceof JTextComponent
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Control takes an array of strings
|
||||
else if (key instanceof String[]) {
|
||||
if (!(
|
||||
JCOMBOBOX.getClass().isAssignableFrom(control.getClass())
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Invalid control type
|
||||
else return false;
|
||||
|
||||
// Add the control to the collection
|
||||
controls.put(control, key);
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove all controls from being managed
|
||||
public void clearControls() {
|
||||
controls.clear();
|
||||
}
|
||||
|
||||
// Configure a dictionary entry
|
||||
public String put(String key, String value) {
|
||||
String ret = value == null ?
|
||||
messages.remove(key) :
|
||||
messages.put(key, value)
|
||||
;
|
||||
update();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Remove a control from the collection
|
||||
public boolean remove(Object control) {
|
||||
return controls.remove(control) != null;
|
||||
}
|
||||
|
||||
// Specify a message dictionary
|
||||
public void set(String text) {
|
||||
|
||||
// Pre-processing
|
||||
messages.clear();
|
||||
if (text == null) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure working variables
|
||||
var chars = text.replaceAll("\\r\\n?", "\n").toCharArray();
|
||||
int col = 1; // Current character on the current line
|
||||
int line = 1; // Current line in the file
|
||||
int start = 0; // Position of first character in key or value
|
||||
var stack = new Stack<String>(); // Nested key chain
|
||||
int state = 0; // Current parsing context
|
||||
|
||||
// Process all characters
|
||||
for (int x = 0; x < chars.length; x++, col++) {
|
||||
char c = chars[x];
|
||||
boolean end = x == chars.length - 1;
|
||||
String pos = line + ":" + col + ": ";
|
||||
boolean white = c == ' ' || c == '\t';
|
||||
|
||||
// Comment
|
||||
if (state == -1) {
|
||||
if (c != '\n' && !end)
|
||||
continue;
|
||||
col = 1;
|
||||
state = 0;
|
||||
line++;
|
||||
}
|
||||
|
||||
// Pre-key
|
||||
else if (state == 0) {
|
||||
|
||||
// Ignore leading whitespace
|
||||
if (white)
|
||||
continue;
|
||||
|
||||
// The line is a comment
|
||||
if (c == '#') {
|
||||
state = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of input has been reached
|
||||
if (end)
|
||||
break;
|
||||
|
||||
// The line is empty
|
||||
if (c == '\n') {
|
||||
col = 1;
|
||||
line++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Proceed to key
|
||||
start = x;
|
||||
state = 1;
|
||||
}
|
||||
|
||||
// Key
|
||||
else if (state == 1) {
|
||||
|
||||
// Any non-whitespace character is valid in a key
|
||||
if (!white && c != '\n' && !end)
|
||||
continue;
|
||||
|
||||
// Produce the key as a string
|
||||
String key = new String(chars, start, x - start).trim();
|
||||
|
||||
// Close a key group
|
||||
if (key.equals("}")) {
|
||||
|
||||
// Nesting error
|
||||
if (stack.size() == 0) throw new RuntimeException(
|
||||
pos + "Unexpected '}'");
|
||||
|
||||
// Syntax error
|
||||
outer: for (int y = x; y < chars.length; y++) {
|
||||
char h = chars[y];
|
||||
if (h == '\n') break;
|
||||
if (h != ' ' && h != '\t') throw new RuntimeException(
|
||||
pos + "Newline expected");
|
||||
}
|
||||
|
||||
// Proceed to key
|
||||
state = 0;
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Curly braces are not valid in keys
|
||||
if (key.contains("{") || key.contains("}"))
|
||||
throw new RuntimeException(
|
||||
line + ": Curly braces are not allowed in keys");
|
||||
|
||||
// Proceed to interim
|
||||
stack.push(key);
|
||||
state = 2;
|
||||
}
|
||||
|
||||
// Post-key, pre-value
|
||||
if (state == 2) {
|
||||
|
||||
// Ignore any whitespace between key and value
|
||||
if (white)
|
||||
continue;
|
||||
|
||||
// No value is present
|
||||
if (c == '\n') throw new RuntimeException(
|
||||
pos + "Unexpected newline");
|
||||
if (end) throw new RuntimeException(
|
||||
pos + "Unexpected end of input");
|
||||
|
||||
// Proceed to value
|
||||
start = x;
|
||||
state = 3;
|
||||
}
|
||||
|
||||
// Value
|
||||
if (state == 3) {
|
||||
|
||||
// Escape sequence
|
||||
if (c == '\\') {
|
||||
|
||||
// EOF
|
||||
if (x == chars.length - 1)
|
||||
throw new RuntimeException(
|
||||
pos + "Unexpected end of input");
|
||||
|
||||
// Cannot escape a newline
|
||||
if (chars[x + 1] == '\n')
|
||||
throw new RuntimeException(
|
||||
pos + "Unexpected newline");
|
||||
|
||||
// Skip the next character
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The end of the value has not yet been reached
|
||||
if (c != '\n' && !end)
|
||||
continue;
|
||||
|
||||
// Produce the value as a string
|
||||
if (end)
|
||||
x++;
|
||||
String value = new String(chars, start, x - start).trim();
|
||||
col = 1;
|
||||
state = 0;
|
||||
line++;
|
||||
|
||||
// Open a key group
|
||||
if (value.equals("{")) {
|
||||
state = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for nesting errors
|
||||
int depth = 0;
|
||||
for (int y = start; y < x; y++) {
|
||||
switch (chars[y]) {
|
||||
case '\\': y++; continue;
|
||||
case '{' : depth++; continue;
|
||||
case '}' : if (--depth == -1)
|
||||
throw new RuntimeException(
|
||||
line + ":" + (x - start + y + 1) +
|
||||
": Unexpected '}'"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (depth != 0) throw new RuntimeException(
|
||||
pos + "'}' expected");
|
||||
|
||||
// Produce the full key as a string
|
||||
var pkey = new StringBuilder();
|
||||
for (String item : stack)
|
||||
pkey.append((pkey.length() != 0 ? "." : "") + item);
|
||||
String key = pkey.toString();
|
||||
|
||||
// Check for duplicate keys
|
||||
String lkey = key.toLowerCase();
|
||||
if (messages.get(lkey) != null) throw new RuntimeException(
|
||||
(line-1)+ ": Key '" + key + "' has already been defined.");
|
||||
|
||||
// Add the pair to the dictionary
|
||||
messages.put(lkey, value);
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update all controls
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Private Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Process substitutions and escapes on a message for a given key
|
||||
private String evaluate(String key) {
|
||||
String ret = messages.get(key.toLowerCase());
|
||||
|
||||
// The topmost key does not exist in the dictionary
|
||||
if (ret == null)
|
||||
return null;
|
||||
|
||||
// Perform all substitutions
|
||||
outer: for (;;) {
|
||||
var chars = ret.toCharArray();
|
||||
int start = 0;
|
||||
|
||||
// Look for the first complete substitution
|
||||
for (int x = 0; x < chars.length; x++) {
|
||||
char c = chars[x];
|
||||
|
||||
// Escape sequence
|
||||
if (c == '\\') {
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Begin substitution
|
||||
if (c == '{')
|
||||
start = x + 1;
|
||||
|
||||
// Substitution has not yet ended
|
||||
if (c != '}')
|
||||
continue;
|
||||
|
||||
// Determine the substitution
|
||||
key = new String(chars, start, x - start);
|
||||
String value = messages.get(key.toLowerCase());
|
||||
if (value == null)
|
||||
value = "\\{" + key + "\\}";
|
||||
|
||||
// Apply the substitution to the message
|
||||
ret =
|
||||
new String(chars, 0, start - 1) +
|
||||
value +
|
||||
new String(chars, x + 1, chars.length - x - 1)
|
||||
;
|
||||
continue outer;
|
||||
}
|
||||
|
||||
// No further substitutions
|
||||
break;
|
||||
}
|
||||
|
||||
// Unescape all escape sequences
|
||||
var chars = ret.toCharArray();
|
||||
int length = 0;
|
||||
for (int x = 0; x < chars.length; x++, length++) {
|
||||
char c = chars[x];
|
||||
if (c == '\\') switch (c = chars[++x]) {
|
||||
case 'n': c = '\n'; break;
|
||||
case 't': c = '\t'; break;
|
||||
}
|
||||
chars[length] = c;
|
||||
}
|
||||
return new String(chars, 0, length);
|
||||
}
|
||||
|
||||
// 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((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(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
import javax.imageio.*;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.*;
|
||||
import javax.swing.event.*;
|
||||
import javax.swing.plaf.basic.*;
|
||||
|
||||
// General utility methods
|
||||
public final class Util {
|
||||
|
||||
// This class cannot be instantiated
|
||||
private Util() { }
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Types //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Event listener interfaces
|
||||
public interface OnClose { void call(WindowEvent e); }
|
||||
public interface OnClose2 { void call(InternalFrameEvent e); }
|
||||
public interface OnFocus { void call(FocusEvent e); }
|
||||
public interface OnKey { void call(KeyEvent e); }
|
||||
public interface OnMouse { void call(MouseEvent e); }
|
||||
public interface OnResize { void call(ComponentEvent e); }
|
||||
|
||||
// Data class for byte-order marks
|
||||
private static class BOM {
|
||||
byte[] mark;
|
||||
Charset set;
|
||||
BOM(byte[] m, Charset s) { mark = m; set = s; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constants //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Text file byte-order marks
|
||||
private static final BOM[] BOMS = {
|
||||
new BOM(new byte[] { -17, -69, -65 }, StandardCharsets.UTF_8 ),
|
||||
new BOM(new byte[] { - 2, - 1 }, StandardCharsets.UTF_16BE),
|
||||
new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE),
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Static Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Apply an alpha value to a Color object
|
||||
public static Color alpha(Color color, float alpha) {
|
||||
int a = (int) Math.round(alpha * 255);
|
||||
return color == null || a < 0 || a > 255 ? null :
|
||||
new Color(color.getRGB() & 0x00FFFFFF | a << 24, true);
|
||||
}
|
||||
|
||||
// Blend one color into another
|
||||
public static Color blend(Color front, Color back, float alpha) {
|
||||
|
||||
// Error checking
|
||||
if (front == null || back == null ||
|
||||
!Float.isFinite(alpha) || alpha < 0 || alpha > 1)
|
||||
return null;
|
||||
|
||||
// Decompose components
|
||||
var colors = new Color[] { front, back };
|
||||
var argb = new float[3][4];
|
||||
for (int x = 0; x < 2; x++) {
|
||||
int bits = colors[x].getRGB();
|
||||
for (int y = 3; y >= 0; y++, bits >>= 8)
|
||||
argb[x][y] = (bits & 0xFF) / 255.0f;
|
||||
}
|
||||
|
||||
// Combine the colors
|
||||
argb[2][0] = argb[0][0] + argb[1][0] * (1 - argb[0][0]);
|
||||
for (int x = 1; x < 4; x++)
|
||||
argb[2][x] = (
|
||||
argb[0][x] * argb[0][0] +
|
||||
argb[1][x] * argb[1][0] * (1 - argb[0][0])
|
||||
) / argb[2][0];
|
||||
|
||||
// Produce the resulting Color object
|
||||
int bits = 0;
|
||||
for (int x = 0; x < 4; x++)
|
||||
bits = bits << 8 | (int) Math.round(argb[2][x] * 255);
|
||||
return new Color(bits, true);
|
||||
}
|
||||
|
||||
// Read a file, first from disk, then from .jar
|
||||
public static byte[] fileRead(String filename) {
|
||||
InputStream stream = null;
|
||||
|
||||
// Open the file on disk
|
||||
try { stream = new FileInputStream(filename); }
|
||||
|
||||
// File on disk could not be opened, so get resource from .jar
|
||||
catch (Exception e) {
|
||||
stream = Util.class.getResourceAsStream("/" + filename);
|
||||
if (stream == null)
|
||||
return null; // Resource in .jar could not be found
|
||||
}
|
||||
|
||||
// Read the file data into memory
|
||||
byte[] data = null;
|
||||
try { data = stream.readAllBytes(); } catch (Exception e) { }
|
||||
try { stream.close(); } catch (Exception e) { }
|
||||
return data;
|
||||
}
|
||||
|
||||
// Read an image file as an icon
|
||||
public static Icon iconRead(String filename) {
|
||||
BufferedImage img = imageRead(filename);
|
||||
return img == null ? null : new ImageIcon(img);
|
||||
}
|
||||
|
||||
// Read an image file by filename
|
||||
public static BufferedImage imageRead(String filename) {
|
||||
try { return
|
||||
ImageIO.read(new ByteArrayInputStream(fileRead(filename)));
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// Read an image file by handle
|
||||
public static BufferedImage imageRead(File file) {
|
||||
try { return imageRead(file.getAbsolutePath()); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// Produce a WindowListener with a functional interface
|
||||
public static WindowListener onClose(OnClose close) {
|
||||
return new WindowListener() {
|
||||
public void windowActivated (WindowEvent e) { }
|
||||
public void windowClosed (WindowEvent e) { }
|
||||
public void windowDeactivated(WindowEvent e) { }
|
||||
public void windowDeiconified(WindowEvent e) { }
|
||||
public void windowIconified (WindowEvent e) { }
|
||||
public void windowOpened (WindowEvent e) { }
|
||||
public void windowClosing (WindowEvent e)
|
||||
{ if (close != null) close.call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce an InternalFrameListener with a functional interface
|
||||
public static InternalFrameListener onClose2(OnClose2 close) {
|
||||
return new InternalFrameListener() {
|
||||
public void internalFrameActivated (InternalFrameEvent e) { }
|
||||
public void internalFrameClosed (InternalFrameEvent e) { }
|
||||
public void internalFrameDeactivated(InternalFrameEvent e) { }
|
||||
public void internalFrameDeiconified(InternalFrameEvent e) { }
|
||||
public void internalFrameIconified (InternalFrameEvent e) { }
|
||||
public void internalFrameOpened (InternalFrameEvent e) { }
|
||||
public void internalFrameClosing (InternalFrameEvent e)
|
||||
{ if (close != null) close.call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce a FocusListener with functional interfaces
|
||||
public static FocusListener onFocus(OnFocus focus, OnFocus blur) {
|
||||
return new FocusListener() {
|
||||
public void focusGained(FocusEvent e)
|
||||
{ if (focus != null) focus.call(e); }
|
||||
public void focusLost (FocusEvent e)
|
||||
{ if (blur != null) blur .call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce a KeyListener with functional interfaces
|
||||
public static KeyListener onKey(OnKey down, OnKey up) {
|
||||
return new KeyListener() {
|
||||
public void keyTyped (KeyEvent e) { }
|
||||
public void keyPressed (KeyEvent e)
|
||||
{ if (down != null) down.call(e); }
|
||||
public void keyReleased(KeyEvent e)
|
||||
{ if (up != null) up .call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce a MouseMotionListener with a functional interface
|
||||
public static MouseMotionListener onMouseMove(OnMouse move, OnMouse drag) {
|
||||
return new MouseMotionListener() {
|
||||
public void mouseMoved (MouseEvent e)
|
||||
{ if (move != null) move.call(e); }
|
||||
public void mouseDragged(MouseEvent e)
|
||||
{ if (drag != null) drag.call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce a MouseListener with functional interfaces
|
||||
public static MouseListener onMouse(OnMouse down, OnMouse up) {
|
||||
return new MouseListener() {
|
||||
public void mouseClicked (MouseEvent e) { }
|
||||
public void mouseEntered (MouseEvent e) { }
|
||||
public void mouseExited (MouseEvent e) { }
|
||||
public void mousePressed (MouseEvent e)
|
||||
{ if (down != null) down.call(e); }
|
||||
public void mouseReleased(MouseEvent e)
|
||||
{ if (up != null) up .call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Produce a ComponentListener with a functional interface
|
||||
public static ComponentListener onResize(OnResize resize) {
|
||||
return new ComponentListener() {
|
||||
public void componentHidden (ComponentEvent e) { }
|
||||
public void componentMoved (ComponentEvent e) { }
|
||||
public void componentShown (ComponentEvent e) { }
|
||||
public void componentResized(ComponentEvent e)
|
||||
{ if (resize != null) resize.call(e); }
|
||||
};
|
||||
}
|
||||
|
||||
// Configure the Swing look-and-feel with the system theme
|
||||
public static boolean setSystemLAF() {
|
||||
try {
|
||||
UIManager.setLookAndFeel(
|
||||
UIManager.getSystemLookAndFeelClassName());
|
||||
return true;
|
||||
} catch (Exception e) { return false; }
|
||||
}
|
||||
|
||||
// Produce a JSplitPane with an un-styled divider
|
||||
public static JSplitPane splitPane(int orientation) {
|
||||
var split = new JSplitPane(orientation, null, null);
|
||||
split.setContinuousLayout(true);
|
||||
split.setDividerSize(2);
|
||||
split.setUI(new BasicSplitPaneUI() {
|
||||
public BasicSplitPaneDivider createDefaultDivider() {
|
||||
return new BasicSplitPaneDivider(this) {
|
||||
public void setBorder(Border b) { }
|
||||
};
|
||||
}
|
||||
});
|
||||
split.setBorder(null);
|
||||
return split;
|
||||
}
|
||||
|
||||
// Read a text file, accounting for BOMs and guaranteeing Unix line endings
|
||||
public static String textRead(String filename) {
|
||||
|
||||
// Read the file
|
||||
byte[] data = fileRead(filename);
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
// Configure working variables
|
||||
byte[] mark = null;
|
||||
Charset set = StandardCharsets.ISO_8859_1; // Default charset
|
||||
|
||||
// Check for a byte-order mark
|
||||
outer: for (BOM bom : BOMS) {
|
||||
if (data.length < bom.mark.length)
|
||||
continue;
|
||||
for (int x = 0; x < bom.mark.length; x++)
|
||||
if (data[x] != bom.mark[x])
|
||||
continue outer;
|
||||
mark = bom.mark;
|
||||
set = bom.set;
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the byte-order mark from the data
|
||||
if (mark != null) {
|
||||
byte[] temp = new byte[data.length - mark.length];
|
||||
System.arraycopy(data, mark.length, temp, 0, temp.length);
|
||||
data = temp;
|
||||
}
|
||||
|
||||
// Produce a string with LF-style (Unix) line endings
|
||||
return new String(data, set).replaceAll("\\r\\n?", "\n");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javax.xml.parsers.*;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
// Utility methods for managing XML documents
|
||||
public class XML {
|
||||
|
||||
// This class cannot be instantiated
|
||||
private XML() { }
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constants //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Parsing instance
|
||||
private static final DocumentBuilder XML_PARSER;
|
||||
|
||||
// Static initializer
|
||||
static {
|
||||
DocumentBuilder parser = null;
|
||||
try { parser =
|
||||
DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
} catch (Exception e) { }
|
||||
XML_PARSER = parser;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Static Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Retrieve an attribute value from an XML element
|
||||
public static String attribute(Element element, String attribute) {
|
||||
return element.hasAttribute(attribute) ?
|
||||
element.getAttribute(attribute) : null;
|
||||
}
|
||||
|
||||
// Retrieve the child elements of an XML node
|
||||
public static Element[] children(Node node) {
|
||||
var nodes = node.getChildNodes();
|
||||
int count = nodes.getLength();
|
||||
var ret = new ArrayList<Element>();
|
||||
for (int x = 0; x < count; x++) {
|
||||
node = nodes.item(x);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE)
|
||||
ret.add((Element) node);
|
||||
}
|
||||
return ret.toArray(new Element[ret.size()]);
|
||||
}
|
||||
|
||||
// Retrieve the first node matching the given hierarchy
|
||||
public static Element element(Node node, String hierarchy) {
|
||||
|
||||
// Error checking
|
||||
if (node == null || hierarchy == null)
|
||||
return null;
|
||||
|
||||
// Propagate down the hierarchy
|
||||
outer: for (String name : hierarchy.split("\\.")) {
|
||||
var nodes = node.getChildNodes();
|
||||
int count = nodes.getLength();
|
||||
for (int x = 0; x < count; x++) {
|
||||
node = nodes.item(x);
|
||||
if (
|
||||
node.getNodeType() == Node.ELEMENT_NODE &&
|
||||
node.getNodeName().equals(name)
|
||||
) continue outer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Element) node;
|
||||
}
|
||||
|
||||
// Retrieve all nodes matching the endpoint of a given hierarchy
|
||||
public static Element[] elements(Node node, String hierarchy) {
|
||||
|
||||
// Find the first matching node
|
||||
node = element(node, hierarchy);
|
||||
if (node == null)
|
||||
return new Element[0];
|
||||
|
||||
// Working variables
|
||||
var names = hierarchy.split("\\.");
|
||||
String name = names[names.length - 1];
|
||||
var ret = new ArrayList<Element>();
|
||||
|
||||
// Include all siblings with the same tag name
|
||||
ret.add((Element) node);
|
||||
for (;;) {
|
||||
node = node.getNextSibling();
|
||||
if (node == null)
|
||||
break;
|
||||
if (
|
||||
node.getNodeType() == Node.ELEMENT_NODE &&
|
||||
node.getNodeName().equals(name)
|
||||
) ret.add((Element) node);
|
||||
}
|
||||
|
||||
return ret.toArray(new Element[ret.size()]);
|
||||
}
|
||||
|
||||
// Read and parse an XML file
|
||||
public static Node read(String filename) {
|
||||
try { return XML_PARSER.parse(
|
||||
new ByteArrayInputStream(Util.fileRead(filename)));
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// Retrieve the text content of an element
|
||||
public static String text(Node node) {
|
||||
|
||||
// Error checking
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
// Find the child node called #text
|
||||
var nodes = node.getChildNodes();
|
||||
int count = nodes.getLength();
|
||||
for (int x = 0; x < count; x++) {
|
||||
node = nodes.item(x);
|
||||
if (node.getNodeType() != Node.TEXT_NODE)
|
||||
continue;
|
||||
String text = node.getNodeValue();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue