zeichenmaschine/src/main/java/schule/ngb/zm/Zeichenmaschine.java

1617 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 ) {
draw();
}
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>
* DrawingLayer draw = getLayer(DrawingLayer.class);
* </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>
* setCursor(Cursor.HAND_CURSOR);
* </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>
* shape.move(50*delta, 0.0);
* </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() ) {
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();
// 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);
}