1634 lines
42 KiB
Java
1634 lines
42 KiB
Java
package schule.ngb.zm;
|
|
|
|
import schule.ngb.zm.anim.Animation;
|
|
import schule.ngb.zm.layers.ColorLayer;
|
|
import schule.ngb.zm.layers.DrawingLayer;
|
|
import schule.ngb.zm.layers.ImageLayer;
|
|
import schule.ngb.zm.layers.ShapesLayer;
|
|
import schule.ngb.zm.util.io.ImageLoader;
|
|
import schule.ngb.zm.util.Log;
|
|
import schule.ngb.zm.util.tasks.TaskRunner;
|
|
|
|
import javax.swing.*;
|
|
import javax.swing.event.MouseInputListener;
|
|
import java.awt.*;
|
|
import java.awt.event.*;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.concurrent.*;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
/**
|
|
* Hauptklasse der Zeichenmaschine.
|
|
* <p>
|
|
* Projekte der Zeichenmaschine sollten als Unterklasse implementiert werden.
|
|
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der
|
|
* nötigen Komponenten.
|
|
*/
|
|
@SuppressWarnings( "unused" )
|
|
public class Zeichenmaschine extends Constants {
|
|
|
|
/**
|
|
* Gibt an, ob die Zeichenmaschine aus BlueJ heraus gestartet wurde.
|
|
*/
|
|
public static final boolean IN_BLUEJ;
|
|
|
|
static {
|
|
IN_BLUEJ = System.getProperty("java.class.path").contains("bluej");
|
|
}
|
|
|
|
/*
|
|
* Objektvariablen, die von Unterklassen benutzt werden können.
|
|
*/
|
|
|
|
/**
|
|
* Die Leinwand, auf die alles gezeichnet wird. Die Leinwand enthält
|
|
* {@link Layer Ebenen}, die einzelne Zeichnungen enthalten. Die Inhalte
|
|
* aller Ebenen werden einmal pro Frame auf die Hauptleinwand übertragen.
|
|
*/
|
|
protected Zeichenleinwand canvas;
|
|
|
|
/**
|
|
* Ebene mit der Hintergrundfarbe.
|
|
*/
|
|
protected static ColorLayer background;
|
|
|
|
/**
|
|
* Zeichenebene
|
|
*/
|
|
protected static DrawingLayer drawing;
|
|
|
|
/**
|
|
* Formenebene
|
|
*/
|
|
protected static ShapesLayer shapes;
|
|
|
|
////////
|
|
|
|
|
|
/*
|
|
* Interne Attribute zur Steuerung der Zeichenmaschine.
|
|
*/
|
|
//@formatter:off
|
|
|
|
/**
|
|
* Das Zeichenfenster der Zeichenmaschine
|
|
*/
|
|
protected Zeichenfenster frame;
|
|
|
|
// Aktueller Zustand der Zeichenmaschine.
|
|
|
|
/**
|
|
* Zustand der Zeichenmaschine insgesamt
|
|
*/
|
|
private Options.AppState state;
|
|
|
|
/**
|
|
* Zustand des update/draw Threads
|
|
*/
|
|
private Options.AppState updateState = Options.AppState.STOPPED;
|
|
|
|
/**
|
|
* Ob der Zeichenthread noch laufen soll, oder beendet.
|
|
*/
|
|
private boolean running;
|
|
|
|
private boolean terminateImediately = false;
|
|
|
|
/**
|
|
* Ob die ZM nach dem nächsten Frame pausiert werden soll.
|
|
*/
|
|
private boolean pause_pending = false;
|
|
|
|
/**
|
|
* Ob die ZM bei nicht überschriebener update() Methode stoppen soll,
|
|
* oder trotzdem weiterläuft.
|
|
*/
|
|
private final boolean run_once;
|
|
|
|
/**
|
|
* Aktuelle Frames pro Sekunde der Zeichenmaschine.
|
|
*/
|
|
private int framesPerSecondInternal;
|
|
|
|
/**
|
|
* Hauptthread der Zeichenmaschine.
|
|
*/
|
|
private final Thread mainThread;
|
|
|
|
/**
|
|
* Queue für geplante Aufgaben
|
|
*/
|
|
private final DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
|
|
|
|
/**
|
|
* Queue für abgefangene InputEvents
|
|
*/
|
|
private final BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
|
|
|
|
/**
|
|
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
|
|
* oder das Zeichenfenster weiter geöffnet bleibt.
|
|
*/
|
|
private boolean quitAfterShutdown = false;
|
|
|
|
// Mauszeiger
|
|
/**
|
|
* Cache für den unsichtbaren Mauszeiger, wenn {@link #hideCursor()}
|
|
* aufgerufen wurde.
|
|
*/
|
|
private Cursor invisibleCursor = null;
|
|
|
|
/**
|
|
* Ob der Mauszeiger derzeit sichtbar ist (bzw. sein sollte).
|
|
*/
|
|
protected boolean cursorVisible = true;
|
|
//@formatter:on
|
|
|
|
|
|
/**
|
|
* Erstellt eine neue Zeichenmaschine mit Standardwerten für Titel und
|
|
* Größe.
|
|
* <p>
|
|
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
|
|
* Details.
|
|
*/
|
|
public Zeichenmaschine() {
|
|
this(APP_NAME + " " + APP_VERSION);
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue Zeichenmaschine mit Standardwerten für Titel und
|
|
* Größe.
|
|
* <p>
|
|
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
|
|
* Details.
|
|
*
|
|
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
|
|
* Aufruf von {@code draw()}.
|
|
*/
|
|
public Zeichenmaschine( boolean run_once ) {
|
|
this(APP_NAME + " " + APP_VERSION, run_once);
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue Zeichenmaschine mit dem angegebene Titel und
|
|
* Standardwerten für die Größe.
|
|
* <p>
|
|
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
|
|
* Details.
|
|
*
|
|
* @param title Der Titel, der oben im Fenster steht.
|
|
*/
|
|
public Zeichenmaschine( String title ) {
|
|
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, true);
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue Zeichenmaschine mit dem angegebene Titel und
|
|
* Standardwerten für die Größe.
|
|
* <p>
|
|
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
|
|
* Details.
|
|
*
|
|
* @param title Der Titel, der oben im Fenster steht.
|
|
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
|
|
* Aufruf von {@code draw()}.
|
|
*/
|
|
public Zeichenmaschine( String title, boolean run_once ) {
|
|
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, run_once);
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue zeichenmaschine mit einer Leinwand der angegebenen
|
|
* Größe und dem angegebenen Titel.
|
|
* <p>
|
|
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
|
|
* Details.
|
|
*
|
|
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
|
|
* @param height Höhe der {@link Zeichenleinwand Zeichenleinwand}.
|
|
* @param title Der Titel, der oben im Fenster steht.
|
|
*/
|
|
public Zeichenmaschine( int width, int height, String title ) {
|
|
this(width, height, title, true);
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine neue zeichenmaschine mit einer Leinwand der angegebenen
|
|
* Größe und dem angegebenen Titel.
|
|
* <p>
|
|
* Die Zeichenmaschine öffnet automatisch ein Fenster mit einer
|
|
* {@link Zeichenleinwand}, die {@code width} Pixel breit und {@code height}
|
|
* Pixel hoch ist. Die Leinwand hat immer eine Mindestgröße von 100x100
|
|
* Pixeln und kann nicht größer als der aktuelle Bildschirm werden. Das
|
|
* Fenster bekommt den angegebenen Titel.
|
|
* <p>
|
|
* Falls {@code run_once} gleich {@code false} ist, werden
|
|
* {@link #update(double)} und {@link #draw()} entsprechend der
|
|
* {@link #framesPerSecond} kontinuierlich aufgerufen. Falls das Programm
|
|
* als Unterklasse der Zeichenmaschine verfasst wird, dann kann auch,
|
|
* {@code update(double)} überschrieben werden, damit die Maschine nicht
|
|
* automatisch beendet.
|
|
*
|
|
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
|
|
* @param height Höhe der {@link Zeichenleinwand Zeichenleinwand}.
|
|
* @param title Der Titel, der oben im Fenster steht.
|
|
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
|
|
* Aufruf von {@code draw()}.
|
|
*/
|
|
@SuppressWarnings("static-access")
|
|
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
|
|
LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
|
|
|
|
// Register Cmd+Q on macOS
|
|
if( Constants.MACOS ) {
|
|
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
|
|
}
|
|
|
|
// Erstellen der Leinwand
|
|
canvas = new Zeichenleinwand(width, height);
|
|
|
|
// Erstellen des Zeichenfensters
|
|
frame = createFrame(canvas, title);
|
|
|
|
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
|
|
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
|
|
this.canvasWidth = canvasBounds.width;
|
|
this.canvasHeight = canvasBounds.height;
|
|
java.awt.Rectangle displayBounds = frame.getScreenBounds();
|
|
this.screenWidth = displayBounds.width;
|
|
this.screenHeight = displayBounds.height;
|
|
|
|
// Die drei Standardebenen merken, für den einfachen Zugriff aus Unterklassen.
|
|
background = getBackgroundLayer();
|
|
drawing = getDrawingLayer();
|
|
shapes = getShapesLayer();
|
|
|
|
// FPS setzen
|
|
framesPerSecondInternal = DEFAULT_FPS;
|
|
this.run_once = run_once;
|
|
|
|
// Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen
|
|
// verändert werden soll.
|
|
// TODO: (ngb) Wann sollte settings() aufgerufen werden?
|
|
settings();
|
|
|
|
// Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören.
|
|
InputListener inputListener = new InputListener();
|
|
canvas.addMouseListener(inputListener);
|
|
canvas.addMouseMotionListener(inputListener);
|
|
canvas.addMouseWheelListener(inputListener);
|
|
canvas.addKeyListener(inputListener);
|
|
|
|
/*KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
|
|
@Override
|
|
public boolean dispatchKeyEvent( KeyEvent e ) {
|
|
enqueueEvent(e);
|
|
return false;
|
|
}
|
|
});*/
|
|
|
|
// Programm beenden, wenn Fenster geschlossen wird
|
|
frame.addWindowListener(new WindowAdapter() {
|
|
@Override
|
|
public void windowClosing( WindowEvent e ) {
|
|
if( isTerminated() ) {
|
|
quit(true);
|
|
} else {
|
|
exitNow();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fenster anzeigen
|
|
frame.centerFrame();
|
|
frame.setVisible(true);
|
|
|
|
// Nach dem Anzeigen kann die Pufferstrategie erstellt werden.
|
|
canvas.allocateBuffer();
|
|
|
|
// Erstellen des Haupt-Zeichenthreads.
|
|
running = true;
|
|
mainThread = new Zeichenthread();
|
|
|
|
//frame.requestFocusInWindow();
|
|
canvas.requestFocus();
|
|
|
|
// Fertig mit der Initialisierung!
|
|
state = Options.AppState.INITIALIZED;
|
|
// Los geht's ...
|
|
mainThread.start();
|
|
}
|
|
|
|
/**
|
|
* Erstellt ein neues Zeichenfesnter mit der aktuellen Konfiguration.
|
|
*
|
|
* @param title
|
|
*/
|
|
private Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
|
|
while( frame == null ) {
|
|
try {
|
|
TaskRunner.invokeLater(() -> {
|
|
Zeichenfenster.setLookAndFeel();
|
|
frame = new Zeichenfenster(canvas, title);
|
|
}).get();
|
|
} catch( InterruptedException e ) {
|
|
// Keep waiting
|
|
} catch( ExecutionException e ) {
|
|
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
/**
|
|
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
|
|
*/
|
|
public final void centerFrame() {
|
|
frame.centerFrame();
|
|
}
|
|
|
|
/**
|
|
* Aktiviert oder deaktiviert den Vollbildmodus für die Zeichenmaschine.
|
|
* <p>
|
|
* Der Vollbildmodus wird abhängig von {@code pEnable} entweder aktiviert
|
|
* oder deaktiviert. Wird die Zeichenmaschine in den Vollbildmodus versetzt,
|
|
* dann wird automatisch ein {@link KeyListener} aktiviert, der bei
|
|
* Betätigung der ESCAPE-Taste den Vollbildmodus verlässt. Wird der
|
|
* Vollbildmodus verlassen, wird die zuletzt gesetzte Fenstergröße
|
|
* wiederhergestellt.
|
|
*
|
|
* @param pEnable Wenn {@code true}, wird der Vollbildmodus aktiviert,
|
|
* ansonsten deaktiviert.
|
|
*/
|
|
public final void setFullscreen( boolean pEnable ) {
|
|
if( pEnable && !frame.isFullscreen() ) {
|
|
frame.setFullscreen(true);
|
|
|
|
canvasWidth = canvas.getWidth();
|
|
canvasHeight = canvas.getHeight();
|
|
|
|
if( frame.isFullscreen() )
|
|
fullscreenChanged();
|
|
} else if( !pEnable && frame.isFullscreen() ) {
|
|
frame.setFullscreen(false);
|
|
|
|
canvasWidth = canvas.getWidth();
|
|
canvasHeight = canvas.getHeight();
|
|
|
|
if( !frame.isFullscreen() )
|
|
fullscreenChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prüft, ob das Zeichenfenster im Vollbild läuft.
|
|
*
|
|
* @return {@code true}, wenn sich das Fesnter im Vollbildmodus befindet,
|
|
* {@code false} sonst.
|
|
*/
|
|
public boolean isFullscreen() {
|
|
return frame.isFullscreen();
|
|
}
|
|
|
|
/**
|
|
* Gibt den aktuellen {@link Options.AppState Zustand} der Zeichenmaschine
|
|
* zurück.
|
|
*
|
|
* @return Der Zustand der Zeichenmaschine.
|
|
*/
|
|
public Options.AppState getState() {
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Zeigt das Zeichenfenster an.
|
|
*/
|
|
public final void show() {
|
|
if( !frame.isVisible() ) {
|
|
frame.setVisible(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Versteckt das Zeichenfenster.
|
|
*/
|
|
public final void hide() {
|
|
if( frame.isVisible() ) {
|
|
frame.setVisible(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zeichnet die {@link Zeichenleinwand} neu und zeigt den aktuellen Inhalt
|
|
* im Zeichenfenster an.
|
|
*/
|
|
public final void redraw() {
|
|
if( state == Options.AppState.PAUSED
|
|
|| state == Options.AppState.TERMINATED ) {
|
|
draw();
|
|
}
|
|
render();
|
|
}
|
|
|
|
/**
|
|
* Zeigt den aktuellen Inhalt der {@link Zeichenleinwand}
|
|
* im Zeichenfenster an, ohne vorher {@link #draw()} aufzurufen.
|
|
*/
|
|
public final void render() {
|
|
canvas.render();
|
|
// canvas.invalidate();
|
|
// frame.repaint();
|
|
// hide();
|
|
// show();
|
|
}
|
|
|
|
/**
|
|
* Pausiert die Ausführung von {@link #update(double)} und {@link #draw()}
|
|
* nach dem nächsten vollständigen Frame.
|
|
* <p>
|
|
* Die Zeichenmaschine wechselt in den Zustand
|
|
* {@link Options.AppState#PAUSED}, sobald der aktuelle Frame beendet
|
|
* wurde.
|
|
*/
|
|
public final void pause() {
|
|
pause_pending = true;
|
|
}
|
|
|
|
/**
|
|
* Setzt die Ausführung der Zeichenmaschine fort, nachdem diese mit
|
|
* {@link #pause()} pausiert wurde.
|
|
*/
|
|
public final void resume() {
|
|
pause_pending = false;
|
|
if( state == Options.AppState.PAUSED ) {
|
|
state = Options.AppState.RUNNING;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prüft, ob die Zeichenmaschine gerade pausiert ist.
|
|
*
|
|
* @return
|
|
*/
|
|
public final boolean isPaused() {
|
|
return state == Options.AppState.PAUSED;
|
|
}
|
|
|
|
public final boolean isTerminated() {
|
|
return state == Options.AppState.TERMINATED;
|
|
}
|
|
|
|
public final boolean isTerminating() {
|
|
return state == Options.AppState.STOPPED || state == Options.AppState.TERMINATED;
|
|
}
|
|
|
|
/**
|
|
* Stoppt die Zeichenmaschine.
|
|
* <p>
|
|
* Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine
|
|
* in den Zustand {@link Options.AppState#STOPPED} und ruft
|
|
* {@link #shutdown()} auf. Nachdem {@code teardown()} ausgeführt wurde
|
|
* wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das
|
|
* Zeichenfenster bleibt weiter geöffnet.
|
|
* <p>
|
|
* Die Zeichenmaschine reagiert in diesem Zustand weiter auf Eingaben,
|
|
* allerdings muss die Zeichnung nun manuell mit {@link #redraw()}
|
|
* aktualisiert werden.
|
|
*/
|
|
public final void stop() {
|
|
running = false;
|
|
}
|
|
|
|
/**
|
|
* Führt interne Aufräumarbeiten durch.
|
|
* <p>
|
|
* Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und
|
|
* verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die
|
|
* Äquivalente Methode für Unterklassen ist {@link #shutdown()}, die direkt
|
|
* vor {@code cleanup()} aufgerufen wird.
|
|
*/
|
|
private void cleanup() {
|
|
LOG.debug("%s shutting down.", APP_NAME);
|
|
// Alle noch nicht ausgelösten Events werden entfernt
|
|
eventQueue.clear();
|
|
// Alle noch nicht ausgeführten Tasks werden entfernt
|
|
taskQueue.clear();
|
|
// TaskRunner stoppen
|
|
TaskRunner.shutdown();
|
|
}
|
|
|
|
/**
|
|
* Beendet die Zeichenmaschine vollständig.
|
|
* <p>
|
|
* Das Programm wird {@link #quit() beendet} und alle geöffneten Fenster
|
|
* geschlossen. Falls die Maschine noch läuft, wird sie zunächst nach dem
|
|
* nächsten vollständigen Frame {@link #stop() gestoppt}.
|
|
*/
|
|
public final void exit() {
|
|
if( running ) {
|
|
running = false;
|
|
quitAfterShutdown = true;
|
|
} else {
|
|
quit(true);
|
|
}
|
|
}
|
|
|
|
public final void exitNow() {
|
|
// Do nothing, when already quitting
|
|
if( state == Options.AppState.QUITING ) {
|
|
return;
|
|
}
|
|
|
|
if( running ) {
|
|
running = false;
|
|
terminateImediately = true;
|
|
quitAfterShutdown = true;
|
|
mainThread.interrupt();
|
|
} else {
|
|
quit(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Beendet das Programm vollständig.
|
|
* <p>
|
|
* Enspricht dem Aufruf {@code quit(true)}.
|
|
*
|
|
* @see #quit(boolean)
|
|
*/
|
|
public final void quit() {
|
|
//quit(!IN_BLUEJ);
|
|
quit(true);
|
|
}
|
|
|
|
/**
|
|
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird
|
|
* die komplette VM beendet.
|
|
* <p>
|
|
* Die Methode sorgt nicht für ein ordnungsgemäßes herunterfahren und
|
|
* freigeben aller Ressourcen, da die Zeichenmaschine gegebenenfalls
|
|
* geöffnet bleiben und weitere Aufgaben erfüllen soll. Aufrufende Methoden
|
|
* sollten dies berücksichtigen.
|
|
* <p>
|
|
* Soll das Programm vollständig beendet werden, ist es ratsamer
|
|
* {@link #exit()} zu verwenden.
|
|
*
|
|
* @param exit Ob die VM beendet werden soll.
|
|
* @see System#exit(int)
|
|
*/
|
|
public final void quit( boolean exit ) {
|
|
state = Options.AppState.QUITING;
|
|
TaskRunner.invokeLater(() -> {
|
|
frame.setVisible(false);
|
|
canvas.dispose();
|
|
frame.dispose();
|
|
|
|
if( exit ) {
|
|
System.exit(0);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ändert die Größe der {@link Zeichenleinwand}.
|
|
*
|
|
* @param width Neue Breite der Zeichenleinwand.
|
|
* @param height Neue Höhe der Zeichenleinwand.
|
|
* @see Zeichenleinwand#setSize(int, int)
|
|
*/
|
|
public final void setSize( int width, int height ) {
|
|
frame.setCanvasSize(width, height);
|
|
|
|
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
|
|
canvasWidth = canvasBounds.width;
|
|
canvasHeight = canvasBounds.height;
|
|
}
|
|
|
|
/**
|
|
* Die Breite der {@link Zeichenleinwand}.
|
|
*
|
|
* @return Die Breite der {@link Zeichenleinwand}.
|
|
*/
|
|
public final int getWidth() {
|
|
return canvasWidth;
|
|
}
|
|
|
|
/**
|
|
* Die Höhe der {@link Zeichenleinwand}.
|
|
*
|
|
* @return Die Höhe der {@link Zeichenleinwand}.
|
|
*/
|
|
public final int getHeight() {
|
|
return canvasHeight;
|
|
}
|
|
|
|
/**
|
|
* Setzt den Titel des Zeichenfensters.
|
|
*
|
|
* @param title Der Titel, der oben im Zeichenfenster angezeigt wird.
|
|
*/
|
|
public final void setTitle( String title ) {
|
|
frame.setTitle(title);
|
|
}
|
|
|
|
/**
|
|
* Gibt die Zeichenleinwand zurück.
|
|
*
|
|
* @return Die Zeichenleinwand.
|
|
*/
|
|
public final Zeichenleinwand getCanvas() {
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* Fügt der {@link Zeichenleinwand} eine weitere {@link Layer Ebene} hinzu.
|
|
*
|
|
* @param layer Die neue Ebene.
|
|
*/
|
|
public final void addLayer( Layer layer ) {
|
|
canvas.addLayer(layer);
|
|
layer.setSize(getWidth(), getHeight());
|
|
}
|
|
|
|
/**
|
|
* Gibt die Anzahl der {@link Layer Ebenen} in der {@link Zeichenleinwand}
|
|
* zurück.
|
|
*
|
|
* @return Die Anzahl der Ebenen.
|
|
*/
|
|
public final int getLayerCount() {
|
|
return canvas.getLayerCount();
|
|
}
|
|
|
|
/**
|
|
* Gibt die {@link Layer Ebene} am angegebenen Index zurück. Gibt es keine
|
|
* Ebene mit diesem Index.
|
|
*
|
|
* @param index
|
|
* @return
|
|
*/
|
|
public final Layer getLayer( int index ) {
|
|
return canvas.getLayer(index);
|
|
}
|
|
|
|
/**
|
|
* Gibt die erste (unterste) {@link Layer Ebene} der angegebenen Klasse
|
|
* zurück.
|
|
*
|
|
* <pre><code>
|
|
* DrawingLayer draw = getLayer(DrawingLayer.class);
|
|
* </code></pre>
|
|
*
|
|
* @param layerClass
|
|
* @param <LT>
|
|
* @return
|
|
*/
|
|
public final <LT extends Layer> LT getLayer( Class<LT> layerClass ) {
|
|
return canvas.getLayer(layerClass);
|
|
}
|
|
|
|
/**
|
|
* Gibt die {@link ColorLayer Ebene} mit der Hintergrundfarbe zurück. Gibt
|
|
* es keine solche Ebene, so wird eine erstellt und der
|
|
* {@link Zeichenleinwand} hinzugefügt.
|
|
* <p>
|
|
* In der Regel sollte dies dieselbe Ebene sein wie {@link #background}.
|
|
*
|
|
* @return Die Hintergrundebene.
|
|
*/
|
|
public final ColorLayer getBackgroundLayer() {
|
|
ColorLayer layer = canvas.getLayer(ColorLayer.class);
|
|
if( layer == null ) {
|
|
layer = new ColorLayer(DEFAULT_BACKGROUND);
|
|
canvas.addLayer(0, layer);
|
|
}
|
|
return layer;
|
|
}
|
|
|
|
/**
|
|
* Gibt die Standard-{@link DrawingLayer Zeichenebene} zurück. Gibt es keine
|
|
* solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
|
|
* hinzugefügt.
|
|
* <p>
|
|
* In der Regel sollte dies dieselbe Ebene sein wie {@link #drawing}.
|
|
*
|
|
* @return Die Zeichenebene.
|
|
*/
|
|
public final DrawingLayer getDrawingLayer() {
|
|
DrawingLayer layer = canvas.getLayer(DrawingLayer.class);
|
|
if( layer == null ) {
|
|
layer = new DrawingLayer(getWidth(), getHeight());
|
|
canvas.addLayer(1, layer);
|
|
}
|
|
return layer;
|
|
}
|
|
|
|
/**
|
|
* Gibt die Standard-{@link ShapesLayer Formenebene} zurück. Gibt es keine
|
|
* solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
|
|
* hinzugefügt.
|
|
* <p>
|
|
* In der Regel sollte dies dieselbe Ebene sein wie {@link #shapes}.
|
|
*
|
|
* @return Die Formenebene.
|
|
*/
|
|
public final ShapesLayer getShapesLayer() {
|
|
ShapesLayer layer = canvas.getLayer(ShapesLayer.class);
|
|
if( layer == null ) {
|
|
layer = new ShapesLayer(getWidth(), getHeight());
|
|
canvas.addLayer(2, layer);
|
|
}
|
|
return layer;
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuellen Frames pro Sekunde zurück.
|
|
*
|
|
* @return Angepeilte Frames pro Sekunde
|
|
*/
|
|
public final int getFramesPerSecond() {
|
|
return framesPerSecondInternal;
|
|
}
|
|
|
|
/**
|
|
* Setzt die Anzahl an Frames pro Sekunde auf einen neuen Wert.
|
|
*
|
|
* @param pFramesPerSecond Neue FPS.
|
|
*/
|
|
public final void setFramesPerSecond( int pFramesPerSecond ) {
|
|
if( pFramesPerSecond > 0 ) {
|
|
framesPerSecondInternal = pFramesPerSecond;
|
|
} else {
|
|
framesPerSecondInternal = 1;
|
|
// Logger ...
|
|
}
|
|
framesPerSecond = framesPerSecondInternal;
|
|
}
|
|
|
|
/**
|
|
* Speichert den aktuellen Inhalt der {@link Zeichenleinwand} in einer
|
|
* Bilddatei auf der Festplatte. Zur Auswahl der Zieldatei wird dem Nutzer
|
|
* ein {@link JFileChooser} angezeigt.
|
|
*/
|
|
public final void saveImage() {
|
|
JFileChooser jfc = new JFileChooser();
|
|
jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
|
|
jfc.setMultiSelectionEnabled(false);
|
|
|
|
int status = jfc.showSaveDialog(frame);
|
|
if( status == JFileChooser.APPROVE_OPTION ) {
|
|
File outfile = jfc.getSelectedFile();
|
|
if( outfile.isDirectory() ) {
|
|
outfile = new File(outfile.getAbsolutePath() + File.separator + "zeichenmaschine.png");
|
|
}
|
|
saveImage(outfile.getAbsolutePath());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Speichert den aktuellen Inhalt der {@link Zeichenleinwand} in einer
|
|
* Bilddatei im angegebenen Dateipfad auf der Festplatte.
|
|
*/
|
|
public final void saveImage( String filepath ) {
|
|
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
|
|
|
|
Graphics2D g = img.createGraphics();
|
|
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
|
|
g.fillRect(0, 0, img.getWidth(), img.getHeight());
|
|
canvas.draw(g);
|
|
g.dispose();
|
|
|
|
try {
|
|
ImageLoader.saveImage(img, new File(filepath), true);
|
|
} catch( IOException ex ) {
|
|
ex.printStackTrace();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Erstellt eine Momentanaufnahme des aktuellen Inhalts der
|
|
* {@link Zeichenleinwand} und erstellt daraus eine
|
|
* {@link ImageLayer Bildebene}. Die Ebene wird automatisch der
|
|
* {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt.
|
|
*
|
|
* @return Die neue Bildebene.
|
|
*/
|
|
public final ImageLayer snapshot() {
|
|
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
|
|
|
|
Graphics2D g = img.createGraphics();
|
|
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
|
|
g.fillRect(0, 0, img.getWidth(), img.getHeight());
|
|
canvas.draw(g);
|
|
g.dispose();
|
|
|
|
/*
|
|
float factor = 0.8f;
|
|
float base = 255f * (1f - factor);
|
|
RescaleOp op = new RescaleOp(factor, base, null);
|
|
BufferedImage filteredImage
|
|
= new BufferedImage(img.getWidth(), img.getHeight(), img.getType());
|
|
op.filter(img, filteredImage);
|
|
*/
|
|
|
|
ImageLayer imgLayer = new ImageLayer(img);
|
|
if( canvas.getLayer(0) instanceof ColorLayer ) {
|
|
canvas.addLayer(1, imgLayer);
|
|
} else {
|
|
canvas.addLayer(0, imgLayer);
|
|
}
|
|
return imgLayer;
|
|
}
|
|
|
|
/**
|
|
* Pausiert die Schleife der Zeichenmaschine für die angegebene Anzahl an
|
|
* Millisekunden.
|
|
* <p>
|
|
* Falls {@code delay()} während eines Aufrufs von {@link #draw()}
|
|
* aufgerufen wird, dann wird der aktuelle Zustand der Leinwand angezeigt.
|
|
* <p>
|
|
* Die Methode übernimmt keine Garantie, dass die Wartezeit exakt {@code ms}
|
|
* Millisekunden beträgt. Sie kann etwas kürzer oder (für kurze Wartezeiten)
|
|
* etwas länger sein. Für zeitkritische Simulationen sollte daher die genaue
|
|
* Zeitdifferenz gemessen und berücksichtigt werden.
|
|
*
|
|
* @param ms Wartezeit in Millisekunden.
|
|
*/
|
|
public final void delay( int ms ) {
|
|
if( ms <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
if( state == Options.AppState.INITIALIZING ||
|
|
state == Options.AppState.INITIALIZED ) {
|
|
LOG.warn("Don't use delay(int) from within settings() or setup().");
|
|
return;
|
|
}
|
|
|
|
long timer = 0L;
|
|
if( /*updateState == Options.AppState.DRAWING*/
|
|
isTerminating() ) {
|
|
// Falls gerade draw() ausgeführt wird, zeigen wir den aktuellen
|
|
// Stand der Zeichnung auf der Leinwand an. Die Zeit für das
|
|
// Rendern wird gemessen und von der Wartezeit abgezogen.
|
|
timer = System.nanoTime();
|
|
canvas.render();
|
|
timer = System.nanoTime() - timer;
|
|
}
|
|
|
|
Options.AppState oldState = updateState;
|
|
try {
|
|
int sub = (int) Math.ceil(timer / 1000000.0);
|
|
|
|
if( sub >= ms ) {
|
|
return;
|
|
}
|
|
|
|
updateState = Options.AppState.DELAYED;
|
|
Thread.sleep(ms - sub, (int) (timer % 1000000L));
|
|
} catch( InterruptedException ignored ) {
|
|
// Nothing
|
|
} finally {
|
|
updateState = oldState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Macht den Mauszeiger unsichtbar.
|
|
* <p>
|
|
* Nach dem Aufruf gilt {@code cursorVisible == false}.
|
|
* <p>
|
|
* Der Aufruf von {@code hideCursor()} ist dasselbe wie der Aufruf von
|
|
* {@link #setCursor(Cursor) setCursor(null)}.
|
|
*/
|
|
public final void hideCursor() {
|
|
setCursor(null);
|
|
}
|
|
|
|
/**
|
|
* Zeigt den Mauszeiger wieder an, falls er zuvor
|
|
* {@link #hideCursor() versteckt} wurde.
|
|
* <p>
|
|
* Nach dem Aufruf gilt {@code cursorVisible == true}.
|
|
* <p>
|
|
* Der Aufruf von {@code hideCursor()} ist dasselbe wie der Aufruf von
|
|
* {@link #setCursor(int) setCursor(Cursor.DEFAULT_CURSOR)}.
|
|
*/
|
|
public final void showCursor() {
|
|
setCursor(Cursor.DEFAULT_CURSOR);
|
|
}
|
|
|
|
/**
|
|
* Ändert den Mauszeiger auf ein eigenes Bild.
|
|
* <p>
|
|
* Das Bild darf die vom Betriebssystem vorgegebene Mindestgröße nicht
|
|
* überschreiten und kann aus einer beliebigen Quelle geladen werden, oder
|
|
* direkt im Programm erstellt werden. Die Koordinaten des Hotspot geben an,
|
|
* an welcher Stelle des Bildes sich die "Spitze" befindet. Die Koordinaten
|
|
* werden relativ zur oberen linken Ecke des Bildes angegeben.
|
|
*
|
|
* @param pCursorImage Ein Bild, das den Mauszeiger ersetzt.
|
|
* @param hotSpotX Relative x-Koordinate des Hotspots.
|
|
* @param hotSpotY Relative y-Koordinate des Hotspots.
|
|
* @see ImageLoader#loadImage(String)
|
|
* @see Toolkit#createCustomCursor(Image, Point, String)
|
|
*/
|
|
public final void setCursor( Image pCursorImage, int hotSpotX, int hotSpotY ) {
|
|
Point hotSpot = new Point(hotSpotX, hotSpotY);
|
|
setCursor(canvas.getToolkit().createCustomCursor(pCursorImage, hotSpot, "zmCursor"));
|
|
}
|
|
|
|
/**
|
|
* Setzt den Mauszeiger auf eines der vordefinierten Symbole.
|
|
* <p>
|
|
* Die Konstanten der Klasse {@link Cursor} definieren 13 Standardzeiger,
|
|
* die durch angabe der Nummer geladen werden können.
|
|
* <pre><code>
|
|
* setCursor(Cursor.HAND_CURSOR);
|
|
* </code></pre>
|
|
*
|
|
* @param pPredefinedCursor Eine der Cursor-Konstanten.
|
|
* @see Cursor
|
|
*/
|
|
public final void setCursor( int pPredefinedCursor ) {
|
|
setCursor(Cursor.getPredefinedCursor(pPredefinedCursor));
|
|
}
|
|
|
|
/**
|
|
* Setzt den Mauszeiger auf das übergebenen Cursor-Objekt. Wenn
|
|
* {@code pCursor} {@code null} ist, wird der Mauszeiger unsichtbar gemacht
|
|
* (dies ist dasselbe wie der Aufruf von {@link #hideCursor()}).
|
|
*
|
|
* @param pCursor Ein Cursor-Objekt oder {@code null}.
|
|
* <p>
|
|
* Nach Aufruf der Methode kann über {@link #cursorVisible} abgefragt
|
|
* werden, ob der Cursor zurzeit sichtbar ist oder nicht.
|
|
*/
|
|
public final void setCursor( Cursor pCursor ) {
|
|
if( pCursor == null && cursorVisible ) {
|
|
// Falls null übergeben, Zeiger verstecken
|
|
|
|
// Übernommen aus processing.awt.PSurfaceAWT von Processing4
|
|
if( invisibleCursor == null ) {
|
|
BufferedImage cursorImg =
|
|
new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
|
|
invisibleCursor =
|
|
canvas.getToolkit().createCustomCursor(cursorImg, new Point(8, 8), "blank");
|
|
}
|
|
canvas.setCursor(invisibleCursor);
|
|
cursorVisible = false;
|
|
} else if( pCursor != null ) {
|
|
// Zeiger neu zuweisen
|
|
canvas.setCursor(pCursor);
|
|
cursorVisible = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Methoden, die von Unterklassen überschrieben werden können / sollen.
|
|
*/
|
|
|
|
/**
|
|
* Die Settings werden einmal beim Erstellten der Zeichenmaschine
|
|
* aufgerufen.
|
|
* <p>
|
|
* {@code settings()} wird nur selten benötigt, wenn das Zeichenfenster
|
|
*/
|
|
public void settings() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
/**
|
|
* Methode, die von Unterklassen überschrieben werden sollte, um die
|
|
* Zeichenmaschine vor dem Start zu konfigurieren. Hier können vorbereitende
|
|
* Befehle ausgeführt werden, die die {@link Zeichenleinwand} zu
|
|
* initialisieren, neue Objekte instanziieren und Variablen initialisieren.
|
|
*/
|
|
public void setup() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
/**
|
|
* {@code update()} wird einmal pro Frame vor {@link #draw()} aufgerufen, um
|
|
* notwendige Aktualisierungen vorzunehmen. Im Gegensatz zu {@link #draw()}
|
|
* bekommt {@code update()} zusätzlich {@link #delta} übergeben, um die
|
|
* Aktualisierungen abhängig von der echten Verzögerung zwischen zwei Frames
|
|
* zu berechnen.
|
|
* <p>
|
|
* {@code delta} wird in Sekunden angegeben. Um eine Form zum Beispiel um
|
|
* {@code 50} Pixel pro Sekunde in {@code x}-Richtung zu bewegen, kann so
|
|
* vorgegangen werden:
|
|
* <pre><code>
|
|
* shape.move(50*delta, 0.0);
|
|
* </code></pre>
|
|
*
|
|
* @param delta
|
|
*/
|
|
public void update( double delta ) {
|
|
running = !run_once;
|
|
}
|
|
|
|
/**
|
|
* {@code draw()} wird einmal pro Frame aufgerufen. Bei einer
|
|
* {@link #getFramesPerSecond() Framerate} von 60 also in etwa 60-Mal pro
|
|
* Sekunde. In der {@code draw}-Methode wird der Inhalt der Ebenen
|
|
* manipuliert und deren Inhalte gezeichnet. Am Ende des Frames werden alle
|
|
* Ebenen auf die {@link Zeichenleinwand} übertragen.
|
|
* <p>
|
|
* {@code draw()} stellt die wichtigste Methode für eine Zeichenmaschine
|
|
* dar, da hier die Zeichnung des Programms erstellt wird.
|
|
*/
|
|
public void draw() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
/**
|
|
* {@code teardown()} wird aufgerufen, sobald die Schleife des
|
|
* Hauptprogramms beendet wurde. Dies passiert entweder nach dem ersten
|
|
* Durchlauf (wenn keine eigene {@link #update(double)} erstellt wurde),
|
|
* nach dem Aufruf von {@link #stop()} oder nachdem das
|
|
* {@link Zeichenfenster} geschlossen wurde.
|
|
* <p>
|
|
* In {@code teardown()} kann zum Beispiel der Abschlussbildschirm eines
|
|
* Spiels oder der Abspann einer Animation angezeigt werden, oder mit
|
|
* {@link #saveImage()} die erstellte Zeichnung abgespeichert werden.
|
|
*/
|
|
public void shutdown() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
/*
|
|
* Task scheduling
|
|
*/
|
|
|
|
public void scheduleTask( Runnable runnable, int delay ) {
|
|
taskQueue.add(new DelayedTask(delay, runnable));
|
|
}
|
|
|
|
public void scheduleTask( Runnable runnable, int delay, boolean concurrent ) {
|
|
DelayedTask task = new DelayedTask(delay, runnable);
|
|
task.concurrent = concurrent;
|
|
taskQueue.add(task);
|
|
}
|
|
|
|
private void runTasks() {
|
|
synchronized( taskQueue ) {
|
|
DelayedTask task = taskQueue.poll();
|
|
while( task != null ) {
|
|
if( task.concurrent ) {
|
|
// SwingUtilities.invokeLater(task.runnable);
|
|
TaskRunner.run(task.runnable);
|
|
} else {
|
|
task.runnable.run();
|
|
}
|
|
|
|
task = taskQueue.poll();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Input handling
|
|
*/
|
|
private void enqueueEvent( InputEvent evt ) {
|
|
if( updateState != Options.AppState.DELAYED ) {
|
|
eventQueue.add(evt);
|
|
}
|
|
|
|
if( isPaused() || isTerminated() ) {
|
|
if( MouseEvent.class.isInstance(evt) ) {
|
|
saveMousePosition((MouseEvent)evt);
|
|
}
|
|
dispatchEvents();
|
|
}
|
|
}
|
|
|
|
private void dispatchEvents() {
|
|
//synchronized( eventQueue ) {
|
|
while( !eventQueue.isEmpty() ) {
|
|
InputEvent evt = eventQueue.poll();
|
|
|
|
switch( evt.getID() ) {
|
|
case KeyEvent.KEY_TYPED:
|
|
case KeyEvent.KEY_PRESSED:
|
|
case KeyEvent.KEY_RELEASED:
|
|
handleKeyEvent((KeyEvent) evt);
|
|
break;
|
|
|
|
case MouseEvent.MOUSE_CLICKED:
|
|
case MouseEvent.MOUSE_PRESSED:
|
|
case MouseEvent.MOUSE_RELEASED:
|
|
case MouseEvent.MOUSE_MOVED:
|
|
case MouseEvent.MOUSE_DRAGGED:
|
|
case MouseEvent.MOUSE_WHEEL:
|
|
handleMouseEvent((MouseEvent) evt);
|
|
break;
|
|
}
|
|
}
|
|
//}
|
|
}
|
|
|
|
private void handleKeyEvent( KeyEvent evt ) {
|
|
keyEvent = evt;
|
|
key = evt.getKeyChar();
|
|
keyCode = evt.getKeyCode();
|
|
|
|
switch( evt.getID() ) {
|
|
case KeyEvent.KEY_TYPED:
|
|
keyTyped(evt);
|
|
break;
|
|
case KeyEvent.KEY_PRESSED:
|
|
keyPressed = true;
|
|
keyPressed(evt);
|
|
break;
|
|
case KeyEvent.KEY_RELEASED:
|
|
keyPressed = false;
|
|
keyReleased(evt);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleMouseEvent( MouseEvent evt ) {
|
|
mouseEvent = evt;
|
|
|
|
switch( evt.getID() ) {
|
|
case MouseEvent.MOUSE_CLICKED:
|
|
mouseClicked(evt);
|
|
break;
|
|
case MouseEvent.MOUSE_PRESSED:
|
|
mousePressed = true;
|
|
mouseButton = evt.getButton();
|
|
mousePressed(evt);
|
|
break;
|
|
case MouseEvent.MOUSE_RELEASED:
|
|
mousePressed = false;
|
|
mouseButton = NOMOUSE;
|
|
mouseReleased(evt);
|
|
break;
|
|
case MouseEvent.MOUSE_DRAGGED:
|
|
//saveMousePosition(evt);
|
|
mouseDragged(evt);
|
|
break;
|
|
case MouseEvent.MOUSE_MOVED:
|
|
//saveMousePosition(evt);
|
|
mouseMoved(evt);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void mouseClicked( MouseEvent e ) {
|
|
mouseClicked();
|
|
}
|
|
|
|
public void mouseClicked() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void mousePressed( MouseEvent e ) {
|
|
mousePressed();
|
|
}
|
|
|
|
public void mousePressed() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void mouseReleased( MouseEvent e ) {
|
|
mouseReleased();
|
|
}
|
|
|
|
public void mouseReleased() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void mouseDragged( MouseEvent e ) {
|
|
mouseDragged();
|
|
}
|
|
|
|
public void mouseDragged() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void mouseMoved( MouseEvent e ) {
|
|
mouseMoved();
|
|
}
|
|
|
|
public void mouseMoved() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
private void saveMousePosition( MouseEvent event ) {
|
|
if( mouseEvent != null && event.getComponent() == canvas ) {
|
|
pmouseX = mouseX;
|
|
pmouseY = mouseY;
|
|
|
|
mouseX = cmouseX;
|
|
mouseY = cmouseY;
|
|
}
|
|
}
|
|
|
|
private void saveMousePosition() {
|
|
pmouseX = mouseX;
|
|
pmouseY = mouseY;
|
|
|
|
// Calculates mouse position based on screen, not based on canvas
|
|
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
|
|
java.awt.Point compLoc = canvas.getLocationOnScreen();
|
|
mouseX = mouseLoc.x - compLoc.x;
|
|
mouseY = mouseLoc.y - compLoc.y;
|
|
}
|
|
|
|
/*
|
|
* Keyboard handling
|
|
*/
|
|
public void keyTyped( KeyEvent e ) {
|
|
keyTyped();
|
|
}
|
|
|
|
public void keyTyped() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void keyPressed( KeyEvent e ) {
|
|
keyPressed();
|
|
}
|
|
|
|
public void keyPressed() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
public void keyReleased( KeyEvent e ) {
|
|
keyReleased();
|
|
}
|
|
|
|
public void keyReleased() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
// Window changes
|
|
public void fullscreenChanged() {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
////
|
|
// Zeichenthread
|
|
////
|
|
|
|
/**
|
|
* Globaler Monitor, der einmal pro Frame vom Zeichenthread freigegeben
|
|
* wird. Andere Threads können {@link Object#wait()} auf dem Monitor
|
|
* aufrufen, um sich mit dem Zeichenthread zu synchronisieren. Der
|
|
* {@code wait()} Aufruf sollte sich zur Sicherheit in einer Schleife
|
|
* befinden, die prüft, ob sich der Aktuelle {@link #tick} erhöht hat.
|
|
* <pre><code>
|
|
* int lastTick = Constants.tick;
|
|
*
|
|
* // Do some work
|
|
*
|
|
* while( lastTick >= Constants.tick ) {
|
|
* synchronized( Zeichenmaschine.globalSyncLock ) {
|
|
* try {
|
|
* Zeichenmaschine.globalSyncLock.wait();
|
|
* } catch( InterruptedException ex ) {}
|
|
* }
|
|
* }
|
|
* // Next frame has started
|
|
* </code></pre>
|
|
* <p>
|
|
* Die {@link schule.ngb.zm.util.tasks.FrameSynchronizedTask} implementiert
|
|
* eine {@link schule.ngb.zm.util.tasks.Task}, die sich automatisch auf
|
|
* diese Wiese mit dem Zeichenthread synchronisiert.
|
|
*/
|
|
public static final Object globalSyncLock = new Object[0];
|
|
|
|
class Zeichenthread extends Thread {
|
|
|
|
public Zeichenthread() {
|
|
super(APP_NAME);
|
|
//super(APP_NAME + " " + APP_VERSION);
|
|
}
|
|
|
|
@Override
|
|
public final void run() {
|
|
// Wait for full initialization before start
|
|
while( state != Options.AppState.INITIALIZED ) {
|
|
Thread.yield();
|
|
}
|
|
|
|
// ThreadExecutor for the update/draw Thread
|
|
final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor();
|
|
|
|
// Start des Thread in ms
|
|
final long start = System.currentTimeMillis();
|
|
// Aktuelle Zeit in ns
|
|
long beforeTime;
|
|
long updateBeforeTime = System.nanoTime();
|
|
// Speicher für Änderung
|
|
long overslept = 0L;
|
|
// Interne Zähler für tick und runtime
|
|
int _tick = 0;
|
|
long _runtime;
|
|
// Öffentliche Zähler für Unterklassen
|
|
tick = 0;
|
|
runtime = 0;
|
|
|
|
// setup() der Unterklasse aufrufen
|
|
setup();
|
|
|
|
// Alles startklar ...
|
|
state = Options.AppState.RUNNING;
|
|
while( running ) {
|
|
// Aktuelle Zeit in ns merken
|
|
beforeTime = System.nanoTime();
|
|
|
|
// Mausposition einmal pro Frame merken
|
|
saveMousePosition(mouseEvent);
|
|
|
|
if( state != Options.AppState.PAUSED ) {
|
|
// update() und draw() der Unterklasse werden in einem
|
|
// eigenen Thread ausgeführt, aber der Zeichenthread
|
|
// wartet, bis der Thread fertig ist. Außer die Unterklasse
|
|
// ruft delay() auf und lässt den Thread eine länger Zeit
|
|
// schlafen. Dann wird der nächst Frame vorzeitig gerendert,
|
|
// bis der update-Thread wieder bereit ist. Dadurch können
|
|
// nebenläufige Aufgaben (z.B. Animationen) weiterlaufen.
|
|
if( !updateThreadExecutor.isRunning() ) {
|
|
delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0;
|
|
updateBeforeTime = System.nanoTime();
|
|
|
|
// uddate()/draw() ausführen
|
|
updateThreadExecutor.execute(() -> {
|
|
if( state == Options.AppState.RUNNING
|
|
&& updateState == Options.AppState.IDLE ) {
|
|
// Call to update()
|
|
updateState = Options.AppState.UPDATING;
|
|
Zeichenmaschine.this.update(delta);
|
|
// Update Layers
|
|
canvas.updateLayers(delta);
|
|
// Call to draw()
|
|
updateState = Options.AppState.DRAWING;
|
|
Zeichenmaschine.this.draw();
|
|
updateState = Options.AppState.DISPATCHING;
|
|
// Send latest input events after finishing draw
|
|
// since these may also block
|
|
dispatchEvents();
|
|
updateState = Options.AppState.IDLE;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for the update/draw Thread to finish
|
|
while( updateThreadExecutor.isRunning()
|
|
&& !updateThreadExecutor.isWaiting() ) {
|
|
Thread.yield();
|
|
|
|
if( Thread.interrupted() ) {
|
|
running = false;
|
|
terminateImediately = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Display the current buffer content
|
|
if( canvas != null && frame.isDisplayable() ) {
|
|
canvas.render();
|
|
// canvas.invalidate();
|
|
// frame.repaint();
|
|
}
|
|
|
|
// dispatchEvents();
|
|
}
|
|
|
|
// Running pending tasks and notify any
|
|
// waiting FrameSynchonizedTasks
|
|
// TODO: should this this also happen in the updateThread?
|
|
runTasks();
|
|
synchronized( globalSyncLock ) {
|
|
globalSyncLock.notifyAll();
|
|
}
|
|
|
|
// delta time in ns
|
|
long afterTime = System.nanoTime();
|
|
long dt = afterTime - beforeTime;
|
|
long sleep = ((1000000000L / framesPerSecondInternal) - dt) - overslept;
|
|
|
|
// Sleep before next frame
|
|
if( sleep > 0 ) {
|
|
try {
|
|
Thread.sleep(sleep / 1000000L, (int) (sleep % 1000000L));
|
|
} catch( InterruptedException e ) {
|
|
// Interrupt not relevant
|
|
}
|
|
|
|
overslept = (System.nanoTime() - afterTime) - sleep;
|
|
} else {
|
|
overslept = 0L;
|
|
}
|
|
|
|
// Update stats
|
|
_tick += 1;
|
|
_runtime = System.currentTimeMillis() - start;
|
|
tick = _tick;
|
|
runtime = _runtime;
|
|
framesPerSecond = framesPerSecondInternal;
|
|
|
|
// If pause requested, we pause now
|
|
if( pause_pending ) {
|
|
state = Options.AppState.PAUSED;
|
|
pause_pending = false;
|
|
}
|
|
}
|
|
state = Options.AppState.STOPPED;
|
|
// Shutdown the updateThread
|
|
while( !terminateImediately && updateThreadExecutor.isRunning() ) {
|
|
Thread.yield();
|
|
}
|
|
updateThreadExecutor.shutdownNow();
|
|
try {
|
|
updateThreadExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
|
|
} catch( InterruptedException ex ) {
|
|
|
|
} finally {
|
|
// Cleanup
|
|
shutdown();
|
|
cleanup();
|
|
state = Options.AppState.TERMINATED;
|
|
|
|
if( quitAfterShutdown ) {
|
|
quit();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: Remove
|
|
static class DelayedTask implements Delayed {
|
|
|
|
long startTime; // in ms
|
|
|
|
Runnable runnable;
|
|
|
|
boolean concurrent = false;
|
|
|
|
public DelayedTask( int delay, Runnable runnable ) {
|
|
this.startTime = System.currentTimeMillis() + delay;
|
|
this.runnable = runnable;
|
|
}
|
|
|
|
@Override
|
|
public long getDelay( TimeUnit unit ) {
|
|
int diff = (int) (startTime - System.currentTimeMillis());
|
|
return unit.convert(diff, TimeUnit.MILLISECONDS);
|
|
}
|
|
|
|
@Override
|
|
public int compareTo( Delayed o ) {
|
|
return (int) (startTime - ((DelayedTask) o).startTime);
|
|
}
|
|
|
|
}
|
|
|
|
class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener {
|
|
|
|
@Override
|
|
public void mouseClicked( MouseEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void mousePressed( MouseEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void mouseReleased( MouseEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void mouseEntered( MouseEvent e ) {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
@Override
|
|
public void mouseExited( MouseEvent e ) {
|
|
// Intentionally left blank
|
|
}
|
|
|
|
@Override
|
|
public void mouseDragged( MouseEvent e ) {
|
|
cmouseX = e.getX();
|
|
cmouseY = e.getY();
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void mouseMoved( MouseEvent e ) {
|
|
cmouseX = e.getX();
|
|
cmouseY = e.getY();
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void keyTyped( KeyEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void keyPressed( KeyEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void keyReleased( KeyEvent e ) {
|
|
enqueueEvent(e);
|
|
}
|
|
|
|
@Override
|
|
public void mouseWheelMoved( MouseWheelEvent e ) {
|
|
// enqueueEvent(e);
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: (ngb) exception handling when update/draw throws ex
|
|
class UpdateThreadExecutor extends ThreadPoolExecutor {
|
|
|
|
private Thread updateThread;
|
|
|
|
private boolean running = false;
|
|
|
|
public UpdateThreadExecutor() {
|
|
super(1, 1, 0L,
|
|
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
|
|
new ThreadFactory() {
|
|
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
|
@Override
|
|
public Thread newThread( Runnable r ) {
|
|
Thread t = new Thread(mainThread.getThreadGroup(), r,
|
|
"updateThread-" + threadNumber.getAndIncrement(),
|
|
0);
|
|
t.setDaemon(true);
|
|
return t;
|
|
}
|
|
});
|
|
updateState = Options.AppState.IDLE;
|
|
}
|
|
|
|
@Override
|
|
public void execute( Runnable command ) {
|
|
running = true;
|
|
super.execute(command);
|
|
}
|
|
|
|
@Override
|
|
protected void beforeExecute( Thread t, Runnable r ) {
|
|
// We store the one Thread this Executor holds,
|
|
// but it might change if a new Thread needed to be spawned
|
|
// due to en error.
|
|
updateThread = t;
|
|
}
|
|
|
|
@Override
|
|
protected void afterExecute( Runnable r, Throwable t ) {
|
|
running = false;
|
|
updateState = Options.AppState.IDLE;
|
|
}
|
|
|
|
/**
|
|
* Ermittelt, ob der interne Thread gerade eine update/draw Task
|
|
* ausführt.
|
|
*
|
|
* @return
|
|
*/
|
|
public boolean isRunning() {
|
|
return running;
|
|
}
|
|
|
|
/**
|
|
* Ermittelt, ob der interne Thread gerade eine update/draw Task
|
|
* ausführt und dabei in einen Wartezustand versetzt wurde. Das bedeutet
|
|
* in der Regel, dass innerhalb von {@link #update(double)} oder
|
|
* {@link #draw()} ein {@link #delay(int)} ausgeführt wurde, oder aus
|
|
* einem anderen Grund beispielsweise {@link Thread#sleep(long)}
|
|
* aufgerufen wurde. (Dies kann zum Beispiel beim
|
|
* {@link Animation#await() Warten auf Animationen} der Fall sein.)
|
|
*
|
|
* @return
|
|
*/
|
|
public boolean isWaiting() {
|
|
//return running && updateThread.getState() == Thread.State.TIMED_WAITING;
|
|
//return running && updateThread != null && updateThread.getState() == Thread.State.TIMED_WAITING;
|
|
return running && updateThread != null && updateState == Options.AppState.DELAYED;
|
|
}
|
|
|
|
}
|
|
|
|
private static final Log LOG = Log.getLogger(Zeichenmaschine.class);
|
|
|
|
}
|