tiling window manager example
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sat, 23 Feb 2019 16:46:43 +0000 (10:46 -0600)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sat, 23 Feb 2019 16:46:43 +0000 (10:46 -0600)
.gitignore
README.md
examples/JexerTilingWindowManager.java [new file with mode: 0644]

index 733399a10f68ceee79562c7ed7f8903fecad83f2..7c3b76b823ecc9cf8c3726eb3bc407cabcfffdf5 100644 (file)
@@ -26,3 +26,4 @@ misc/**
 
 pmd.bash
 pmd-results.html
+examples/*.sh
index 2a62f1c1eb6531b151b60b6aec531883b5e10593..39ae2a1cc9cc807d1f586f4fda1a0b469088955d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -137,9 +137,19 @@ it and you'll see an application like this:
 
 ![The Example Code Above](/screenshots/readme_application.png?raw=true "The application in the text of README.md")
 
-See the files in jexer.demos for many more detailed examples showing
-all of the existing UI controls.  The available demos can be run as
-follows:
+
+
+More Examples
+-------------
+
+The examples/ folder currently contains:
+
+  * A [prototype tiling window
+    manager](/examples/JavaTilingWindowManager.java) in less than 250
+    lines of code.
+
+jexer.demos contains official demos showing all of the existing UI
+controls.  The demos can be run as follows:
 
   * 'java -jar jexer.jar' .  This will use System.in/out with
     xterm-like sequences on non-Windows non-Mac platforms.  On Windows
diff --git a/examples/JexerTilingWindowManager.java b/examples/JexerTilingWindowManager.java
new file mode 100644 (file)
index 0000000..5b5740a
--- /dev/null
@@ -0,0 +1,223 @@
+import jexer.TApplication;
+import jexer.TTerminalWindow;
+import jexer.TWindow;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
+
+/**
+ * Implements a simple tiling window manager.  A root non-moveable
+ * non-resizable terminal window is created first, which can be split
+ * horizontally or vertically.  Each new window retains a reference to its
+ * "parent", and upon closing resizes that parent back to its original size.
+ *
+ * This example shows what can be done with minimal changes to stock Jexer
+ * widgets. You will quickly see that closing a "parent" tile does not cause
+ * the "child" tile to resize.  You could make a real subclass of
+ * TTerminalWindow that has extra fields and/or communicates more with
+ * JexerTilingWindowManager to get full coverage of tile creation,
+ * destruction, placement, movement, and so on.
+ */
+public class JexerTilingWindowManager extends TApplication {
+
+    /**
+     * Menu item: split the terminal vertically.
+     */
+    private static final int MENU_SPLIT_VERTICAL = 2000;
+
+    /**
+     * Menu item: split the terminal horizontally.
+     */
+    private static final int MENU_SPLIT_HORIZONTAL = 2001;
+
+    /**
+     * Main entry point.
+     */
+    public static void main(String [] args) throws Exception {
+        // For this application, we must use ptypipe so that the tile shells
+        // can be aware of their size.
+        System.setProperty("jexer.TTerminal.ptypipe", "true");
+
+        JexerTilingWindowManager jtwm = new JexerTilingWindowManager();
+        (new Thread(jtwm)).start();
+    }
+
+    /**
+     * Public constructor chooses the ECMA-48 / Xterm backend.
+     */
+    public JexerTilingWindowManager() throws Exception {
+        super(BackendType.XTERM);
+
+        // The stock tool menu has items for redrawing the screen, opening
+        // images, and (when using the Swing backend) setting the font.
+        addToolMenu();
+
+        // We will have one menu containing a mix of new and stock commands
+        TMenu tileMenu = addMenu("&Tile");
+
+        // New commands for this example: split vertical and horizontal.
+        tileMenu.addItem(MENU_SPLIT_VERTICAL, "&Vertical Split");
+        tileMenu.addItem(MENU_SPLIT_HORIZONTAL, "&Horizontal Split");
+
+        // Stock commands: a new shell with resizable window, previous, next,
+        // close, and exit program.
+        tileMenu.addItem(TMenu.MID_SHELL, "&Floating");
+        tileMenu.addSeparator();
+        tileMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
+        tileMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
+        tileMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
+        tileMenu.addSeparator();
+        tileMenu.addDefaultItem(TMenu.MID_EXIT);
+
+        // Spin up the root tile
+        TTerminalWindow rootTile = makeTile(0, 0, getScreen().getWidth(),
+            getDesktopBottom() - 1, null);
+
+        // Let's add some bling!  Enable focus-follows-mouse.
+        setFocusFollowsMouse(true);
+    }
+
+    /**
+     * Process menu events.
+     */
+    @Override
+    protected boolean onMenu(TMenuEvent event) {
+        if (event.getId() == MENU_SPLIT_VERTICAL) {
+            splitVertical();
+            return true;
+        }
+        if (event.getId() == MENU_SPLIT_HORIZONTAL) {
+            splitHorizontal();
+            return true;
+        }
+
+        return super.onMenu(event);
+    }
+
+    /**
+     * Perform the vertical split.
+     */
+    private void splitVertical() {
+        TWindow window = getActiveWindow();
+        if (!(window instanceof TTerminalWindow)) {
+            return;
+        }
+
+        TTerminalWindow tile = (TTerminalWindow) window;
+        // Give the extra column to the new tile.
+        int newWidth = (tile.getWidth() + 1) / 2;
+        int newY = tile.getY() - 1;
+        int newX = tile.getX() + tile.getWidth() - newWidth;
+        makeTile(newX, newY, newWidth, tile.getHeight(), tile);
+        tile.setWidth(tile.getWidth() - newWidth);
+        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                tile.getWidth(), tile.getHeight()));
+    }
+
+    /**
+     * Perform the horizontal split.
+     */
+    private void splitHorizontal() {
+        TWindow window = getActiveWindow();
+        if (!(window instanceof TTerminalWindow)) {
+            return;
+        }
+
+        TTerminalWindow tile = (TTerminalWindow) window;
+        // Give the extra row to the new tile.
+        int newHeight = (tile.getHeight() + 1) / 2;
+        int newY = tile.getY() - 1 + tile.getHeight() - newHeight;
+        int newX = tile.getX();
+        makeTile(newX, newY, tile.getWidth(), newHeight, tile);
+        tile.setHeight(tile.getHeight() - newHeight);
+        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                tile.getWidth(), tile.getHeight()));
+    }
+
+    /**
+     * Create a non-resizable non-movable terminal window.
+     *
+     * @param x the column number to place the top-left corner at.  0 is the
+     * left-most column.
+     * @param y the row number to place the top-left corner at.  0 is the
+     * top-most column.
+     * @param width the width of the window
+     * @param height the height of the window
+     * @param otherTile the other tile to resize when this window closes
+     */
+    private TTerminalWindow makeTile(int x, int y, int width, int height,
+        final TTerminalWindow otherTile) {
+
+        // We pass flags to disable the zoom (maximize) button, disable
+        // "smart" window placement, and set the specific location.
+        TTerminalWindow tile = new TTerminalWindow(this, x, y,
+            TWindow.NOZOOMBOX | TWindow.ABSOLUTEXY,
+            new String[] { "/bin/bash", "--login" }, true) {
+
+            /**
+             * When this terminal closes, if otherTile is defined then resize
+             * it to overcover me.
+             */
+            @Override
+            public void onClose() {
+                super.onClose();
+
+                if (otherTile != null) {
+                    if (otherTile.getX() != getX()) {
+                        // Undo the vertical split
+                        otherTile.setX(Math.min(otherTile.getX(), getX()));
+                        otherTile.setWidth(otherTile.getWidth() + getWidth());
+                    }
+                    if (otherTile.getY() != getY()) {
+                        otherTile.setY(Math.min(otherTile.getY(), getY()));
+                        otherTile.setHeight(otherTile.getHeight() + getHeight());
+                    }
+                    otherTile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                            otherTile.getWidth(), otherTile.getHeight()));
+                }
+            }
+
+            /**
+             * Prevent the user from resizing or moving this window.
+             */
+            @Override
+            public void onMouseDown(final TMouseEvent mouse) {
+                super.onMouseDown(mouse);
+                stopMovements();
+            }
+
+            /**
+             * Prevent the user from resizing or moving this window.
+             */
+            @Override
+            public void onKeypress(final TKeypressEvent keypress) {
+                super.onKeypress(keypress);
+                stopMovements();
+            }
+
+            /**
+             * Permit the user to use all of the menu items.
+             */
+            @Override
+            public void onIdle() {
+                super.onIdle();
+                removeShortcutKeypress(jexer.TKeypress.kbAltT);
+                removeShortcutKeypress(jexer.TKeypress.kbF6);
+            }
+
+        };
+
+        // The initial window size was stock VT100 80x24.  Change that now,
+        // and then call onResize() to notify ptypipe to set the shell's
+        // window size.
+        tile.setWidth(width);
+        tile.setHeight(height);
+        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                tile.getWidth(), tile.getHeight()));
+
+        return tile;
+    }
+
+}