diff --git a/src/main/java/schule/ngb/zm/Zeichenfenster.java b/src/main/java/schule/ngb/zm/Zeichenfenster.java index 61c897c..41cc753 100644 --- a/src/main/java/schule/ngb/zm/Zeichenfenster.java +++ b/src/main/java/schule/ngb/zm/Zeichenfenster.java @@ -11,6 +11,7 @@ import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; @@ -60,8 +61,8 @@ public class Zeichenfenster extends JFrame { private GraphicsDevice displayDevice; /** - * Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat - * es Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter + * Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat es + * Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter * Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine * Garantie für die Größe der Leinwand übernehmen. */ @@ -78,12 +79,13 @@ public class Zeichenfenster extends JFrame { * verlassen. Wird von {@link #setFullscreen(boolean)} automatisch * hinzugefügt und entfernt. */ - private KeyListener fullscreenExitListener = new KeyAdapter() { + private final KeyListener fullscreenExitListener = new KeyAdapter() { @Override public void keyPressed( KeyEvent e ) { if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) { // canvas.removeKeyListener(this); setFullscreen(false); + e.consume(); } } }; @@ -110,12 +112,11 @@ public class Zeichenfenster extends JFrame { this.canvasPreferredWidth = canvas.getWidth(); this.canvasPreferredHeight = canvas.getHeight(); - //this.add(canvas, BorderLayout.CENTER); - this.add(canvas); + this.add(canvas, BorderLayout.CENTER); this.canvas = canvas; // Konfiguration des Frames - this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION: title); + this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION : title); // Kann vom Aufrufenden überschrieben werden this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); @@ -144,6 +145,7 @@ public class Zeichenfenster extends JFrame { // Fenster zusammenbauen, auf dem Bildschirm positionieren ... this.pack(); this.setResizable(false); + this.setFocusable(true); this.setLocationByPlatform(true); // this.centerFrame(); } @@ -153,6 +155,7 @@ public class Zeichenfenster extends JFrame { } public Rectangle getScreenBounds() { + // return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); return displayDevice.getDefaultConfiguration().getBounds(); } @@ -163,6 +166,7 @@ public class Zeichenfenster extends JFrame { public Rectangle getCanvasBounds() { return canvas.getBounds(); } + /** * Zentriert das Zeichenfenster auf dem aktuellen Bildschirm. */ @@ -176,7 +180,7 @@ public class Zeichenfenster extends JFrame { } public void setCanvasSize( int newWidth, int newHeight ) { - // TODO: (ngb) Put constains on max/min frame/canvas size + // TODO: (ngb) Put constrains on max/min frame/canvas size if( fullscreen ) { canvasPreferredWidth = newWidth; canvasPreferredHeight = newHeight; @@ -205,27 +209,53 @@ public class Zeichenfenster extends JFrame { public final void setFullscreen( boolean pEnable ) { // See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html if( displayDevice.isFullScreenSupported() ) { + // Temporarily stop rendering + while( canvas.isRendering() ) { + try { + canvas.suspendRendering(); + } catch( InterruptedException ex ) { + LOG.info(ex, "setFullsceen(true) was interrupted and canceled."); + return; + } + } + if( pEnable && !fullscreen ) { - // frame.setUndecorated(true); - this.setResizable(false); // Should be set anyway + // Activate fullscreen + dispose(); + setUndecorated(true); + setResizable(false); displayDevice.setFullScreenWindow(this); - java.awt.Rectangle bounds = getScreenBounds(); - // TODO: (ngb) We need to switch layouts to allow the LayoutManger to maximize the canvas - canvas.setSize(bounds.width, bounds.height); // Register ESC to exit fullscreen - canvas.addKeyListener(fullscreenExitListener); + this.addKeyListener(fullscreenExitListener); + + // Reset canvas size to its new bounds to recreate buffer and drawing surface + java.awt.Rectangle canvasBounds = getCanvasBounds(); + canvas.setSize(canvasBounds.width, canvasBounds.height); + //canvas.requestFocus(); + this.requestFocus(); fullscreen = true; } else if( !pEnable && fullscreen ) { - fullscreen = false; - - canvas.removeKeyListener(fullscreenExitListener); displayDevice.setFullScreenWindow(null); + dispose(); + setUndecorated(false); + setResizable(false); + + this.removeKeyListener(fullscreenExitListener); canvas.setSize(canvasPreferredWidth, canvasPreferredHeight); - this.pack(); - // frame.setUndecorated(false); + + setVisible(true); + pack(); + + //canvas.requestFocus(); + this.requestFocus(); + + fullscreen = false; } + + // Resume rendering + canvas.resumeRendering(); } } diff --git a/src/main/java/schule/ngb/zm/Zeichenleinwand.java b/src/main/java/schule/ngb/zm/Zeichenleinwand.java index 63c4c96..96ee33d 100644 --- a/src/main/java/schule/ngb/zm/Zeichenleinwand.java +++ b/src/main/java/schule/ngb/zm/Zeichenleinwand.java @@ -1,16 +1,16 @@ package schule.ngb.zm; import schule.ngb.zm.layers.ColorLayer; -import schule.ngb.zm.layers.ShapesLayer; import schule.ngb.zm.util.Log; -import java.awt.*; +import java.awt.Canvas; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Toolkit; import java.awt.image.BufferStrategy; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; /** * Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus @@ -24,10 +24,14 @@ import java.util.concurrent.LinkedBlockingQueue; */ public class Zeichenleinwand extends Canvas { + private final Object[] renderLock = new Object[0]; + /** * Liste der hinzugefügten Ebenen. */ - private LinkedList layers; + private final LinkedList layers; + + private boolean rendering = false, suspended = false; /** * Erstellt eine neue Zeichenleinwand mit einer festen Größe. @@ -48,6 +52,20 @@ public class Zeichenleinwand extends Canvas { } } + public boolean isRendering() { + return rendering; + } + + public void suspendRendering() throws InterruptedException { + synchronized( renderLock ) { + suspended = true; + } + } + + public void resumeRendering() { + suspended = false; + } + /** * Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln. *

@@ -88,11 +106,11 @@ public class Zeichenleinwand extends Canvas { /** * Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn * der Index noch nicht existiert (also größer als die - * {@link #getLayerCount() Anzahl der Ebenen} ist, dann wird die neue Ebene + * {@link #getLayerCount() Anzahl der Ebenen} ist), dann wird die neue Ebene * als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein, * dass die neue Ebene am Ende wirklich am Index {@code i} steht. * - * @param i Index der Ebene, beginnend mit 0. + * @param i Index der Ebene beginnend mit 0. * @param layer Die neue Ebene. */ public void addLayer( int i, Layer layer ) { @@ -249,37 +267,41 @@ public class Zeichenleinwand extends Canvas { * Zeigt den aktuellen Inhalt der Zeichenleinwand an. */ public void render() { - if( getBufferStrategy() == null ) { - allocateBuffer(); - } + if( !suspended && isDisplayable() ) { + if( getBufferStrategy() == null ) { + allocateBuffer(); + } - if( isDisplayable() ) { - BufferStrategy strategy = this.getBufferStrategy(); - if( strategy != null ) { - do { + synchronized( renderLock ) { + rendering = true; + BufferStrategy strategy = this.getBufferStrategy(); + if( strategy != null ) { do { - Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics(); - g2d.clearRect(0, 0, getWidth(), getHeight()); + do { + Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics(); + g2d.clearRect(0, 0, getWidth(), getHeight()); - synchronized( layers ) { - List it = List.copyOf(layers); - for( Layer layer : it ) { - layer.draw(g2d); + synchronized( layers ) { + List it = List.copyOf(layers); + for( Layer layer : it ) { + layer.draw(g2d); + } } + + g2d.dispose(); + } while( strategy.contentsRestored() ); + + // Display the buffer + if( !strategy.contentsLost() ) { + strategy.show(); + + Toolkit.getDefaultToolkit().sync(); } - g2d.dispose(); - } while( strategy.contentsRestored() ); - - // Display the buffer - if( !strategy.contentsLost() ) { - strategy.show(); - - Toolkit.getDefaultToolkit().sync(); - } - - // Repeat the rendering if the drawing buffer was lost - } while( strategy.contentsLost() ); + // Repeat the rendering if the drawing buffer was lost + } while( strategy.contentsLost() ); + } + rendering = false; } } } diff --git a/src/main/java/schule/ngb/zm/Zeichenmaschine.java b/src/main/java/schule/ngb/zm/Zeichenmaschine.java index c9e8878..1f8f5a5 100644 --- a/src/main/java/schule/ngb/zm/Zeichenmaschine.java +++ b/src/main/java/schule/ngb/zm/Zeichenmaschine.java @@ -17,6 +17,7 @@ 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. @@ -37,43 +38,6 @@ public class Zeichenmaschine extends Constants { IN_BLUEJ = System.getProperty("java.class.path").contains("bluej"); } - /** - * Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde. - */ - public static final boolean MACOS; - - /** - * Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde. - */ - public static final boolean WINDOWS; - - /** - * Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde. - */ - public static final boolean LINUX; - - static { - final String name = System.getProperty("os.name"); - - if( name.contains("Mac") ) { - MACOS = true; - WINDOWS = false; - LINUX = false; - } else if( name.contains("Windows") ) { - MACOS = false; - WINDOWS = true; - LINUX = false; - } else if( name.equals("Linux") ) { // true for the ibm vm - MACOS = false; - WINDOWS = false; - LINUX = true; - } else { - MACOS = false; - WINDOWS = false; - LINUX = false; - } - } - /* * Objektvariablen, die von Unterklassen benutzt werden können. */ @@ -111,7 +75,7 @@ public class Zeichenmaschine extends Constants { /** * Das Zeichenfenster der Zeichenmaschine */ - private Zeichenfenster frame; + protected Zeichenfenster frame; // Aktueller Zustand der Zeichenmaschine. @@ -312,8 +276,15 @@ public class Zeichenmaschine extends Constants { 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 - // TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing( WindowEvent e ) { @@ -392,10 +363,18 @@ public class Zeichenmaschine extends Constants { 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(); } @@ -546,6 +525,11 @@ public class Zeichenmaschine extends Constants { } public final void exitNow() { + // Do nothing, when already quitting + if( state == Options.AppState.QUITING ) { + return; + } + if( running ) { running = false; terminateImediately = true; @@ -584,6 +568,7 @@ public class Zeichenmaschine extends Constants { * @see System#exit(int) */ public final void quit( boolean exit ) { + state = Options.AppState.QUITING; frame.setVisible(false); canvas.dispose(); frame.dispose(); @@ -1390,7 +1375,7 @@ public class Zeichenmaschine extends Constants { } // Display the current buffer content - if( canvas != null ) { + if( canvas != null && frame.isDisplayable() ) { canvas.render(); // canvas.invalidate(); // frame.repaint(); @@ -1547,6 +1532,7 @@ public class Zeichenmaschine extends Constants { } + // TODO: (ngb) exception handling when update/draw throws ex class UpdateThreadExecutor extends ThreadPoolExecutor { private Thread updateThread; @@ -1555,7 +1541,18 @@ public class Zeichenmaschine extends Constants { public UpdateThreadExecutor() { super(1, 1, 0L, - TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); + 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; }