From 3e0743556d1f31723a11a6019b5c2b018b4b2104 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Tue, 8 Aug 2017 13:09:57 -0400 Subject: [PATCH] TWindowBackend --- docs/TODO.md | 9 +- docs/worklog.md | 6 + src/jexer/TApplication.java | 12 +- src/jexer/TKeypress.java | 11 + src/jexer/backend/ECMA48Backend.java | 12 + src/jexer/backend/ECMA48Terminal.java | 34 +- src/jexer/backend/LogicalScreen.java | 42 +++ src/jexer/backend/MultiBackend.java | 22 +- src/jexer/backend/MultiScreen.java | 38 +++ src/jexer/backend/Screen.java | 30 ++ src/jexer/backend/SwingBackend.java | 31 ++ src/jexer/backend/SwingSessionInfo.java | 19 ++ src/jexer/backend/SwingTerminal.java | 60 ++-- src/jexer/backend/TSessionInfo.java | 18 + src/jexer/backend/TWindowBackend.java | 416 ++++++++++++++++++++++++ src/jexer/demos/Demo5.java | 48 ++- src/jexer/demos/Demo6.java | 80 ++++- src/jexer/event/TKeypressEvent.java | 10 + src/jexer/event/TMouseEvent.java | 29 ++ 19 files changed, 865 insertions(+), 62 deletions(-) create mode 100644 src/jexer/backend/TWindowBackend.java diff --git a/docs/TODO.md b/docs/TODO.md index 64ec8543..08a912aa 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -10,10 +10,6 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the 0.0.5 -- Multiscreen support: - - cmAbort to cmScreenDisconnected - - cmScreenConnected - - TApplication - getAllWindows() - Expose menu management functions (addMenu, getMenu, getAllMenus, @@ -37,6 +33,11 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the 0.0.6 +- Finish up multiscreen support: + - cmAbort to cmScreenDisconnected + - cmScreenConnected + - Handle screen resizes + - TSpinner - TComboBox - TCalendar diff --git a/docs/worklog.md b/docs/worklog.md index 2573fb2c..2d1ae20f 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,12 @@ Jexer Work Log ============== +August 8, 2017 + +Multiscreen is looking really cool! Demo6 now brings up three +screens, including one that is inside a TWindow of a different +application. + August 7, 2017 Had trouble sleeping, what with a bunch of imaginative thoughts for diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index c783d8d0..ab9c1962 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -54,6 +54,7 @@ import jexer.backend.Backend; import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; +import jexer.backend.TWindowBackend; import jexer.menu.TMenu; import jexer.menu.TMenuItem; import static jexer.TCommand.*; @@ -405,7 +406,15 @@ public class TApplication implements Runnable { * @return the Screen */ public final Screen getScreen() { - return backend.getScreen(); + if (backend instanceof TWindowBackend) { + // We are being rendered to a TWindow. We can't use its + // getScreen() method because that is how it is rendering to a + // hardware backend somewhere. Instead use its getOtherScreen() + // method. + return ((TWindowBackend) backend).getOtherScreen(); + } else { + return backend.getScreen(); + } } /** @@ -719,6 +728,7 @@ public class TApplication implements Runnable { */ public TApplication(final Backend backend) { this.backend = backend; + backend.setListener(this); TApplicationImpl(); } diff --git a/src/jexer/TKeypress.java b/src/jexer/TKeypress.java index 34b286fe..872ebc45 100644 --- a/src/jexer/TKeypress.java +++ b/src/jexer/TKeypress.java @@ -275,6 +275,17 @@ public final class TKeypress { this.shift = shift; } + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public TKeypress dup() { + TKeypress keypress = new TKeypress(isFunctionKey, keyCode, ch, + alt, ctrl, shift); + return keypress; + } + /** * Comparison check. All fields must match to return true. * diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index db390fb4..3e588f99 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -40,6 +40,18 @@ import java.io.UnsupportedEncodingException; */ public final class ECMA48Backend extends GenericBackend { + /** + * Public constructor will use System.in and System.out and UTF-8 + * encoding. On non-Windows systems System.in will be put in raw mode; + * shutdown() will (blindly!) put System.in in cooked mode. + * + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader + */ + public ECMA48Backend() throws UnsupportedEncodingException { + this(null, null, null); + } + /** * Public constructor. * diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 6303f4fc..78aae45c 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -352,6 +352,11 @@ public final class ECMA48Terminal extends LogicalScreen this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); + // Query the screen size + sessionInfo.queryWindowSize(); + setDimensions(sessionInfo.getWindowWidth(), + sessionInfo.getWindowHeight()); + // Hang onto the window size windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); @@ -360,6 +365,8 @@ public final class ECMA48Terminal extends LogicalScreen if (System.getProperty("jexer.ECMA48.rgbColor") != null) { if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) { doRgbColor = true; + } else { + doRgbColor = false; } } @@ -368,10 +375,6 @@ public final class ECMA48Terminal extends LogicalScreen readerThread = new Thread(this); readerThread.start(); - // Query the screen size - setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); - // Clear the screen this.output.write(clearAll()); this.output.flush(); @@ -439,6 +442,11 @@ public final class ECMA48Terminal extends LogicalScreen this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); + // Query the screen size + sessionInfo.queryWindowSize(); + setDimensions(sessionInfo.getWindowWidth(), + sessionInfo.getWindowHeight()); + // Hang onto the window size windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); @@ -447,6 +455,8 @@ public final class ECMA48Terminal extends LogicalScreen if (System.getProperty("jexer.ECMA48.rgbColor") != null) { if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) { doRgbColor = true; + } else { + doRgbColor = false; } } @@ -455,10 +465,6 @@ public final class ECMA48Terminal extends LogicalScreen readerThread = new Thread(this); readerThread.start(); - // Query the screen size - setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); - // Clear the screen this.output.write(clearAll()); this.output.flush(); @@ -1892,8 +1898,10 @@ public final class ECMA48Terminal extends LogicalScreen synchronized (eventQueue) { eventQueue.addAll(events); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } events.clear(); } @@ -1905,8 +1913,10 @@ public final class ECMA48Terminal extends LogicalScreen eventQueue.addAll(events); } events.clear(); - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index 8432e4ea..bfe1c722 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -229,6 +229,21 @@ public class LogicalScreen implements Screen { return attr; } + /** + * Get the cell at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return the character + attributes + */ + public Cell getCharXY(final int x, final int y) { + Cell cell = new Cell(); + if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) { + cell.setTo(logical[x][y]); + } + return cell; + } + /** * Set the attributes at one location. * @@ -771,6 +786,33 @@ public class LogicalScreen implements Screen { cursorVisible = false; } + /** + * Get the cursor visibility. + * + * @return true if the cursor is visible + */ + public boolean isCursorVisible() { + return cursorVisible; + } + + /** + * Get the cursor X position. + * + * @return the cursor x column position + */ + public int getCursorX() { + return cursorX; + } + + /** + * Get the cursor Y position. + * + * @return the cursor y row position + */ + public int getCursorY() { + return cursorY; + } + /** * Set the window title. Default implementation does nothing. * diff --git a/src/jexer/backend/MultiBackend.java b/src/jexer/backend/MultiBackend.java index a397c655..9166e1c1 100644 --- a/src/jexer/backend/MultiBackend.java +++ b/src/jexer/backend/MultiBackend.java @@ -56,7 +56,11 @@ public class MultiBackend implements Backend { */ public MultiBackend(final Backend backend) { backends.add(backend); - multiScreen = new MultiScreen(backend.getScreen()); + if (backend instanceof TWindowBackend) { + multiScreen = new MultiScreen(((TWindowBackend) backend).getOtherScreen()); + } else { + multiScreen = new MultiScreen(backend.getScreen()); + } } /** @@ -66,7 +70,11 @@ public class MultiBackend implements Backend { */ public void addBackend(final Backend backend) { backends.add(backend); - multiScreen.addScreen(backend.getScreen()); + if (backend instanceof TWindowBackend) { + multiScreen.addScreen(((TWindowBackend) backend).getOtherScreen()); + } else { + multiScreen.addScreen(backend.getScreen()); + } } /** @@ -76,7 +84,11 @@ public class MultiBackend implements Backend { */ public void removeBackend(final Backend backend) { if (backends.size() > 1) { - multiScreen.removeScreen(backend.getScreen()); + if (backend instanceof TWindowBackend) { + multiScreen.removeScreen(((TWindowBackend) backend).getOtherScreen()); + } else { + multiScreen.removeScreen(backend.getScreen()); + } backends.remove(backend); } } @@ -86,7 +98,7 @@ public class MultiBackend implements Backend { * * @return the SessionInfo */ - public final SessionInfo getSessionInfo() { + public SessionInfo getSessionInfo() { return backends.get(0).getSessionInfo(); } @@ -95,7 +107,7 @@ public class MultiBackend implements Backend { * * @return the Screen */ - public final Screen getScreen() { + public Screen getScreen() { return multiScreen; } diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java index f7b61ddf..209105d9 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -196,6 +196,17 @@ public class MultiScreen implements Screen { return screens.get(0).getAttrXY(x, y); } + /** + * Get the cell at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return the character + attributes + */ + public Cell getCharXY(final int x, final int y) { + return screens.get(0).getCharXY(x, y); + } + /** * Set the attributes at one location. * @@ -520,6 +531,33 @@ public class MultiScreen implements Screen { } } + /** + * Get the cursor visibility. + * + * @return true if the cursor is visible + */ + public boolean isCursorVisible() { + return screens.get(0).isCursorVisible(); + } + + /** + * Get the cursor X position. + * + * @return the cursor x column position + */ + public int getCursorX() { + return screens.get(0).getCursorX(); + } + + /** + * Get the cursor Y position. + * + * @return the cursor y row position + */ + public int getCursorY() { + return screens.get(0).getCursorY(); + } + /** * Set the window title. * diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java index 9b5890fc..0fbbea4c 100644 --- a/src/jexer/backend/Screen.java +++ b/src/jexer/backend/Screen.java @@ -123,6 +123,15 @@ public interface Screen { */ public CellAttributes getAttrXY(final int x, final int y); + /** + * Get the cell at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return the character + attributes + */ + public Cell getCharXY(final int x, final int y); + /** * Set the attributes at one location. * @@ -345,6 +354,27 @@ public interface Screen { */ public void hideCursor(); + /** + * Get the cursor visibility. + * + * @return true if the cursor is visible + */ + public boolean isCursorVisible(); + + /** + * Get the cursor X position. + * + * @return the cursor x column position + */ + public int getCursorX(); + + /** + * Get the cursor Y position. + * + * @return the cursor y row position + */ + public int getCursorY(); + /** * Set the window title. * diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index fc779681..281670d1 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -37,6 +37,13 @@ import javax.swing.JComponent; */ public final class SwingBackend extends GenericBackend { + /** + * Public constructor. The window will be 80x25 with font size 20 pts. + */ + public SwingBackend() { + this(null, 80, 25, 20); + } + /** * Public constructor. The window will be 80x25 with font size 20 pts. * @@ -47,6 +54,30 @@ public final class SwingBackend extends GenericBackend { this(listener, 80, 25, 20); } + /** + * Public constructor will spawn a new JFrame with font size 20 pts. + * + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + */ + public SwingBackend(final int windowWidth, final int windowHeight) { + this(null, windowWidth, windowHeight, 20); + } + + /** + * Public constructor will spawn a new JFrame. + * + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @param fontSize the size in points. Good values to pick are: 16, 20, + * 22, and 24. + */ + public SwingBackend(final int windowWidth, final int windowHeight, + final int fontSize) { + + this(null, windowWidth, windowHeight, fontSize); + } + /** * Public constructor will spawn a new JFrame. * diff --git a/src/jexer/backend/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java index 7df751d8..7457f57a 100644 --- a/src/jexer/backend/SwingSessionInfo.java +++ b/src/jexer/backend/SwingSessionInfo.java @@ -154,6 +154,25 @@ public final class SwingSessionInfo implements SessionInfo { this.textHeight = textHeight; } + /** + * Public constructor. + * + * @param swing the Swing JFrame or JComponent + * @param textWidth the width of a cell in pixels + * @param textHeight the height of a cell in pixels + * @param width the number of columns + * @param height the number of rows + */ + public SwingSessionInfo(final SwingComponent swing, final int textWidth, + final int textHeight, final int width, final int height) { + + this.swing = swing; + this.textWidth = textWidth; + this.textHeight = textHeight; + this.windowWidth = width; + this.windowHeight = height; + } + /** * Re-query the text window size. */ diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index c2fbfb43..6e902195 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -500,7 +500,6 @@ public final class SwingTerminal extends LogicalScreen // monospace. textHeight = fm.getMaxAscent() + maxDescent - leading; - // TODO: is this still necessary? if (gotTerminus == true) { textHeight++; } @@ -1069,9 +1068,9 @@ public final class SwingTerminal extends LogicalScreen // Pull the system property for triple buffering. if (System.getProperty("jexer.Swing.tripleBuffer") != null) { - if (System.getProperty("jexer.Swing.tripleBuffer"). - equals("false")) { - + if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) { + SwingComponent.tripleBuffer = true; + } else { SwingComponent.tripleBuffer = false; } } @@ -1142,7 +1141,8 @@ public final class SwingTerminal extends LogicalScreen SwingTerminal.this.sessionInfo = new SwingSessionInfo(SwingTerminal.this.swing, SwingTerminal.this.textWidth, - SwingTerminal.this.textHeight); + SwingTerminal.this.textHeight, + windowWidth, windowHeight); SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); @@ -1171,7 +1171,7 @@ public final class SwingTerminal extends LogicalScreen } /** - * Public constructor creates a new JFrame to render to. + * Public constructor renders to an existing JComponent. * * @param component the Swing component to render to * @param windowWidth the number of text columns to start with @@ -1540,8 +1540,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(new TKeypressEvent(keypress)); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1576,8 +1578,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(new TCommandEvent(cmAbort)); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1664,8 +1668,10 @@ public final class SwingTerminal extends LogicalScreen sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); eventQueue.add(windowResize); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1700,8 +1706,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(mouseEvent); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1726,8 +1734,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(mouseEvent); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1789,8 +1799,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(mouseEvent); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1834,8 +1846,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(mouseEvent); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } @@ -1878,8 +1892,10 @@ public final class SwingTerminal extends LogicalScreen synchronized (eventQueue) { eventQueue.add(mouseEvent); } - synchronized (listener) { - listener.notifyAll(); + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } } } diff --git a/src/jexer/backend/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java index 880098d7..bc321753 100644 --- a/src/jexer/backend/TSessionInfo.java +++ b/src/jexer/backend/TSessionInfo.java @@ -115,4 +115,22 @@ public final class TSessionInfo implements SessionInfo { // NOP } + /** + * Public constructor. + */ + public TSessionInfo() { + this(80, 24); + } + + /** + * Public constructor. + * + * @param width the number of columns + * @param height the number of rows + */ + public TSessionInfo(final int width, final int height) { + this.windowWidth = width; + this.windowHeight = height; + } + } diff --git a/src/jexer/backend/TWindowBackend.java b/src/jexer/backend/TWindowBackend.java new file mode 100644 index 00000000..7de6229a --- /dev/null +++ b/src/jexer/backend/TWindowBackend.java @@ -0,0 +1,416 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.util.LinkedList; +import java.util.List; + +import jexer.bits.CellAttributes; +import jexer.event.TInputEvent; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.TApplication; +import jexer.TWindow; + +/** + * TWindowBackend uses a window in one TApplication to provide a backend for + * another TApplication. + * + * Note that TWindow has its own getScreen() and setTitle() functions. + * Clients in TWindowBackend's application won't be able to use it to get at + * the other application's screen. getOtherScreen() has been provided. + */ +public class TWindowBackend extends TWindow implements Backend { + + /** + * The listening object that run() wakes up on new input. + */ + private Object listener; + + /** + * The object to sync on in draw(). This is normally otherScreen, but it + * could also be a MultiScreen. + */ + private Object drawLock; + + /** + * The event queue, filled up by a thread reading on input. + */ + private List eventQueue; + + /** + * The screen to use. + */ + private Screen otherScreen; + + /** + * The mouse X position as seen on the other screen. + */ + private int otherMouseX = -1; + + /** + * The mouse Y position as seen on the other screen. + */ + private int otherMouseY = -1; + + /** + * The session information. + */ + private SessionInfo sessionInfo; + + /** + * Set the object to sync to in draw(). + * + * @param drawLock the object to synchronize on + */ + public void setDrawLock(final Object drawLock) { + this.drawLock = drawLock; + } + + /** + * Getter for the other application's screen. + * + * @return the Screen + */ + public Screen getOtherScreen() { + return otherScreen; + } + + /** + * Getter for sessionInfo. + * + * @return the SessionInfo + */ + public final SessionInfo getSessionInfo() { + return sessionInfo; + } + + /** + * Public constructor. Window will be located at (0, 0). + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param application TApplication that manages this window + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + */ + public TWindowBackend(final Object listener, + final TApplication application, final String title, + final int width, final int height) { + + super(application, title, width, height); + + this.listener = listener; + eventQueue = new LinkedList(); + sessionInfo = new TSessionInfo(width, height); + otherScreen = new LogicalScreen(); + otherScreen.setDimensions(width - 2, height - 2); + drawLock = otherScreen; + } + + /** + * Public constructor. Window will be located at (0, 0). + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param application TApplication that manages this window + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + * @param flags bitmask of RESIZABLE, CENTERED, or MODAL + */ + public TWindowBackend(final Object listener, + final TApplication application, final String title, + final int width, final int height, final int flags) { + + super(application, title, width, height, flags); + + this.listener = listener; + eventQueue = new LinkedList(); + sessionInfo = new TSessionInfo(width, height); + otherScreen = new LogicalScreen(); + otherScreen.setDimensions(width - 2, height - 2); + drawLock = otherScreen; + } + + /** + * Public constructor. + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param application TApplication that manages this window + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + */ + public TWindowBackend(final Object listener, + final TApplication application, final String title, + final int x, final int y, final int width, final int height) { + + super(application, title, x, y, width, height); + + this.listener = listener; + eventQueue = new LinkedList(); + sessionInfo = new TSessionInfo(width, height); + otherScreen = new LogicalScreen(); + otherScreen.setDimensions(width - 2, height - 2); + drawLock = otherScreen; + } + + /** + * Public constructor. + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param application TApplication that manages this window + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + * @param flags mask of RESIZABLE, CENTERED, or MODAL + */ + public TWindowBackend(final Object listener, + final TApplication application, final String title, + final int x, final int y, final int width, final int height, + final int flags) { + + super(application, title, x, y, width, height, flags); + + this.listener = listener; + eventQueue = new LinkedList(); + sessionInfo = new TSessionInfo(width, height); + otherScreen = new LogicalScreen(); + otherScreen.setDimensions(width - 2, height - 2); + drawLock = otherScreen; + } + + /** + * Subclasses must provide an implementation that syncs the logical + * screen to the physical device. + */ + public void flushScreen() { + // NOP + } + + /** + * Subclasses must provide an implementation to get keyboard, mouse, and + * screen resize events. + * + * @param queue list to append new events to + */ + public void getEvents(List queue) { + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + synchronized (queue) { + queue.addAll(eventQueue); + } + eventQueue.clear(); + } + } + } + + /** + * Subclasses must provide an implementation that closes sockets, + * restores console, etc. + */ + public void shutdown() { + // NOP + } + + /** + * Set listener to a different Object. + * + * @param listener the new listening object that run() wakes up on new + * input + */ + public void setListener(final Object listener) { + this.listener = listener; + } + + /** + * Draw the foreground colors grid. + */ + @Override + public void draw() { + + // Sync on other screen, so that we do not draw in the middle of + // their screen update. + synchronized (drawLock) { + // Draw the box + super.draw(); + + // Draw every cell of the other screen + for (int y = 0; y < otherScreen.getHeight(); y++) { + for (int x = 0; x < otherScreen.getWidth(); x++) { + putCharXY(x + 1, y + 1, otherScreen.getCharXY(x, y)); + } + } + + // If the mouse pointer is over the other window, draw its + // pointer again here. (Their TApplication drew it, then our + // TApplication drew it again (undo-ing it), so now we draw it a + // third time so that it is visible.) + if ((otherMouseX != -1) && (otherMouseY != -1)) { + CellAttributes attr = getAttrXY(otherMouseX, otherMouseY); + attr.setForeColor(attr.getForeColor().invert()); + attr.setBackColor(attr.getBackColor().invert()); + putAttrXY(otherMouseX, otherMouseY, attr, false); + } + + // If their cursor is visible, draw that here too. + if (otherScreen.isCursorVisible()) { + setCursorX(otherScreen.getCursorX() + 1); + setCursorY(otherScreen.getCursorY() + 1); + setCursorVisible(true); + } else { + setCursorVisible(false); + } + } + } + + /** + * Subclasses should override this method to cleanup resources. This is + * called by application.closeWindow(). + */ + public void onClose() { + // TODO: send a screen disconnect + } + + /** + * Returns true if the mouse is currently in the otherScreen window. + * + * @param mouse mouse event + * @return true if mouse is currently in the otherScreen window. + */ + protected boolean mouseOnOtherScreen(final TMouseEvent mouse) { + if ((mouse.getY() >= 1) + && (mouse.getY() <= otherScreen.getHeight()) + && (mouse.getX() >= 1) + && (mouse.getX() <= otherScreen.getWidth()) + ) { + return true; + } + return false; + } + + /** + * Handle mouse button presses. + * + * @param mouse mouse button event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouseOnOtherScreen(mouse)) { + TMouseEvent event = mouse.dup(); + event.setX(mouse.getX() - 1); + event.setY(mouse.getY() - 1); + event.setAbsoluteX(event.getX()); + event.setAbsoluteY(event.getY()); + synchronized (eventQueue) { + eventQueue.add(event); + } + synchronized (listener) { + listener.notifyAll(); + } + } + super.onMouseDown(mouse); + } + + /** + * Handle mouse button releases. + * + * @param mouse mouse button release event + */ + @Override + public void onMouseUp(final TMouseEvent mouse) { + if (mouseOnOtherScreen(mouse)) { + TMouseEvent event = mouse.dup(); + event.setX(mouse.getX() - 1); + event.setY(mouse.getY() - 1); + event.setAbsoluteX(event.getX()); + event.setAbsoluteY(event.getY()); + synchronized (eventQueue) { + eventQueue.add(event); + } + synchronized (listener) { + listener.notifyAll(); + } + } + super.onMouseUp(mouse); + } + + /** + * Handle mouse movements. + * + * @param mouse mouse motion event + */ + @Override + public void onMouseMotion(final TMouseEvent mouse) { + if (mouseOnOtherScreen(mouse)) { + TMouseEvent event = mouse.dup(); + event.setX(mouse.getX() - 1); + event.setY(mouse.getY() - 1); + event.setAbsoluteX(event.getX()); + event.setAbsoluteY(event.getY()); + otherMouseX = event.getX() + 1; + otherMouseY = event.getY() + 2; + synchronized (eventQueue) { + eventQueue.add(event); + } + synchronized (listener) { + listener.notifyAll(); + } + } else { + otherMouseX = -1; + otherMouseY = -1; + } + super.onMouseMotion(mouse); + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + TKeypressEvent event = keypress.dup(); + synchronized (eventQueue) { + eventQueue.add(event); + } + synchronized (listener) { + listener.notifyAll(); + } + } + +} diff --git a/src/jexer/demos/Demo5.java b/src/jexer/demos/Demo5.java index 8763aa1f..df8075a9 100644 --- a/src/jexer/demos/Demo5.java +++ b/src/jexer/demos/Demo5.java @@ -127,29 +127,55 @@ public class Demo5 implements WindowListener { */ private void addApplications() { - // Spin up the frame - JFrame frame = new JFrame(); - frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - frame.addWindowListener(this); - - // Create two panels with two applications, each with a different - // font size. + /* + * In this demo we will create two swing panels with two + * independently running applications, each with a different font + * size. + */ + + /* + * First we create a panel to put it on. We need this to pass to + * SwingBackend's constructor, so that it knows not to create a new + * frame. + */ JPanel app1Panel = new JPanel(); - SwingBackend app1Backend = new SwingBackend(app1Panel, new Object(), + + /* + * Next, we create the Swing backend. The "listener" (second + * argument, set to null) is what the backend wakes up on every event + * received. Typically this is the TApplication. TApplication sets + * it in its constructor, so we can pass null here and be fine. + */ + SwingBackend app1Backend = new SwingBackend(app1Panel, null, 80, 25, 16); + // Now that we have the backend, construct the TApplication. app1 = new DemoApplication(app1Backend); - app1Backend.setListener(app1); + /* + * The second panel is the same sequence, except that we also change + * the font from the default Terminus to JVM monospaced. + */ JPanel app2Panel = new JPanel(); - SwingBackend app2Backend = new SwingBackend(app2Panel, new Object(), + SwingBackend app2Backend = new SwingBackend(app2Panel, null, 80, 25, 18); app2 = new DemoApplication(app2Backend); Font font = new Font(Font.MONOSPACED, Font.PLAIN, 18); app2Backend.setFont(font); - app2Backend.setListener(app2); + + /* + * Now that the applications are ready, spin them off on their + * threads. + */ (new Thread(app1)).start(); (new Thread(app2)).start(); + /* + * The rest of this is standard Swing. Set up a frame, a split pane, + * put each of the panels on it, and make it visible. + */ + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.addWindowListener(this); JSplitPane mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, app1Panel, app2Panel); mainPane.setOneTouchExpandable(true); diff --git a/src/jexer/demos/Demo6.java b/src/jexer/demos/Demo6.java index fba67a07..c0ec4273 100644 --- a/src/jexer/demos/Demo6.java +++ b/src/jexer/demos/Demo6.java @@ -28,6 +28,7 @@ */ package jexer.demos; +import jexer.TApplication; import jexer.backend.*; /** @@ -42,22 +43,87 @@ public class Demo6 { */ public static void main(final String [] args) { try { + + /* + * In this demo we will create two applications spanning three + * screens. One of the applications will have both an ECMA48 + * screen and a Swing screen, with all I/O mirrored between them. + * The second application will have a Swing screen containing a + * window showing the first application, also mirroring I/O + * between the window and the other two screens. + */ + /* - * Spin up a Swing backend to match the ECMA48 backend on - * System.in/out. + * We create the first screen and use it to establish a + * MultiBackend. */ - ECMA48Backend ecmaBackend = new ECMA48Backend(new Object(), null, - null); + ECMA48Backend ecmaBackend = new ECMA48Backend(); MultiBackend multiBackend = new MultiBackend(ecmaBackend); + + /* + * Now we create the first application (a standard demo). + */ DemoApplication demoApp = new DemoApplication(multiBackend); + + /* + * We will need the width and height of the ECMA48 screen, so get + * the Screen reference now. + */ Screen multiScreen = multiBackend.getScreen(); - SwingBackend swingBackend = new SwingBackend(new Object(), - multiScreen.getWidth(), multiScreen.getHeight(), 16); + /* + * Now we create the second screen (backend) for the first + * application. It will be the same size as the ECMA48 screen, + * with a font size of 16 points. + */ + SwingBackend swingBackend = new SwingBackend(multiScreen.getWidth(), + multiScreen.getHeight(), 16); + + /* + * Add this screen to the MultiBackend, and at this point we have + * one demo application spanning two physical screens. + */ multiBackend.addBackend(swingBackend); - multiBackend.setListener(demoApp); + /* + * Time for the second application. This one will have a single + * window mirroring the contents of the first application. Let's + * make it a little larger than the first application's + * width/height. + */ + int width = multiScreen.getWidth(); + int height = multiScreen.getHeight(); + + /* + * Make a new Swing window for the second application. + */ + SwingBackend monitorBackend = new SwingBackend(width + 5, + height + 5, 16); + + /* + * Setup the second application, give it the basic file and + * window menus. + */ + TApplication monitor = new TApplication(monitorBackend); + monitor.addFileMenu(); + monitor.addWindowMenu(); + + /* + * Now add the third screen to the first application. We want to + * change the object it locks on in its draw() method to the + * MultiScreen, that will dramatically reduce (not totally + * eliminate) screen tearing/artifacts. + */ + TWindowBackend windowBackend = new TWindowBackend(demoApp, + monitor, "Monitor Window", width + 2, height + 2); + windowBackend.setDrawLock(multiScreen); + multiBackend.addBackend(windowBackend); + + /* + * Three screens, two applications: spin them up! + */ (new Thread(demoApp)).start(); + (new Thread(monitor)).start(); } catch (Exception e) { e.printStackTrace(); } diff --git a/src/jexer/event/TKeypressEvent.java b/src/jexer/event/TKeypressEvent.java index 62c6be29..32a16158 100644 --- a/src/jexer/event/TKeypressEvent.java +++ b/src/jexer/event/TKeypressEvent.java @@ -89,6 +89,16 @@ public final class TKeypressEvent extends TInputEvent { alt, ctrl, shift); } + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public TKeypressEvent dup() { + TKeypressEvent keypress = new TKeypressEvent(key.dup()); + return keypress; + } + /** * Comparison check. All fields must match to return true. * diff --git a/src/jexer/event/TMouseEvent.java b/src/jexer/event/TMouseEvent.java index e098fa20..c29ebc35 100644 --- a/src/jexer/event/TMouseEvent.java +++ b/src/jexer/event/TMouseEvent.java @@ -135,6 +135,15 @@ public final class TMouseEvent extends TInputEvent { return absoluteX; } + /** + * Set absoluteX. + * + * @param absoluteX the new value + */ + public void setAbsoluteX(final int absoluteX) { + this.absoluteX = absoluteX; + } + /** * Mouse Y - absolute screen coordinate. */ @@ -149,6 +158,15 @@ public final class TMouseEvent extends TInputEvent { return absoluteY; } + /** + * Set absoluteY. + * + * @param absoluteY the new value + */ + public void setAbsoluteY(final int absoluteY) { + this.absoluteY = absoluteY; + } + /** * Mouse button 1 (left button). */ @@ -250,6 +268,17 @@ public final class TMouseEvent extends TInputEvent { this.mouseWheelDown = mouseWheelDown; } + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public TMouseEvent dup() { + TMouseEvent mouse = new TMouseEvent(type, x, y, absoluteX, absoluteY, + mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); + return mouse; + } + /** * Make human-readable description of this TMouseEvent. * -- 2.27.0