From: Kevin Lamonte Date: Sun, 18 Aug 2019 15:52:10 +0000 (-0500) Subject: TSplitPane initial X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=5ffeabccc177e9fdadb62002c6d3bf1f6ae650fa;p=fanfix-jexer.git TSplitPane initial --- diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 9c9c571..f86ca27 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -1296,6 +1296,8 @@ public class TApplication implements Runnable { } } else if (event instanceof TKeypressEvent) { dispatchToDesktop = false; + } else if (event instanceof TMenuEvent) { + dispatchToDesktop = false; } if (debugEvents) { diff --git a/src/jexer/TPanel.java b/src/jexer/TPanel.java index 1a8dab1..c38f8e1 100644 --- a/src/jexer/TPanel.java +++ b/src/jexer/TPanel.java @@ -28,6 +28,8 @@ */ package jexer; +import jexer.event.TResizeEvent; + /** * TPanel is an empty container for other widgets. */ @@ -56,6 +58,35 @@ public class TPanel extends TWidget { super(parent, x, y, width, height); } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Method that subclasses can override to handle window/screen resize + * events. + * + * @param resize resize event + */ + @Override + public void onResize(final TResizeEvent resize) { + if (resize.getType() == TResizeEvent.Type.WIDGET) { + if (getChildren().size() == 1) { + TWidget child = getChildren().get(0); + if ((child instanceof TSplitPane) + || (child instanceof TPanel) + ) { + child.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + resize.getWidth(), resize.getHeight())); + } + return; + } + } + + // Pass on to TWidget. + super.onResize(resize); + } + // ------------------------------------------------------------------------ // TWidget ---------------------------------------------------------------- // ------------------------------------------------------------------------ diff --git a/src/jexer/TSplitPane.java b/src/jexer/TSplitPane.java new file mode 100644 index 0000000..0a3443b --- /dev/null +++ b/src/jexer/TSplitPane.java @@ -0,0 +1,385 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 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; + +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TMenuEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import jexer.menu.TMenu; + +/** + * TSplitPane contains two widgets with a draggable horizontal or vertical + * bar between them. + */ +public class TSplitPane extends TWidget { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * If true, split vertically. If false, split horizontally. + */ + private boolean vertical = true; + + /** + * The location of the split bar, either as a column number for vertical + * split or a row number for horizontal split. + */ + private int split = 0; + + /** + * The widget on the left side. + */ + private TWidget left; + + /** + * The widget on the right side. + */ + private TWidget right; + + /** + * The widget on the top side. + */ + private TWidget top; + + /** + * The widget on the bottom side. + */ + private TWidget bottom; + + /** + * If true, we are in the middle of a split move. + */ + private boolean inSplitMove = false; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width width of widget + * @param height height of widget + * @param vertical if true, split vertically + */ + public TSplitPane(final TWidget parent, final int x, final int y, + final int width, final int height, final boolean vertical) { + + super(parent, x, y, width, height); + + this.vertical = vertical; + center(); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + if (event.getType() == TResizeEvent.Type.WIDGET) { + // Resize me + super.onResize(event); + + if (vertical && (split >= getWidth() - 2)) { + center(); + } else if (!vertical && (split >= getHeight() - 2)) { + center(); + } else { + layoutChildren(); + } + } + } + + /** + * Handle mouse button presses. + * + * @param mouse mouse button event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + + inSplitMove = false; + + if (mouse.isMouse1()) { + if (vertical) { + inSplitMove = (mouse.getX() == split); + } else { + inSplitMove = (mouse.getY() == split); + } + if (inSplitMove) { + return; + } + } + + // I didn't take it, pass it on to my children + super.onMouseDown(mouse); + } + + /** + * Handle mouse button releases. + * + * @param mouse mouse button release event + */ + @Override + public void onMouseUp(final TMouseEvent mouse) { + + if (inSplitMove && mouse.isMouse1()) { + // Stop moving split + inSplitMove = false; + return; + } + + // I didn't take it, pass it on to my children + super.onMouseUp(mouse); + } + + /** + * Handle mouse movements. + * + * @param mouse mouse motion event + */ + @Override + public void onMouseMotion(final TMouseEvent mouse) { + + if (inSplitMove) { + if (vertical) { + split = mouse.getX(); + split = Math.min(Math.max(1, split), getWidth() - 2); + } else { + split = mouse.getY(); + split = Math.min(Math.max(1, split), getHeight() - 2); + } + layoutChildren(); + return; + } + + // I didn't take it, pass it on to my children + super.onMouseMotion(mouse); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw me on screen. + */ + @Override + public void draw() { + CellAttributes attr = getTheme().getColor("tsplitpane"); + if (vertical) { + vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr); + // TODO: draw intersections of children + } else { + hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr); + // TODO: draw intersections of children + } + } + + // ------------------------------------------------------------------------ + // TSplitPane ------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get the widget on the left side. + * + * @return the widget on the left, or null if not set + */ + public TWidget getLeft() { + return left; + } + + /** + * Set the widget on the left side. + * + * @param left the widget to set, or null to remove + */ + public void setLeft(final TWidget left) { + if (!vertical) { + throw new IllegalArgumentException("cannot set left on " + + "horizontal split pane"); + } + if (left == null) { + remove(this.left); + this.left = null; + return; + } + this.left = left; + left.setParent(this, false); + onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), + getHeight())); + } + + /** + * Get the widget on the right side. + * + * @return the widget on the right, or null if not set + */ + public TWidget getRight() { + return right; + } + + /** + * Set the widget on the right side. + * + * @param right the widget to set, or null to remove + */ + public void setRight(final TWidget right) { + if (!vertical) { + throw new IllegalArgumentException("cannot set right on " + + "horizontal split pane"); + } + if (right == null) { + remove(this.right); + this.right = null; + return; + } + this.right = right; + right.setParent(this, false); + onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), + getHeight())); + } + + /** + * Get the widget on the top side. + * + * @return the widget on the top, or null if not set + */ + public TWidget getTop() { + return top; + } + + /** + * Set the widget on the top side. + * + * @param top the widget to set, or null to remove + */ + public void setTop(final TWidget top) { + if (vertical) { + throw new IllegalArgumentException("cannot set top on vertical " + + "split pane"); + } + if (top == null) { + remove(this.top); + this.top = null; + return; + } + this.top = top; + top.setParent(this, false); + onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), + getHeight())); + } + + /** + * Get the widget on the bottom side. + * + * @return the widget on the bottom, or null if not set + */ + public TWidget getBottom() { + return bottom; + } + + /** + * Set the widget on the bottom side. + * + * @param bottom the widget to set, or null to remove + */ + public void setBottom(final TWidget bottom) { + if (vertical) { + throw new IllegalArgumentException("cannot set bottom on " + + "vertical split pane"); + } + if (bottom == null) { + remove(this.bottom); + this.bottom = null; + return; + } + this.bottom = bottom; + bottom.setParent(this, false); + onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), + getHeight())); + } + + /** + * Layout the two child widgets. + */ + private void layoutChildren() { + if (vertical) { + if (left != null) { + left.setDimensions(0, 0, split, getHeight()); + left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + left.getWidth(), left.getHeight())); + } + if (right != null) { + right.setDimensions(split + 1, 0, getWidth() - split - 1, + getHeight()); + right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + right.getWidth(), right.getHeight())); + } + } else { + if (top != null) { + top.setDimensions(0, 0, getWidth(), split); + top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + top.getWidth(), top.getHeight())); + } + if (bottom != null) { + bottom.setDimensions(0, split + 1, getWidth(), + getHeight() - split - 1); + bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + bottom.getWidth(), bottom.getHeight())); + } + } + } + + /** + * Recenter the split to the middle of this split pane. + */ + public void center() { + if (vertical) { + split = getWidth() / 2; + } else { + split = getHeight() / 2; + } + layoutChildren(); + } + +} diff --git a/src/jexer/TText.java b/src/jexer/TText.java index 6fea16f..22bc4b8 100644 --- a/src/jexer/TText.java +++ b/src/jexer/TText.java @@ -225,8 +225,10 @@ public class TText extends TScrollableWidget { } else { line = ""; } - String formatString = "%-" + Integer.toString(getWidth() - 1) + "s"; - putStringXY(0, topY, String.format(formatString, line), color); + if (getWidth() > 3) { + String formatString = "%-" + Integer.toString(getWidth() - 1) + "s"; + putStringXY(0, topY, String.format(formatString, line), color); + } topY++; if (topY >= (getHeight() - 1)) { diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index d48ffe4..bcf798a 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -611,6 +611,61 @@ public abstract class TWidget implements Comparable { * @param menu menu event */ public void onMenu(final TMenuEvent menu) { + + // Special case: if a split command comes in, insert a TPanel and + // TSplitPane in the hierarchy here. + TPanel panel = null; + TSplitPane pane = null; + List widgets = null; + switch (menu.getId()) { + case TMenu.MID_SPLIT_VERTICAL: + panel = new TPanel(null, x, y, width, height); + pane = new TSplitPane(null, x, y, width, height, true); + widgets = new ArrayList(children); + for (TWidget w: widgets) { + w.setParent(panel, false); + } + children.clear(); + pane.setParent(this, false); + pane.setLeft(panel); + activate(pane); + for (TWidget w: widgets) { + assert (w.window != null); + assert (w.parent != null); + } + assert (pane.getWindow() != null); + assert (pane.getParent() != null); + assert (panel.getWindow() != null); + assert (panel.getParent() != null); + assert (pane.isActive() == true); + assert (panel.isActive() == true); + return; + case TMenu.MID_SPLIT_HORIZONTAL: + panel = new TPanel(null, x, y, width, height); + pane = new TSplitPane(null, x, y, width, height, false); + widgets = new ArrayList(children); + for (TWidget w: widgets) { + w.setParent(panel, false); + } + children.clear(); + pane.setParent(this, false); + pane.setTop(panel); + activate(pane); + for (TWidget w: widgets) { + assert (w.window != null); + assert (w.parent != null); + } + assert (pane.getWindow() != null); + assert (pane.getParent() != null); + assert (panel.getWindow() != null); + assert (panel.getParent() != null); + assert (pane.isActive() == true); + assert (panel.isActive() == true); + return; + default: + break; + } + // Default: do nothing, pass to children instead for (TWidget widget: children) { widget.onMenu(menu); @@ -729,6 +784,15 @@ public abstract class TWidget implements Comparable { } } + /** + * Remove a child widget from this container. + * + * @param child the child widget to remove + */ + public final void remove(final TWidget child) { + remove(child, true); + } + /** * Remove a child widget from this container. * @@ -746,6 +810,7 @@ public abstract class TWidget implements Comparable { } children.remove(child); child.parent = null; + child.window = null; if (layout != null) { layout.remove(this); } @@ -763,29 +828,31 @@ public abstract class TWidget implements Comparable { if (parent != null) { parent.remove(this, doClose); + window = null; } assert (parent == null); - window = newParent.window; - newParent.addChild(this); + assert (window == null); + parent = newParent; + setWindow(parent.window); + parent.addChild(this); } /** - * Set this widget's window to a specific window. Parent must already be - * null. Having a null parent with a specified window is only used - * within Jexer by TStatusBar because TApplication routes events directly - * to it and calls its draw() method. Any other non-parented widgets - * will require similar special case functionality to receive events or - * be drawn to screen. + * Set this widget's window to a specific window. + * + * Having a null parent with a specified window is only used within Jexer + * by TStatusBar because TApplication routes events directly to it and + * calls its draw() method. Any other non-parented widgets will require + * similar special case functionality to receive events or be drawn to + * screen. * * @param window the window to use */ public final void setWindow(final TWindow window) { - - if (parent != null) { - throw new IllegalArgumentException("Cannot have different " + - "windows for parent and child"); - } this.window = window; + for (TWidget child: getChildren()) { + child.setWindow(window); + } } /** @@ -2507,4 +2574,20 @@ public abstract class TWidget implements Comparable { return new TPanel(this, x, y, width, height); } + /** + * Convenience function to add a split pane to this container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + * @param vertical if true, split vertically + * @return the new split pane + */ + public final TSplitPane addSplitPane(final int x, final int y, + final int width, final int height, final boolean vertical) { + + return new TSplitPane(this, x, y, width, height, vertical); + } + } diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 20f88d6..222efbc 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -849,6 +849,31 @@ public class TWindow extends TWidget { super.onMenu(menu); } + /** + * Method that subclasses can override to handle window/screen resize + * events. + * + * @param resize resize event + */ + @Override + public void onResize(final TResizeEvent resize) { + if (resize.getType() == TResizeEvent.Type.WIDGET) { + if (getChildren().size() == 1) { + TWidget child = getChildren().get(0); + if ((child instanceof TSplitPane) + || (child instanceof TPanel) + ) { + child.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + resize.getWidth() - 2, resize.getHeight() - 2)); + } + return; + } + } + + // Pass on to TWidget. + super.onResize(resize); + } + // ------------------------------------------------------------------------ // TWidget ---------------------------------------------------------------- // ------------------------------------------------------------------------ diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index ace1ebd..ffba4d4 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -666,6 +666,13 @@ public class ColorTheme { color.setBold(false); colors.put("ttable.border", color); + // TSplitPane + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(false); + colors.put("tsplitpane", color); + } /** diff --git a/src/jexer/demos/Demo8.java b/src/jexer/demos/Demo8.java new file mode 100644 index 0000000..d7b7501 --- /dev/null +++ b/src/jexer/demos/Demo8.java @@ -0,0 +1,121 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 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.demos; + +import java.util.ResourceBundle; + +import jexer.TApplication; +import jexer.TPanel; +import jexer.TSplitPane; +import jexer.TText; +import jexer.TWindow; +import jexer.layout.BoxLayoutManager; +import jexer.menu.TMenu; + +/** + * This class shows off TSplitPane. + */ +public class Demo8 { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo8.class.getName()); + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + private static String TEXT = +"This is an example of a reflowable text field. Some example text follows.\n" + +"\n" + +"Notice that some menu items should be disabled when this window has focus.\n" + +"\n" + +"This library implements a text-based windowing system loosely " + +"reminiscent of Borland's [Turbo " + +"Vision](http://en.wikipedia.org/wiki/Turbo_Vision) library. For those " + +"wishing to use the actual C++ Turbo Vision library, see [Sergio " + +"Sigala's updated version](http://tvision.sourceforge.net/) that runs " + +"on many more platforms.\n" + +"\n" + +"This library is licensed MIT. See the file LICENSE for the full license " + +"for the details.\n"; + + // ------------------------------------------------------------------------ + // Demo8 ------------------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Main entry point. + * + * @param args Command line arguments + */ + public static void main(final String [] args) throws Exception { + // This demo will build everything "from the outside". + + // Swing is the default backend on Windows unless explicitly + // overridden by jexer.Swing. + TApplication.BackendType backendType = TApplication.BackendType.XTERM; + if (System.getProperty("os.name").startsWith("Windows")) { + backendType = TApplication.BackendType.SWING; + } + if (System.getProperty("os.name").startsWith("Mac")) { + backendType = TApplication.BackendType.SWING; + } + if (System.getProperty("jexer.Swing") != null) { + if (System.getProperty("jexer.Swing", "false").equals("true")) { + backendType = TApplication.BackendType.SWING; + } else { + backendType = TApplication.BackendType.XTERM; + } + } + + // For this demo, let's disable the status bar. + System.setProperty("jexer.hideStatusBar", "true"); + + TApplication app = new TApplication(backendType); + app.addToolMenu(); + app.addFileMenu(); + TWindow window = new TWindow(app, i18n.getString("windowTitle"), + 60, 22); + + TMenu paneMenu = app.addMenu(i18n.getString("paneMenu")); + paneMenu.addDefaultItem(TMenu.MID_SPLIT_VERTICAL, true); + paneMenu.addDefaultItem(TMenu.MID_SPLIT_HORIZONTAL, true); + + TSplitPane pane = window.addSplitPane(0, 0, window.getWidth() - 2, + window.getHeight() - 2, true); + + pane.setLeft(new TText(null, TEXT, 0, 0, 10, 10)); + pane.setRight(new TText(null, TEXT, 0, 0, 10, 10)); + + app.run(); + } + +} diff --git a/src/jexer/demos/Demo8.properties b/src/jexer/demos/Demo8.properties new file mode 100644 index 0000000..2a94ca1 --- /dev/null +++ b/src/jexer/demos/Demo8.properties @@ -0,0 +1,5 @@ +windowTitle=TSplitPane Demo + +paneMenu=&Pane +paneSplitVertical=Split &Vertical +paneSplitHorizontal=Split &Horizontal diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 7580243..9983580 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -133,6 +133,10 @@ public class TMenu extends TWindow { public static final int MID_TABLE_FILE_SAVE_CSV = 116; public static final int MID_TABLE_FILE_SAVE_TEXT = 117; + // Miscellaneous + public static final int MID_SPLIT_VERTICAL = 200; + public static final int MID_SPLIT_HORIZONTAL = 201; + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -772,6 +776,13 @@ public class TMenu extends TWindow { label = i18n.getString("menuTableFileSaveText"); break; + case MID_SPLIT_VERTICAL: + label = i18n.getString("menuSplitVertical"); + break; + case MID_SPLIT_HORIZONTAL: + label = i18n.getString("menuSplitHorizontal"); + break; + default: throw new IllegalArgumentException("Invalid menu ID: " + id); } diff --git a/src/jexer/menu/TMenu.properties b/src/jexer/menu/TMenu.properties index 1581d0b..494fad9 100644 --- a/src/jexer/menu/TMenu.properties +++ b/src/jexer/menu/TMenu.properties @@ -59,3 +59,5 @@ menuTableFileSaveText=Save As &Text... menuRepaintDesktop=&Repaint desktop menuViewImage=&Open image... menuScreenOptions=&Screen options... +menuSplitVertical=Split &Vertical +menuSplitHorizontal=Split &Horizontal