--- /dev/null
+/*
+ * 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 java.util.HashSet;
+import java.util.Set;
+
+import jexer.backend.Screen;
+import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
+import jexer.bits.StringUtils;
+import jexer.event.TCommandEvent;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
+import static jexer.TCommand.*;
+import static jexer.TKeypress.*;
+
+/**
+ * TWindow is the top-level container and drawing surface for other widgets.
+ */
+public class TWindow extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Window is resizable (default yes).
+ */
+ public static final int RESIZABLE = 0x01;
+
+ /**
+ * Window is modal (default no).
+ */
+ public static final int MODAL = 0x02;
+
+ /**
+ * Window is centered (default no).
+ */
+ public static final int CENTERED = 0x04;
+
+ /**
+ * Window has no close box (default no). Window can still be closed via
+ * TApplication.closeWindow() and TWindow.close().
+ */
+ public static final int NOCLOSEBOX = 0x08;
+
+ /**
+ * Window has no maximize box (default no).
+ */
+ public static final int NOZOOMBOX = 0x10;
+
+ /**
+ * Window is placed at absolute position (no smart placement) (default
+ * no).
+ */
+ public static final int ABSOLUTEXY = 0x20;
+
+ /**
+ * Hitting the closebox with the mouse calls TApplication.hideWindow()
+ * rather than TApplication.closeWindow() (default no).
+ */
+ public static final int HIDEONCLOSE = 0x40;
+
+ /**
+ * Menus cannot be used when this window is active (default no).
+ */
+ public static final int OVERRIDEMENU = 0x80;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Window flags. Note package private access.
+ */
+ int flags = RESIZABLE;
+
+ /**
+ * Window title.
+ */
+ private String title = "";
+
+ /**
+ * Window's parent TApplication.
+ */
+ private TApplication application;
+
+ /**
+ * Z order. Lower number means more in-front.
+ */
+ private int z = 0;
+
+ /**
+ * Window's keyboard shortcuts. Any key in this set will be passed to
+ * the window directly rather than processed through the menu
+ * accelerators.
+ */
+ private Set<TKeypress> keyboardShortcuts = new HashSet<TKeypress>();
+
+ /**
+ * If true, then the user clicked on the title bar and is moving the
+ * window.
+ */
+ protected boolean inWindowMove = false;
+
+ /**
+ * If true, then the user clicked on the bottom right corner and is
+ * resizing the window.
+ */
+ protected boolean inWindowResize = false;
+
+ /**
+ * If true, then the user selected "Size/Move" (or hit Ctrl-F5) and is
+ * resizing/moving the window via the keyboard.
+ */
+ protected boolean inKeyboardResize = false;
+
+ /**
+ * If true, this window is maximized.
+ */
+ private boolean maximized = false;
+
+ /**
+ * Remember mouse state.
+ */
+ protected TMouseEvent mouse;
+
+ // For moving the window. resizing also uses moveWindowMouseX/Y
+ private int moveWindowMouseX;
+ private int moveWindowMouseY;
+ private int oldWindowX;
+ private int oldWindowY;
+
+ // Resizing
+ private int resizeWindowWidth;
+ private int resizeWindowHeight;
+ private int minimumWindowWidth = 10;
+ private int minimumWindowHeight = 2;
+ private int maximumWindowWidth = -1;
+ private int maximumWindowHeight = -1;
+
+ // For maximize/restore
+ private int restoreWindowWidth;
+ private int restoreWindowHeight;
+ private int restoreWindowX;
+ private int restoreWindowY;
+
+ /**
+ * Hidden flag. A hidden window will still have its onIdle() called, and
+ * will also have onClose() called at application exit. Note package
+ * private access: TApplication will force hidden false if a modal window
+ * is active.
+ */
+ boolean hidden = false;
+
+ /**
+ * A window may have a status bar associated with it. TApplication will
+ * draw this status bar last, and will also route events to it first
+ * before the window.
+ */
+ protected TStatusBar statusBar = null;
+
+ /**
+ * A window may request that TApplication NOT draw the mouse cursor over
+ * it by setting this to true. This is currently only used within Jexer
+ * by TTerminalWindow so that only the bottom-most instance of nested
+ * Jexer's draws the mouse within its application window. But perhaps
+ * other applications can use it, so public getter/setter is provided.
+ */
+ private boolean hideMouse = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @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 TWindow(final TApplication application, final String title,
+ final int width, final int height) {
+
+ this(application, title, 0, 0, width, height, RESIZABLE);
+ }
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @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 TWindow(final TApplication application, final String title,
+ final int width, final int height, final int flags) {
+
+ this(application, title, 0, 0, width, height, flags);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @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 TWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height) {
+
+ this(application, title, x, y, width, height, RESIZABLE);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @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 TWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ super();
+
+ // I am my own window and parent
+ setupForTWindow(this, x, y + application.getDesktopTop(),
+ width, height);
+
+ // Save fields
+ this.title = title;
+ this.application = application;
+ this.flags = flags;
+
+ // Minimum width/height are 10 and 2
+ assert (width >= 10);
+ assert (getHeight() >= 2);
+
+ // MODAL implies CENTERED
+ if (isModal()) {
+ this.flags |= CENTERED;
+ }
+
+ // Center window if specified
+ center();
+
+ // Add me to the application
+ application.addWindowToApplication(this);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the close button.
+ *
+ * @return true if mouse is currently on the close button
+ */
+ protected boolean mouseOnClose() {
+ if ((flags & NOCLOSEBOX) != 0) {
+ return false;
+ }
+ if ((mouse != null)
+ && (mouse.getAbsoluteY() == getY())
+ && (mouse.getAbsoluteX() == getX() + 3)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the maximize/restore button.
+ *
+ * @return true if the mouse is currently on the maximize/restore button
+ */
+ protected boolean mouseOnMaximize() {
+ if ((flags & NOZOOMBOX) != 0) {
+ return false;
+ }
+ if ((mouse != null)
+ && !isModal()
+ && (mouse.getAbsoluteY() == getY())
+ && (mouse.getAbsoluteX() == getX() + getWidth() - 4)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the resizable lower right
+ * corner.
+ *
+ * @return true if the mouse is currently on the resizable lower right
+ * corner
+ */
+ protected boolean mouseOnResize() {
+ if (((flags & RESIZABLE) != 0)
+ && !isModal()
+ && (mouse != null)
+ && (mouse.getAbsoluteY() == getY() + getHeight() - 1)
+ && ((mouse.getAbsoluteX() == getX() + getWidth() - 1)
+ || (mouse.getAbsoluteX() == getX() + getWidth() - 2))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Subclasses should override this method to perform any user prompting
+ * before they are offscreen. Note that unlike other windowing toolkits,
+ * windows can NOT use this function in some manner to avoid being
+ * closed. This is called by application.closeWindow().
+ */
+ protected void onPreClose() {
+ // Default: do nothing.
+ }
+
+ /**
+ * Subclasses should override this method to cleanup resources. This is
+ * called by application.closeWindow().
+ */
+ protected void onClose() {
+ // Default: perform widget-specific cleanup.
+ for (TWidget w: getChildren()) {
+ w.close();
+ }
+ }
+
+ /**
+ * Called by application.switchWindow() when this window gets the
+ * focus, and also by application.addWindow().
+ */
+ protected void onFocus() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.switchWindow() when another window gets the
+ * focus.
+ */
+ protected void onUnfocus() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.hideWindow().
+ */
+ protected void onHide() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.showWindow().
+ */
+ protected void onShow() {
+ // Default: do nothing
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ inKeyboardResize = false;
+ inWindowMove = false;
+ inWindowResize = false;
+
+ if ((mouse.getAbsoluteY() == getY())
+ && mouse.isMouse1()
+ && (getX() <= mouse.getAbsoluteX())
+ && (mouse.getAbsoluteX() < getX() + getWidth())
+ && !mouseOnClose()
+ && !mouseOnMaximize()
+ ) {
+ // Begin moving window
+ inWindowMove = true;
+ moveWindowMouseX = mouse.getAbsoluteX();
+ moveWindowMouseY = mouse.getAbsoluteY();
+ oldWindowX = getX();
+ oldWindowY = getY();
+ if (maximized) {
+ maximized = false;
+ }
+ return;
+ }
+ if (mouseOnResize()) {
+ // Begin window resize
+ inWindowResize = true;
+ moveWindowMouseX = mouse.getAbsoluteX();
+ moveWindowMouseY = mouse.getAbsoluteY();
+ resizeWindowWidth = getWidth();
+ resizeWindowHeight = getHeight();
+ if (maximized) {
+ maximized = false;
+ }
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarMouseDown(mouse)) {
+ 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) {
+ this.mouse = mouse;
+
+ if ((inWindowMove) && (mouse.isMouse1())) {
+ // Stop moving window
+ inWindowMove = false;
+ return;
+ }
+
+ if ((inWindowResize) && (mouse.isMouse1())) {
+ // Stop resizing window
+ inWindowResize = false;
+ return;
+ }
+
+ if (mouse.isMouse1() && mouseOnClose()) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ return;
+ }
+
+ if ((mouse.getAbsoluteY() == getY())
+ && mouse.isMouse1()
+ && mouseOnMaximize()) {
+ if (maximized) {
+ // Restore
+ restore();
+ } else {
+ // Maximize
+ maximize();
+ }
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarMouseUp(mouse)) {
+ 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) {
+ this.mouse = mouse;
+
+ if (inWindowMove) {
+ // Move window over
+ setX(oldWindowX + (mouse.getAbsoluteX() - moveWindowMouseX));
+ setY(oldWindowY + (mouse.getAbsoluteY() - moveWindowMouseY));
+ // Don't cover up the menu bar
+ if (getY() < application.getDesktopTop()) {
+ setY(application.getDesktopTop());
+ }
+ // Don't go below the status bar
+ if (getY() >= application.getDesktopBottom()) {
+ setY(application.getDesktopBottom() - 1);
+ }
+ return;
+ }
+
+ if (inWindowResize) {
+ // Do not permit resizing below the status line
+ if (mouse.getAbsoluteY() == application.getDesktopBottom()) {
+ inWindowResize = false;
+ return;
+ }
+
+ // Move window over
+ setWidth(resizeWindowWidth + (mouse.getAbsoluteX()
+ - moveWindowMouseX));
+ setHeight(resizeWindowHeight + (mouse.getAbsoluteY()
+ - moveWindowMouseY));
+ if (getX() + getWidth() > getScreen().getWidth()) {
+ setWidth(getScreen().getWidth() - getX());
+ }
+ if (getY() + getHeight() > application.getDesktopBottom()) {
+ setY(application.getDesktopBottom() - getHeight() + 1);
+ }
+ // Don't cover up the menu bar
+ if (getY() < application.getDesktopTop()) {
+ setY(application.getDesktopTop());
+ }
+
+ // Keep within min/max bounds
+ if (getWidth() < minimumWindowWidth) {
+ setWidth(minimumWindowWidth);
+ inWindowResize = false;
+ }
+ if (getHeight() < minimumWindowHeight) {
+ setHeight(minimumWindowHeight);
+ inWindowResize = false;
+ }
+ if ((maximumWindowWidth > 0)
+ && (getWidth() > maximumWindowWidth)
+ ) {
+ setWidth(maximumWindowWidth);
+ inWindowResize = false;
+ }
+ if ((maximumWindowHeight > 0)
+ && (getHeight() > maximumWindowHeight)
+ ) {
+ setHeight(maximumWindowHeight);
+ inWindowResize = false;
+ }
+
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ statusBar.statusBarMouseMotion(mouse);
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseMotion(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ if (inWindowMove || inWindowResize) {
+ // ESC or ENTER - Exit size/move
+ if (keypress.equals(kbEsc) || keypress.equals(kbEnter)) {
+ inWindowMove = false;
+ inWindowResize = false;
+ return;
+ }
+ }
+
+ if (inKeyboardResize) {
+
+ // ESC or ENTER - Exit size/move
+ if (keypress.equals(kbEsc) || keypress.equals(kbEnter)) {
+ inKeyboardResize = false;
+ }
+
+ if (keypress.equals(kbLeft)) {
+ if (getX() > 0) {
+ setX(getX() - 1);
+ }
+ }
+ if (keypress.equals(kbRight)) {
+ if (getX() < getScreen().getWidth() - 1) {
+ setX(getX() + 1);
+ }
+ }
+ if (keypress.equals(kbDown)) {
+ if (getY() < application.getDesktopBottom() - 1) {
+ setY(getY() + 1);
+ }
+ }
+ if (keypress.equals(kbUp)) {
+ if (getY() > 1) {
+ setY(getY() - 1);
+ }
+ }
+
+ /*
+ * Only permit keyboard resizing if the window was RESIZABLE.
+ */
+ if ((flags & RESIZABLE) != 0) {
+
+ if (keypress.equals(kbShiftLeft)) {
+ if ((getWidth() > minimumWindowWidth)
+ || (minimumWindowWidth <= 0)
+ ) {
+ setWidth(getWidth() - 1);
+ }
+ }
+ if (keypress.equals(kbShiftRight)) {
+ if ((getWidth() < maximumWindowWidth)
+ || (maximumWindowWidth <= 0)
+ ) {
+ setWidth(getWidth() + 1);
+ }
+ }
+ if (keypress.equals(kbShiftUp)) {
+ if ((getHeight() > minimumWindowHeight)
+ || (minimumWindowHeight <= 0)
+ ) {
+ setHeight(getHeight() - 1);
+ }
+ }
+ if (keypress.equals(kbShiftDown)) {
+ if ((getHeight() < maximumWindowHeight)
+ || (maximumWindowHeight <= 0)
+ ) {
+ setHeight(getHeight() + 1);
+ }
+ }
+
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+
+ } // if ((flags & RESIZABLE) != 0)
+
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarKeypress(keypress)) {
+ return;
+ }
+ }
+
+ // These keystrokes will typically not be seen unless a subclass
+ // overrides onMenu() due to how TApplication dispatches
+ // accelerators.
+
+ if (!(this instanceof TDesktop)) {
+
+ // Ctrl-W - close window
+ if (keypress.equals(kbCtrlW)) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ // F6 - behave like Alt-TAB
+ if (keypress.equals(kbF6)) {
+ application.switchWindow(true);
+ return;
+ }
+
+ // Shift-F6 - behave like Shift-Alt-TAB
+ if (keypress.equals(kbShiftF6)) {
+ application.switchWindow(false);
+ return;
+ }
+
+ // F5 - zoom
+ if (keypress.equals(kbF5) && ((flags & NOZOOMBOX) == 0)) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ }
+
+ // Ctrl-F5 - size/move
+ if (keypress.equals(kbCtrlF5)) {
+ inKeyboardResize = !inKeyboardResize;
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+
+ // These commands will typically not be seen unless a subclass
+ // overrides onMenu() due to how TApplication dispatches
+ // accelerators.
+
+ if (!(this instanceof TDesktop)) {
+
+ if (command.equals(cmWindowClose)) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ if (command.equals(cmWindowNext)) {
+ application.switchWindow(true);
+ return;
+ }
+
+ if (command.equals(cmWindowPrevious)) {
+ application.switchWindow(false);
+ return;
+ }
+
+ if (command.equals(cmWindowMove)) {
+ inKeyboardResize = true;
+ return;
+ }
+
+ if (command.equals(cmWindowZoom) && ((flags & NOZOOMBOX) == 0)) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ super.onCommand(command);
+ }
+
+ /**
+ * Handle posted menu events.
+ *
+ * @param menu menu event
+ */
+ @Override
+ public void onMenu(final TMenuEvent menu) {
+
+ if (!(this instanceof TDesktop)) {
+
+ if (menu.getId() == TMenu.MID_WINDOW_CLOSE) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_NEXT) {
+ application.switchWindow(true);
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_PREVIOUS) {
+ application.switchWindow(false);
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_MOVE) {
+ inKeyboardResize = true;
+ return;
+ }
+
+ if ((menu.getId() == TMenu.MID_WINDOW_ZOOM)
+ && ((flags & NOZOOMBOX) == 0)
+ ) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ return;
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ 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)
+ ) {
+ if (this instanceof TDesktop) {
+ child.onResize(new TResizeEvent(
+ TResizeEvent.Type.WIDGET,
+ resize.getWidth(), resize.getHeight()));
+ } else {
+ child.onResize(new TResizeEvent(
+ TResizeEvent.Type.WIDGET,
+ resize.getWidth() - 2, resize.getHeight() - 2));
+ }
+ }
+ return;
+ }
+ }
+
+ // Pass on to TWidget.
+ super.onResize(resize);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get this TWindow's parent TApplication.
+ *
+ * @return this TWindow's parent TApplication
+ */
+ @Override
+ public final TApplication getApplication() {
+ return application;
+ }
+
+ /**
+ * Get the Screen.
+ *
+ * @return the Screen
+ */
+ @Override
+ public final Screen getScreen() {
+ return application.getScreen();
+ }
+
+ /**
+ * Called by TApplication.drawChildren() to render on screen.
+ */
+ @Override
+ public void draw() {
+ // Draw the box and background first.
+ CellAttributes border = getBorder();
+ CellAttributes background = getBackground();
+ int borderType = getBorderType();
+
+ drawBox(0, 0, getWidth(), getHeight(), border, background, borderType,
+ true);
+
+ // Draw the title
+ int titleLength = StringUtils.width(title);
+ int titleLeft = (getWidth() - titleLength - 2) / 2;
+ putCharXY(titleLeft, 0, ' ', border);
+ putStringXY(titleLeft + 1, 0, title, border);
+ putCharXY(titleLeft + titleLength + 1, 0, ' ', border);
+
+ if (isActive()) {
+
+ // Draw the close button
+ if ((flags & NOCLOSEBOX) == 0) {
+ putCharXY(2, 0, '[', border);
+ putCharXY(4, 0, ']', border);
+ if (mouseOnClose() && mouse.isMouse1()) {
+ putCharXY(3, 0, GraphicsChars.CP437[0x0F],
+ getBorderControls());
+ } else {
+ putCharXY(3, 0, GraphicsChars.CP437[0xFE],
+ getBorderControls());
+ }
+ }
+
+ // Draw the maximize button
+ if (!isModal() && ((flags & NOZOOMBOX) == 0)) {
+
+ putCharXY(getWidth() - 5, 0, '[', border);
+ putCharXY(getWidth() - 3, 0, ']', border);
+ if (mouseOnMaximize() && mouse.isMouse1()) {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x0F],
+ getBorderControls());
+ } else {
+ if (maximized) {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x12],
+ getBorderControls());
+ } else {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.UPARROW,
+ getBorderControls());
+ }
+ }
+
+ // Draw the resize corner
+ if ((flags & RESIZABLE) != 0) {
+ putCharXY(getWidth() - 2, getHeight() - 1,
+ GraphicsChars.SINGLE_BAR, getBorderControls());
+ putCharXY(getWidth() - 1, getHeight() - 1,
+ GraphicsChars.LRCORNER, getBorderControls());
+ }
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get window title.
+ *
+ * @return window title
+ */
+ public final String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set window title.
+ *
+ * @param title new window title
+ */
+ public final void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * Get Z order. Lower number means more in-front.
+ *
+ * @return Z value. Lower number means more in-front.
+ */
+ public final int getZ() {
+ return z;
+ }
+
+ /**
+ * Set Z order. Lower number means more in-front.
+ *
+ * @param z the new Z value. Lower number means more in-front.
+ */
+ public final void setZ(final int z) {
+ this.z = z;
+ }
+
+ /**
+ * Add a keypress to be overridden for this window.
+ *
+ * @param key the key to start taking control of
+ */
+ protected void addShortcutKeypress(final TKeypress key) {
+ keyboardShortcuts.add(key);
+ }
+
+ /**
+ * Remove a keypress to be overridden for this window.
+ *
+ * @param key the key to stop taking control of
+ */
+ protected void removeShortcutKeypress(final TKeypress key) {
+ keyboardShortcuts.remove(key);
+ }
+
+ /**
+ * Remove all keypresses to be overridden for this window.
+ */
+ protected void clearShortcutKeypresses() {
+ keyboardShortcuts.clear();
+ }
+
+ /**
+ * Determine if a keypress is overridden for this window.
+ *
+ * @param key the key to check
+ * @return true if this window wants to process this key on its own
+ */
+ public boolean isShortcutKeypress(final TKeypress key) {
+ return keyboardShortcuts.contains(key);
+ }
+
+ /**
+ * Get the window's status bar, or null if it does not have one.
+ *
+ * @return the status bar, or null
+ */
+ public TStatusBar getStatusBar() {
+ return statusBar;
+ }
+
+ /**
+ * Set the window's status bar to a new one.
+ *
+ * @param text the status bar text
+ * @return the status bar
+ */
+ public TStatusBar newStatusBar(final String text) {
+ statusBar = new TStatusBar(this, text);
+ return statusBar;
+ }
+
+ /**
+ * Set the maximum width for this window.
+ *
+ * @param maximumWindowWidth new maximum width
+ */
+ public final void setMaximumWindowWidth(final int maximumWindowWidth) {
+ if ((maximumWindowWidth != -1)
+ && (maximumWindowWidth < minimumWindowWidth + 1)
+ ) {
+ throw new IllegalArgumentException("Maximum window width cannot " +
+ "be smaller than minimum window width + 1");
+ }
+ this.maximumWindowWidth = maximumWindowWidth;
+ }
+
+ /**
+ * Set the minimum width for this window.
+ *
+ * @param minimumWindowWidth new minimum width
+ */
+ public final void setMinimumWindowWidth(final int minimumWindowWidth) {
+ if ((maximumWindowWidth != -1)
+ && (minimumWindowWidth > maximumWindowWidth - 1)
+ ) {
+ throw new IllegalArgumentException("Minimum window width cannot " +
+ "be larger than maximum window width - 1");
+ }
+ this.minimumWindowWidth = minimumWindowWidth;
+ }
+
+ /**
+ * Set the maximum height for this window.
+ *
+ * @param maximumWindowHeight new maximum height
+ */
+ public final void setMaximumWindowHeight(final int maximumWindowHeight) {
+ if ((maximumWindowHeight != -1)
+ && (maximumWindowHeight < minimumWindowHeight + 1)
+ ) {
+ throw new IllegalArgumentException("Maximum window height cannot " +
+ "be smaller than minimum window height + 1");
+ }
+ this.maximumWindowHeight = maximumWindowHeight;
+ }
+
+ /**
+ * Set the minimum height for this window.
+ *
+ * @param minimumWindowHeight new minimum height
+ */
+ public final void setMinimumWindowHeight(final int minimumWindowHeight) {
+ if ((maximumWindowHeight != -1)
+ && (minimumWindowHeight > maximumWindowHeight - 1)
+ ) {
+ throw new IllegalArgumentException("Minimum window height cannot " +
+ "be larger than maximum window height - 1");
+ }
+ this.minimumWindowHeight = minimumWindowHeight;
+ }
+
+ /**
+ * Recenter the window on-screen.
+ */
+ public final void center() {
+ if ((flags & CENTERED) != 0) {
+ if (getWidth() < getScreen().getWidth()) {
+ setX((getScreen().getWidth() - getWidth()) / 2);
+ } else {
+ setX(0);
+ }
+ setY(((application.getDesktopBottom()
+ - application.getDesktopTop()) - getHeight()) / 2);
+ if (getY() < 0) {
+ setY(0);
+ }
+ setY(getY() + application.getDesktopTop());
+ }
+ }
+
+ /**
+ * Maximize window.
+ */
+ public void maximize() {
+ if (maximized) {
+ return;
+ }
+
+ restoreWindowWidth = getWidth();
+ restoreWindowHeight = getHeight();
+ restoreWindowX = getX();
+ restoreWindowY = getY();
+ setWidth(getScreen().getWidth());
+ setHeight(application.getDesktopBottom() - application.getDesktopTop());
+ setX(0);
+ setY(application.getDesktopTop());
+ maximized = true;
+
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Restore (unmaximize) window.
+ */
+ public void restore() {
+ if (!maximized) {
+ return;
+ }
+
+ setWidth(restoreWindowWidth);
+ setHeight(restoreWindowHeight);
+ setX(restoreWindowX);
+ setY(restoreWindowY);
+ maximized = false;
+
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Returns true if this window is hidden.
+ *
+ * @return true if this window is hidden, false if the window is shown
+ */
+ public final boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns true if this window is shown.
+ *
+ * @return true if this window is shown, false if the window is hidden
+ */
+ public final boolean isShown() {
+ return !hidden;
+ }
+
+ /**
+ * Hide window. A hidden window will still have its onIdle() called, and
+ * will also have onClose() called at application exit. Hidden windows
+ * will not receive any other events.
+ */
+ public void hide() {
+ application.hideWindow(this);
+ }
+
+ /**
+ * Show window.
+ */
+ public void show() {
+ application.showWindow(this);
+ }
+
+ /**
+ * Activate window (bring to top and receive events).
+ */
+ @Override
+ public void activate() {
+ application.activateWindow(this);
+ }
+
+ /**
+ * Close window. Note that windows without a close box can still be
+ * closed by calling the close() method.
+ */
+ @Override
+ public void close() {
+ application.closeWindow(this);
+ }
+
+ /**
+ * See if this window is undergoing any movement/resize/etc.
+ *
+ * @return true if the window is moving
+ */
+ public boolean inMovements() {
+ if (inWindowResize || inWindowMove || inKeyboardResize) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Stop any pending movement/resize/etc.
+ */
+ public void stopMovements() {
+ inWindowResize = false;
+ inWindowMove = false;
+ inKeyboardResize = false;
+ }
+
+ /**
+ * Returns true if this window is modal.
+ *
+ * @return true if this window is modal
+ */
+ public final boolean isModal() {
+ if ((flags & MODAL) == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if this window has a close box.
+ *
+ * @return true if this window has a close box
+ */
+ public final boolean hasCloseBox() {
+ if ((flags & NOCLOSEBOX) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this window has a maximize/zoom box.
+ *
+ * @return true if this window has a maximize/zoom box
+ */
+ public final boolean hasZoomBox() {
+ if ((flags & NOZOOMBOX) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this window does not want menus to work while it is
+ * visible.
+ *
+ * @return true if this window does not want menus to work while it is
+ * visible
+ */
+ public final boolean hasOverriddenMenu() {
+ if ((flags & OVERRIDEMENU) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the background color.
+ *
+ * @return the background color
+ */
+ public CellAttributes getBackground() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ assert (isActive());
+ return getTheme().getColor("twindow.background.windowmove");
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return getTheme().getColor("twindow.background.modal");
+ } else if (isModal()) {
+ if (isActive()) {
+ return getTheme().getColor("twindow.background.modal");
+ }
+ return getTheme().getColor("twindow.background.modal.inactive");
+ } else if (isActive()) {
+ assert (!isModal());
+ return getTheme().getColor("twindow.background");
+ } else {
+ assert (!isModal());
+ return getTheme().getColor("twindow.background.inactive");
+ }
+ }
+
+ /**
+ * Retrieve the border color.
+ *
+ * @return the border color
+ */
+ public CellAttributes getBorder() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ if (!isActive()) {
+ // The user's terminal never passed a mouse up event, and now
+ // another window is active but we never finished a drag.
+ inWindowMove = false;
+ inWindowResize = false;
+ inKeyboardResize = false;
+ return getTheme().getColor("twindow.border.inactive");
+ }
+
+ return getTheme().getColor("twindow.border.windowmove");
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return getTheme().getColor("twindow.border.modal.windowmove");
+ } else if (isModal()) {
+ if (isActive()) {
+ return getTheme().getColor("twindow.border.modal");
+ } else {
+ return getTheme().getColor("twindow.border.modal.inactive");
+ }
+ } else if (isActive()) {
+ assert (!isModal());
+ return getTheme().getColor("twindow.border");
+ } else {
+ assert (!isModal());
+ return getTheme().getColor("twindow.border.inactive");
+ }
+ }
+
+ /**
+ * Retrieve the color used by the window movement/sizing controls.
+ *
+ * @return the color used by the zoom box, resize bar, and close box
+ */
+ public CellAttributes getBorderControls() {
+ if (isModal()) {
+ return getTheme().getColor("twindow.border.modal.windowmove");
+ }
+ return getTheme().getColor("twindow.border.windowmove");
+ }
+
+ /**
+ * Retrieve the border line type.
+ *
+ * @return the border line type
+ */
+ private int getBorderType() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ assert (isActive());
+ return 1;
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return 1;
+ } else if (isModal()) {
+ if (isActive()) {
+ return 2;
+ } else {
+ return 1;
+ }
+ } else if (isActive()) {
+ return 2;
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Returns true if this window does not want the application-wide mouse
+ * cursor drawn over it.
+ *
+ * @return true if this window does not want the application-wide mouse
+ * cursor drawn over it
+ */
+ public boolean hasHiddenMouse() {
+ return hideMouse;
+ }
+
+ /**
+ * Set request to prevent the application-wide mouse cursor from being
+ * drawn over this window.
+ *
+ * @param hideMouse if true, this window does not want the
+ * application-wide mouse cursor drawn over it
+ */
+ public final void setHiddenMouse(final boolean hideMouse) {
+ this.hideMouse = hideMouse;
+ }
+
+ /**
+ * Generate a human-readable string for this window.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" +
+ " hidden %s modal %s", getClass().getName(), hashCode(), title,
+ getX(), getY(), getWidth(), getHeight(), hidden, isModal());
+ }
+
+}