From a69ed767c9c07cf35cf1c5f7821fc009cfe79cd2 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Sun, 21 Oct 2018 21:18:05 -0500 Subject: [PATCH] Prep for 2019 release --- .gitignore | 3 + README.md | 17 +- docs/TODO.md | 2 +- docs/worklog.md | 12 + pom.xml | 14 +- src/jexer/Scrollable.java | 2 +- src/jexer/TAction.java | 2 +- src/jexer/TApplication.java | 485 +++++-- src/jexer/TApplication.properties | 3 - src/jexer/TButton.java | 22 +- src/jexer/TCalendar.java | 20 +- src/jexer/TCheckBox.java | 21 +- src/jexer/TComboBox.java | 99 +- src/jexer/TCommand.java | 6 +- src/jexer/TDesktop.java | 2 +- src/jexer/TDirectoryList.java | 97 +- src/jexer/TEditColorThemeWindow.java | 32 +- src/jexer/TEditorWidget.java | 2 +- src/jexer/TEditorWindow.java | 2 +- src/jexer/TField.java | 8 +- src/jexer/TFileOpenBox.java | 110 +- src/jexer/TFileOpenBox.properties | 2 + src/jexer/THScroller.java | 16 +- src/jexer/TImage.java | 499 +++++++ src/jexer/TImageWindow.java | 280 ++++ src/jexer/TInputBox.java | 9 +- src/jexer/TKeypress.java | 7 +- src/jexer/TLabel.java | 4 +- src/jexer/TList.java | 49 +- src/jexer/TMessageBox.java | 49 +- src/jexer/TPasswordField.java | 10 +- src/jexer/TProgressBar.java | 17 +- src/jexer/TRadioButton.java | 13 +- src/jexer/TRadioGroup.java | 10 +- src/jexer/TScrollableWidget.java | 2 +- src/jexer/TScrollableWindow.java | 2 +- src/jexer/TSpinner.java | 8 +- src/jexer/TStatusBar.java | 29 +- src/jexer/TTerminalWindow.java | 90 +- src/jexer/TText.java | 17 +- src/jexer/TTimer.java | 2 +- src/jexer/TVScroller.java | 17 +- src/jexer/TWidget.java | 371 ++++- src/jexer/TWindow.java | 187 +-- src/jexer/backend/Backend.java | 2 +- src/jexer/backend/ECMA48Backend.java | 2 +- src/jexer/backend/ECMA48Terminal.java | 1270 ++++++++++++++++- src/jexer/backend/GenericBackend.java | 2 +- src/jexer/backend/LogicalScreen.java | 92 +- src/jexer/backend/MultiBackend.java | 2 +- src/jexer/backend/MultiScreen.java | 87 +- src/jexer/backend/Screen.java | 15 +- src/jexer/backend/SessionInfo.java | 2 +- src/jexer/backend/SwingBackend.java | 2 +- src/jexer/backend/SwingComponent.java | 10 +- src/jexer/backend/SwingSessionInfo.java | 3 +- src/jexer/backend/SwingTerminal.java | 85 +- src/jexer/backend/TSessionInfo.java | 2 +- src/jexer/backend/TTYSessionInfo.java | 4 +- src/jexer/backend/TWindowBackend.java | 2 +- src/jexer/backend/TerminalReader.java | 2 +- src/jexer/backend/package-info.java | 2 +- src/jexer/bits/Cell.java | 214 ++- src/jexer/bits/CellAttributes.java | 2 +- src/jexer/bits/Color.java | 2 +- src/jexer/bits/ColorTheme.java | 19 +- src/jexer/bits/GraphicsChars.java | 4 +- src/jexer/bits/MnemonicString.java | 2 +- src/jexer/bits/StringUtils.java | 2 +- src/jexer/bits/package-info.java | 2 +- src/jexer/demos/Demo1.java | 2 +- src/jexer/demos/Demo2.java | 25 +- src/jexer/demos/Demo2.properties | 3 + src/jexer/demos/Demo3.java | 2 +- src/jexer/demos/Demo4.java | 2 +- src/jexer/demos/Demo5.java | 12 +- src/jexer/demos/Demo5.properties | 1 + src/jexer/demos/Demo6.java | 3 +- src/jexer/demos/DemoApplication.java | 68 +- src/jexer/demos/DemoApplication.properties | 15 + src/jexer/demos/DemoCheckBoxWindow.java | 85 +- src/jexer/demos/DemoCheckBoxWindow.properties | 30 + src/jexer/demos/DemoEditorWindow.java | 31 +- src/jexer/demos/DemoEditorWindow.properties | 6 + src/jexer/demos/DemoMainWindow.java | 102 +- src/jexer/demos/DemoMainWindow.properties | 33 + src/jexer/demos/DemoMsgBoxWindow.java | 141 +- src/jexer/demos/DemoMsgBoxWindow.properties | 45 + src/jexer/demos/DemoTextFieldWindow.java | 57 +- .../demos/DemoTextFieldWindow.properties | 17 + src/jexer/demos/DemoTextWindow.java | 43 +- src/jexer/demos/DemoTextWindow.properties | 12 + src/jexer/demos/DemoTreeViewWindow.java | 34 +- src/jexer/demos/DemoTreeViewWindow.properties | 7 + src/jexer/demos/DesktopDemo.java | 3 +- src/jexer/demos/DesktopDemoApplication.java | 69 +- .../demos/DesktopDemoApplication.properties | 19 + src/jexer/demos/package-info.java | 2 +- src/jexer/event/TCommandEvent.java | 2 +- src/jexer/event/TInputEvent.java | 2 +- src/jexer/event/TKeypressEvent.java | 2 +- src/jexer/event/TMenuEvent.java | 2 +- src/jexer/event/TMouseEvent.java | 2 +- src/jexer/event/TResizeEvent.java | 2 +- src/jexer/event/package-info.java | 2 +- src/jexer/io/ReadTimeoutException.java | 2 +- src/jexer/io/TimeoutInputStream.java | 11 +- src/jexer/io/package-info.java | 2 +- src/jexer/menu/TMenu.java | 20 +- src/jexer/menu/TMenuItem.java | 20 +- src/jexer/menu/TMenuSeparator.java | 10 +- src/jexer/menu/TSubMenu.java | 5 +- src/jexer/menu/package-info.java | 2 +- src/jexer/net/TelnetInputStream.java | 2 +- src/jexer/net/TelnetOutputStream.java | 2 +- src/jexer/net/TelnetServerSocket.java | 2 +- src/jexer/net/TelnetSocket.java | 2 +- src/jexer/net/package-info.java | 2 +- src/jexer/package-info.java | 2 +- src/jexer/teditor/Document.java | 2 +- src/jexer/teditor/Highlighter.java | 2 +- src/jexer/teditor/Line.java | 2 +- src/jexer/teditor/Word.java | 2 +- src/jexer/teditor/package-info.java | 2 +- src/jexer/tterminal/DECCharacterSets.java | 2 +- src/jexer/tterminal/DisplayLine.java | 2 +- src/jexer/tterminal/DisplayListener.java | 2 +- src/jexer/tterminal/ECMA48.java | 54 +- src/jexer/tterminal/package-info.java | 2 +- src/jexer/ttree/TDirectoryTreeItem.java | 2 +- src/jexer/ttree/TTreeItem.java | 17 +- src/jexer/ttree/TTreeView.java | 2 +- src/jexer/ttree/TTreeViewWidget.java | 2 +- src/jexer/ttree/TTreeViewWindow.java | 2 +- src/jexer/ttree/package-info.java | 2 +- 135 files changed, 4613 insertions(+), 937 deletions(-) create mode 100644 src/jexer/TImage.java create mode 100644 src/jexer/TImageWindow.java create mode 100644 src/jexer/demos/Demo2.properties create mode 100644 src/jexer/demos/Demo5.properties create mode 100644 src/jexer/demos/DemoApplication.properties create mode 100644 src/jexer/demos/DemoCheckBoxWindow.properties create mode 100644 src/jexer/demos/DemoEditorWindow.properties create mode 100644 src/jexer/demos/DemoMainWindow.properties create mode 100644 src/jexer/demos/DemoMsgBoxWindow.properties create mode 100644 src/jexer/demos/DemoTextFieldWindow.properties create mode 100644 src/jexer/demos/DemoTextWindow.properties create mode 100644 src/jexer/demos/DemoTreeViewWindow.properties create mode 100644 src/jexer/demos/DesktopDemoApplication.properties diff --git a/.gitignore b/.gitignore index 704dab2..733399a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ hs_err_pid* # Scratch space misc/** /.project~ + +pmd.bash +pmd-results.html diff --git a/README.md b/README.md index dc97659..009ec6f 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Jexer currently supports three backends: * System.in/out to a command-line ECMA-48 / ANSI X3.64 type terminal (tested on Linux + xterm). I/O is handled through terminal escape sequences generated by the library itself: ncurses is not required - or linked to. xterm mouse tracking using UTF8 and SGR coordinates - are supported. For the demo application, this is the default + or linked to. xterm mouse tracking is supported using both UTF8 and + SGR coordinates. For the demo application, this is the default backend on non-Windows/non-Mac platforms. * The same command-line ECMA-48 / ANSI X3.64 type terminal as above, @@ -36,7 +36,7 @@ constructor. See Demo5 and Demo6 for examples of other backends. The Jexer homepage, which includes additional information and binary release downloads, is at: https://jexer.sourceforge.io . The Jexer -source code is hosted at: https://github.com/klamonte/jexer . +source code is hosted at: https://gitlab.com/klamonte/jexer . @@ -203,7 +203,14 @@ The following properties control features of Jexer: Used by jexer.TTerminalWindow. If true, spawn shell using the 'ptypipe' utility rather than 'script'. This permits terminals to resize with the window. ptypipe is a separate C language utility, - available at https://github.com/klamonte/ptypipe. Default: false. + available at https://gitlab.com/klamonte/ptypipe. Default: false. + + jexer.ECMA48.rgbColor + --------------------- + + Used by jexer.backend.ECMA48Terminal. If true, emit T.416-style RGB + colors for normal system colors. This is expensive in bandwidth, + and potentially terrible looking for non-xterms. Default: false. @@ -234,7 +241,7 @@ ambiguous. This section describes such issues. - TTerminalWindow can only notify the child process of changes in window size if using the 'ptypipe' utility, due to Java's lack of support for forkpty() and similar. ptypipe is available at - https://github.com/klamonte/ptypipe. + https://gitlab.com/klamonte/ptypipe. - Java's InputStreamReader as used by the ECMA48 backend requires a valid UTF-8 stream. The default X10 encoding for mouse diff --git a/docs/TODO.md b/docs/TODO.md index 55b6238..ab4e7c8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -106,7 +106,7 @@ Update written by date to current year: All code headers VERSION -Tag github +Tag gitlab Upload to SF diff --git a/docs/worklog.md b/docs/worklog.md index d544788..5f7e192 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,18 @@ Jexer Work Log ============== +October 21, 2018 + +All future work for this project has been moved to GitLab. GitHub +will remain up for a few months longer, until the next release comes +out. + +September 18, 2018 + +Collecting up a few bug fixes involving race conditions: message box +(and input box), TimeoutInputStream, window close. Will get these +uploaded to GitHub in a little while. + July 13, 2018 This project isn't dead, I swear! diff --git a/pom.xml b/pom.xml index 2118e89..524c4aa 100644 --- a/pom.xml +++ b/pom.xml @@ -2,13 +2,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - com.github.klamonte + com.gitlab.klamonte jexer jar Jexer Java Text User Interface library that resembles Turbo Vision 0.0.6 - https://github.com/klamonte/jexer + https://gitlab.com/klamonte/jexer @@ -24,15 +24,15 @@ - scm:git:https://github.com/klamonte/jexer.git - scm:git:https://github.com/klamonte/jexer.git - https://github.com/klamonte/jexer + scm:git:https://gitlab.com/klamonte/jexer.git + scm:git:https://gitlab.com/klamonte/jexer.git + https://gitlab.com/klamonte/jexer HEAD - github - https://github.com/klamonte/jexer/issues + gitlab + https://gitlab.com/klamonte/jexer/issues diff --git a/src/jexer/Scrollable.java b/src/jexer/Scrollable.java index 00c9ebe..b844ca6 100644 --- a/src/jexer/Scrollable.java +++ b/src/jexer/Scrollable.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TAction.java b/src/jexer/TAction.java index ea01ffa..cc93819 100644 --- a/src/jexer/TAction.java +++ b/src/jexer/TAction.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 96dad6c..96f91f2 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,23 +28,22 @@ */ package jexer; -import java.io.File; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; -import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; import jexer.event.TCommandEvent; @@ -54,8 +53,8 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; -import jexer.backend.Screen; import jexer.backend.MultiBackend; +import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; import jexer.backend.TWindowBackend; @@ -160,6 +159,21 @@ public class TApplication implements Runnable { */ private int oldMouseY; + /** + * Old drawn version of mouse coordinate X. + */ + private int oldDrawnMouseX; + + /** + * Old drawn version mouse coordinate Y. + */ + private int oldDrawnMouseY; + + /** + * Old drawn version mouse cell. + */ + private Cell oldDrawnMouseCell = new Cell(); + /** * The last mouse up click time, used to determine if this is a mouse * double-click. @@ -266,6 +280,16 @@ public class TApplication implements Runnable { */ private boolean focusFollowsMouse = false; + /** + * The images that might be displayed. Note package private access. + */ + private List images; + + /** + * The list of commands to run before the next I/O check. + */ + private List invokeLaters = new LinkedList(); + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -300,6 +324,21 @@ public class TApplication implements Runnable { * The consumer loop. */ public void run() { + // Wrap everything in a try, so that if we go belly up we can let + // the user have their terminal back. + try { + runImpl(); + } catch (Throwable t) { + this.application.restoreConsole(); + t.printStackTrace(); + this.application.exit(); + } + } + + /** + * The consumer loop. + */ + private void runImpl() { boolean first = true; // Loop forever @@ -327,9 +366,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s sleep %d millis\n", + System.err.printf("%d %s %s %s sleep %d millis\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary", timeout); + primary ? "primary" : "secondary", + Thread.currentThread(), timeout); } synchronized (this) { @@ -337,9 +377,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s AWAKE\n", + System.err.printf("%d %s %s %s AWAKE\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary"); + primary ? "primary" : "secondary", + Thread.currentThread()); } if ((!primary) @@ -385,15 +426,16 @@ public class TApplication implements Runnable { ) { // Secondary thread, time to exit. + // Eliminate my reference so that wakeEventHandler() + // resumes working on the primary. + application.secondaryEventHandler = null; + // DO NOT UNLOCK. Primary thread just came back from // primaryHandleEvent() and will unlock in the else // block below. Just wake it up. synchronized (application.primaryEventHandler) { application.primaryEventHandler.notify(); } - // Now eliminate my reference so that - // wakeEventHandler() resumes working on the primary. - application.secondaryEventHandler = null; // All done! return; @@ -552,15 +594,16 @@ public class TApplication implements Runnable { private void TApplicationImpl() { theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new ArrayList(); - drainEventQueue = new ArrayList(); + fillEventQueue = new LinkedList(); + drainEventQueue = new LinkedList(); windows = new LinkedList(); - menus = new LinkedList(); - subMenus = new LinkedList(); + menus = new ArrayList(); + subMenus = new ArrayList(); timers = new LinkedList(); accelerators = new HashMap(); - menuItems = new ArrayList(); + menuItems = new LinkedList(); desktop = new TDesktop(this); + images = new LinkedList(); // Special case: the Swing backend needs to have a timer to drive its // blink state. @@ -617,14 +660,14 @@ public class TApplication implements Runnable { try { if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN sleep"); + " " + Thread.currentThread() + " MAIN sleep"); } this.wait(); if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN AWAKE"); + " " + Thread.currentThread() + " MAIN AWAKE"); } } catch (InterruptedException e) { // I'm awake and don't care why, let's see what's @@ -694,7 +737,8 @@ public class TApplication implements Runnable { if (command.equals(cmExit)) { if (messageBox(i18n.getString("exitDialogTitle"), i18n.getString("exitDialogText"), - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).isYes()) { + exit(); } return true; @@ -744,7 +788,8 @@ public class TApplication implements Runnable { if (menu.getId() == TMenu.MID_EXIT) { if (messageBox(i18n.getString("exitDialogTitle"), i18n.getString("exitDialogText"), - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).isYes()) { + exit(); } return true; @@ -767,11 +812,8 @@ public class TApplication implements Runnable { closeAllWindows(); return true; } - if (menu.getId() == TMenu.MID_ABOUT) { - showAboutDialog(); - return true; - } if (menu.getId() == TMenu.MID_REPAINT) { + getScreen().clearPhysical(); doRepaint(); return true; } @@ -911,7 +953,8 @@ public class TApplication implements Runnable { private void primaryHandleEvent(final TInputEvent event) { if (debugEvents) { - System.err.printf("Handle event: %s\n", event); + System.err.printf("%s primaryHandleEvent: %s\n", + Thread.currentThread(), event); } TMouseEvent doubleClick = null; @@ -1094,6 +1137,11 @@ public class TApplication implements Runnable { private void secondaryHandleEvent(final TInputEvent event) { TMouseEvent doubleClick = null; + if (debugEvents) { + System.err.printf("%s secondaryHandleEvent: %s\n", + Thread.currentThread(), event); + } + // Peek at the mouse position if (event instanceof TMouseEvent) { TMouseEvent mouse = (TMouseEvent) event; @@ -1161,6 +1209,11 @@ public class TApplication implements Runnable { * Yield to the secondary thread. */ public final void yield() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " yield()\n"); + } + assert (secondaryEventReceiver != null); while (secondaryEventReceiver != null) { @@ -1216,6 +1269,15 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.onIdle(); } + + // Run any invokeLaters + synchronized (invokeLaters) { + for (Runnable invoke: invokeLaters) { + invoke.run(); + } + invokeLaters.clear(); + } + } /** @@ -1242,6 +1304,31 @@ public class TApplication implements Runnable { // TApplication ----------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Place a command on the run queue, and run it before the next round of + * checking I/O. + * + * @param command the command to run later + */ + public void invokeLater(final Runnable command) { + synchronized (invokeLaters) { + invokeLaters.add(command); + } + } + + /** + * Restore the console to sane defaults. This is meant to be used for + * improper exits (e.g. a caught exception in main()), and should not be + * necessary for normal program termination. + */ + public void restoreConsole() { + if (backend != null) { + if (backend instanceof ECMA48Backend) { + backend.shutdown(); + } + } + } + /** * Get the Backend. * @@ -1340,7 +1427,7 @@ public class TApplication implements Runnable { * @return a copy of the list of windows for this application */ public final List getAllWindows() { - List result = new LinkedList(); + List result = new ArrayList(); result.addAll(windows); return result; } @@ -1365,16 +1452,6 @@ public class TApplication implements Runnable { this.focusFollowsMouse = focusFollowsMouse; } - /** - * Display the about dialog. - */ - protected void showAboutDialog() { - messageBox(i18n.getString("aboutDialogTitle"), - MessageFormat.format(i18n.getString("aboutDialogText"), - this.getClass().getPackage().getImplementationVersion()), - TMessageBox.Type.OK); - } - // ------------------------------------------------------------------------ // Screen refresh loop ---------------------------------------------------- // ------------------------------------------------------------------------ @@ -1390,18 +1467,22 @@ public class TApplication implements Runnable { System.err.printf("%d %s invertCell() %d %d\n", System.currentTimeMillis(), Thread.currentThread(), x, y); } - CellAttributes attr = getScreen().getAttrXY(x, y); - if (attr.getForeColorRGB() < 0) { - attr.setForeColor(attr.getForeColor().invert()); - } else { - attr.setForeColorRGB(attr.getForeColorRGB() ^ 0x00ffffff); - } - if (attr.getBackColorRGB() < 0) { - attr.setBackColor(attr.getBackColor().invert()); + Cell cell = getScreen().getCharXY(x, y); + if (cell.isImage()) { + cell.invertImage(); } else { - attr.setBackColorRGB(attr.getBackColorRGB() ^ 0x00ffffff); + if (cell.getForeColorRGB() < 0) { + cell.setForeColor(cell.getForeColor().invert()); + } else { + cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff); + } + if (cell.getBackColorRGB() < 0) { + cell.setBackColor(cell.getBackColor().invert()); + } else { + cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff); + } } - getScreen().putAttrXY(x, y, attr, false); + getScreen().putCharXY(x, y, cell); } /** @@ -1415,25 +1496,62 @@ public class TApplication implements Runnable { System.currentTimeMillis(), Thread.currentThread()); } + // I don't think this does anything useful anymore... if (!repaint) { if (debugThreads) { System.err.printf("%d %s drawAll() !repaint\n", System.currentTimeMillis(), Thread.currentThread()); } - synchronized (getScreen()) { - if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { - // The only thing that has happened is the mouse moved. - // Clear the old position and draw the new position. - invertCell(oldMouseX, oldMouseY); - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint MOUSE\n", + System.currentTimeMillis(), Thread.currentThread()); } - if (getScreen().isDirty()) { - backend.flushScreen(); + + // The only thing that has happened is the mouse moved. + + // Redraw the old cell at that position, and save the cell at + // the new mouse position. + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); } - return; + oldDrawnMouseCell.restoreImage(); + getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY, + oldDrawnMouseCell); + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + // Special case: the entire row containing the mouse has + // to be re-drawn if it has any image data, AND any rows + // in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } + + // Draw mouse at the new position. + invertCell(mouseX, mouseY); + + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; + } + if ((images.size() > 0) || getScreen().isDirty()) { + backend.flushScreen(); } + return; } if (debugThreads) { @@ -1453,7 +1571,7 @@ public class TApplication implements Runnable { } // Draw each window in reverse Z order - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); TWindow topLevel = null; if (sorted.size() > 0) { @@ -1494,7 +1612,7 @@ public class TApplication implements Runnable { 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); if (menu.isActive()) { - menu.drawChildren(); + ((TWindow) menu).drawChildren(); // Reset the screen clipping so we can draw the next title. getScreen().resetClipping(); } @@ -1504,8 +1622,9 @@ public class TApplication implements Runnable { for (TMenu menu: subMenus) { // Reset the screen clipping so we can draw the next sub-menu. getScreen().resetClipping(); - menu.drawChildren(); + ((TWindow) menu).drawChildren(); } + getScreen().resetClipping(); // Draw the status bar of the top-level window TStatusBar statusBar = null; @@ -1525,9 +1644,34 @@ public class TApplication implements Runnable { } // Draw the mouse pointer + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); + } + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + // Special case: the entire row containing the mouse has to be + // re-drawn if it has any image data, AND any rows in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; // Place the cursor if it is visible if (!menuIsActive) { @@ -1543,9 +1687,8 @@ public class TApplication implements Runnable { activeWidget.getCursorAbsoluteY()); cursor = true; } else { - getScreen().putCursor(false, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); + // Turn off the cursor. Also place it at 0,0. + getScreen().putCursor(false, 0, 0); cursor = false; } } @@ -1558,7 +1701,7 @@ public class TApplication implements Runnable { } // Flush the screen contents - if (getScreen().isDirty()) { + if ((images.size() > 0) || getScreen().isDirty()) { backend.flushScreen(); } @@ -1693,7 +1836,16 @@ public class TApplication implements Runnable { assert (activeWindow.getZ() == 0); activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + + // Increment every window Z that is on top of window + for (TWindow w: windows) { + if (w == window) { + continue; + } + if (w.getZ() < window.getZ()) { + w.setZ(w.getZ() + 1); + } + } // Unset activeWindow now before unfocus, so that a window // lifecycle change inside onUnfocus() doesn't call @@ -1792,6 +1944,10 @@ public class TApplication implements Runnable { return; } + // Let window know that it is about to be closed, while it is still + // visible on screen. + window.onPreClose(); + synchronized (windows) { // Whatever window might be moving/dragging, stop it now. for (TWindow w: windows) { @@ -2051,7 +2207,7 @@ public class TApplication implements Runnable { int newHeight1 = ((getScreen().getHeight() - 1) / b); int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (int i = 0; i < sorted.size(); i++) { @@ -2096,7 +2252,7 @@ public class TApplication implements Runnable { } int x = 0; int y = 1; - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (TWindow window: sorted) { @@ -2245,6 +2401,46 @@ public class TApplication implements Runnable { window.setY(windowY); } + // ------------------------------------------------------------------------ + // TImage management ------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Add an image to the list. Note package private access. + * + * @param image the image to add + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void addImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.add(image); + } + + /** + * Remove an image from the list. Note package private access. + * + * @param image the image to remove + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void removeImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.remove(image); + } + // ------------------------------------------------------------------------ // TMenu management ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -2259,7 +2455,7 @@ public class TApplication implements Runnable { */ private boolean mouseOnMenu(final TMouseEvent mouse) { assert (activeMenu != null); - List menus = new LinkedList(subMenus); + List menus = new ArrayList(subMenus); Collections.reverse(menus); for (TMenu menu: menus) { if (menu.mouseWouldHit(mouse)) { @@ -2389,9 +2585,11 @@ public class TApplication implements Runnable { assert (windows.get(0).isActive()); assert (windows.get(0) == activeWindow); assert (!window.isActive()); - activeWindow.onUnfocus(); - activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + if (activeWindow != null) { + activeWindow.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + } activeWindow = window; window.setZ(0); window.setActive(true); @@ -2430,7 +2628,7 @@ public class TApplication implements Runnable { * @return a copy of the menu list */ public final List getAllMenus() { - return new LinkedList(menus); + return new ArrayList(menus); } /** @@ -2501,10 +2699,14 @@ public class TApplication implements Runnable { if (forward) { if (i < menus.size() - 1) { i++; + } else { + i = 0; } } else { if (i > 0) { i--; + } else { + i = menus.size() - 1; } } activeMenu.setActive(false); @@ -2557,6 +2759,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(false); + item.getParent().activate(0); } } } @@ -2570,6 +2773,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if (item.getId() == id) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -2585,6 +2789,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -2897,6 +3102,20 @@ public class TApplication implements Runnable { return openTerminal(x, y, TWindow.RESIZABLE); } + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit); + } + /** * Convenience function to open a terminal window. * @@ -2911,6 +3130,21 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags); } + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2926,6 +3160,22 @@ public class TApplication implements Runnable { return openTerminal(x, y, TWindow.RESIZABLE, commandLine); } + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final String commandLine, final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2942,6 +3192,23 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags, command); } + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String [] command, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, command, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2958,6 +3225,24 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s")); } + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String commandLine, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s"), + closeOnExit); + } + /** * Convenience function to spawn an file open box. * @@ -2986,6 +3271,42 @@ public class TApplication implements Runnable { return box.getFilename(); } + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filter a string that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, final String filter) throws IOException { + + ArrayList filters = new ArrayList(); + filters.add(filter); + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filters a list of strings that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, + final List filters) throws IOException { + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + /** * Convenience function to create a new window and make it active. * Window will be located at (0, 0). @@ -3055,18 +3376,4 @@ public class TApplication implements Runnable { return window; } - /** - * Convenience function to open a file in an editor window and make it - * active. - * - * @param file the file to open - * @return the new editor window - * @throws IOException if a java.io operation throws - */ - public final TEditorWindow addEditor(final File file) throws IOException { - - TEditorWindow editor = new TEditorWindow(this, file); - return editor; - } - } diff --git a/src/jexer/TApplication.properties b/src/jexer/TApplication.properties index d43bc24..bf9bcbe 100644 --- a/src/jexer/TApplication.properties +++ b/src/jexer/TApplication.properties @@ -10,6 +10,3 @@ helpMenuStatus=Access online help exitDialogTitle=Confirmation exitDialogText=Exit application? - -aboutDialogTitle=About -aboutDialogText=Jexer Version {0} diff --git a/src/jexer/TButton.java b/src/jexer/TButton.java index 8aa6270..29f3743 100644 --- a/src/jexer/TButton.java +++ b/src/jexer/TButton.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -226,25 +226,25 @@ public class TButton extends TWidget { } if (inButtonPress) { - getScreen().putCharXY(1, 0, ' ', buttonColor); - getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor); - getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor); + putCharXY(1, 0, ' ', buttonColor); + putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor); + putCharXY(getWidth() - 1, 0, ' ', buttonColor); } else { - getScreen().putCharXY(0, 0, ' ', buttonColor); - getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor); - getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor); + putCharXY(0, 0, ' ', buttonColor); + putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor); + putCharXY(getWidth() - 2, 0, ' ', buttonColor); - getScreen().putCharXY(getWidth() - 1, 0, + putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xDC], shadowColor); - getScreen().hLineXY(1, 1, getWidth() - 1, + hLineXY(1, 1, getWidth() - 1, GraphicsChars.CP437[0xDF], shadowColor); } if (mnemonic.getShortcutIdx() >= 0) { if (inButtonPress) { - getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0, + putCharXY(2 + mnemonic.getShortcutIdx(), 0, mnemonic.getShortcut(), menuMnemonicColor); } else { - getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 0, + putCharXY(1 + mnemonic.getShortcutIdx(), 0, mnemonic.getShortcut(), menuMnemonicColor); } diff --git a/src/jexer/TCalendar.java b/src/jexer/TCalendar.java index c117fc1..b5ecf4d 100644 --- a/src/jexer/TCalendar.java +++ b/src/jexer/TCalendar.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -232,27 +232,27 @@ public class TCalendar extends TWidget { // Fill in the interior background for (int i = 0; i < getHeight(); i++) { - getScreen().hLineXY(0, i, getWidth(), ' ', backgroundColor); + hLineXY(0, i, getWidth(), ' ', backgroundColor); } // Draw the title String title = String.format("%tB %tY", displayCalendar, displayCalendar); int titleLeft = (getWidth() - title.length() - 2) / 2; - getScreen().putCharXY(titleLeft, 0, ' ', titleColor); - getScreen().putStringXY(titleLeft + 1, 0, title, titleColor); - getScreen().putCharXY(titleLeft + title.length() + 1, 0, ' ', + putCharXY(titleLeft, 0, ' ', titleColor); + putStringXY(titleLeft + 1, 0, title, titleColor); + putCharXY(titleLeft + title.length() + 1, 0, ' ', titleColor); // Arrows - getScreen().putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor); - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW, + putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor); + putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW, arrowColor); /* * Now draw out the days. */ - getScreen().putStringXY(0, 1, " S M T W T F S ", dayColor); + putStringXY(0, 1, " S M T W T F S ", dayColor); int lastDayNumber = displayCalendar.getActualMaximum( Calendar.DAY_OF_MONTH); GregorianCalendar firstOfMonth = new GregorianCalendar(); @@ -274,10 +274,10 @@ public class TCalendar extends TWidget { && (displayCalendar.get(Calendar.YEAR) == calendar.get( Calendar.YEAR)) ) { - getScreen().putStringXY(dayColumn, row, + putStringXY(dayColumn, row, String.format(" %2d ", dayOfMonth), selectedDayColor); } else { - getScreen().putStringXY(dayColumn, row, + putStringXY(dayColumn, row, String.format(" %2d ", dayOfMonth), dayColor); } dayColumn += 4; diff --git a/src/jexer/TCheckBox.java b/src/jexer/TCheckBox.java index 188db7e..84c2e41 100644 --- a/src/jexer/TCheckBox.java +++ b/src/jexer/TCheckBox.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -53,6 +53,11 @@ public class TCheckBox extends TWidget { */ private String label; + /** + * If true, use the window's background color. + */ + private boolean useWindowBackground = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -144,15 +149,19 @@ public class TCheckBox extends TWidget { } else { checkboxColor = getTheme().getColor("tcheckbox.inactive"); } + if (useWindowBackground) { + CellAttributes background = getWindow().getBackground(); + checkboxColor.setBackColor(background.getBackColor()); + } - getScreen().putCharXY(0, 0, '[', checkboxColor); + putCharXY(0, 0, '[', checkboxColor); if (checked) { - getScreen().putCharXY(1, 0, GraphicsChars.CHECK, checkboxColor); + putCharXY(1, 0, GraphicsChars.CHECK, checkboxColor); } else { - getScreen().putCharXY(1, 0, ' ', checkboxColor); + putCharXY(1, 0, ' ', checkboxColor); } - getScreen().putCharXY(2, 0, ']', checkboxColor); - getScreen().putStringXY(4, 0, label, checkboxColor); + putCharXY(2, 0, ']', checkboxColor); + putStringXY(4, 0, label, checkboxColor); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TComboBox.java b/src/jexer/TComboBox.java index 38224b8..bb223c3 100644 --- a/src/jexer/TComboBox.java +++ b/src/jexer/TComboBox.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -61,6 +61,11 @@ public class TComboBox extends TWidget { */ private TAction updateAction = null; + /** + * If true, the field cannot be updated to a value not on the list. + */ + private boolean limitToListValue = true; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -87,9 +92,11 @@ public class TComboBox extends TWidget { // Set parent and window super(parent, x, y, width, 1); + assert (values != null); + this.updateAction = updateAction; - field = new TField(this, 0, 0, width - 1, false, "", + field = new TField(this, 0, 0, width - 3, false, "", updateAction, null); if (valuesIndex >= 0) { field.setText(values.get(valuesIndex)); @@ -102,18 +109,27 @@ public class TComboBox extends TWidget { list.setEnabled(false); list.setVisible(false); TComboBox.this.setHeight(1); - TComboBox.this.activate(field); + if (TComboBox.this.limitToListValue == false) { + TComboBox.this.activate(field); + } if (updateAction != null) { updateAction.DO(); } } } ); + if (valuesIndex >= 0) { + list.setSelectedIndex(valuesIndex); + } list.setEnabled(false); list.setVisible(false); setHeight(1); - activate(field); + if (limitToListValue) { + field.setEnabled(false); + } else { + activate(field); + } } // ------------------------------------------------------------------------ @@ -128,7 +144,8 @@ public class TComboBox extends TWidget { */ private boolean mouseOnArrow(final TMouseEvent mouse) { if ((mouse.getY() == 0) - && (mouse.getX() == getWidth() - 1) + && (mouse.getX() >= getWidth() - 3) + && (mouse.getX() <= getWidth() - 1) ) { return true; } @@ -148,7 +165,9 @@ public class TComboBox extends TWidget { list.setEnabled(false); list.setVisible(false); setHeight(1); - activate(field); + if (limitToListValue == false) { + activate(field); + } } else { list.setEnabled(true); list.setVisible(true); @@ -156,6 +175,9 @@ public class TComboBox extends TWidget { activate(list); } } + + // Pass to parent for the things we don't care about. + super.onMouseDown(mouse); } /** @@ -165,6 +187,18 @@ public class TComboBox extends TWidget { */ @Override public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbEsc)) { + if (list.isActive()) { + list.setEnabled(false); + list.setVisible(false); + setHeight(1); + if (limitToListValue == false) { + activate(field); + } + return; + } + } + if (keypress.equals(kbAltDown)) { list.setEnabled(true); list.setVisible(true); @@ -181,7 +215,9 @@ public class TComboBox extends TWidget { list.setEnabled(false); list.setVisible(false); setHeight(1); - activate(field); + if (limitToListValue == false) { + activate(field); + } return; } } @@ -201,13 +237,29 @@ public class TComboBox extends TWidget { public void draw() { CellAttributes comboBoxColor; - if (isAbsoluteActive()) { + if (!isAbsoluteActive()) { + // We lost focus, turn off the list. + if (list.isActive()) { + list.setEnabled(false); + list.setVisible(false); + setHeight(1); + if (limitToListValue == false) { + activate(field); + } + } + } + + if (isAbsoluteActive() && (limitToListValue == false)) { comboBoxColor = getTheme().getColor("tcombobox.active"); } else { comboBoxColor = getTheme().getColor("tcombobox.inactive"); } - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, + putCharXY(getWidth() - 3, 0, GraphicsChars.DOWNARROWLEFT, + comboBoxColor); + putCharXY(getWidth() - 2, 0, GraphicsChars.DOWNARROW, + comboBoxColor); + putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROWRIGHT, comboBoxColor); } @@ -240,4 +292,33 @@ public class TComboBox extends TWidget { list.setSelectedIndex(-1); } + /** + * Set combobox text to one of the list values. + * + * @param index the index in the list + */ + public void setIndex(final int index) { + list.setSelectedIndex(index); + field.setText(list.getSelected()); + } + + /** + * Get a copy of the list of strings to display. + * + * @return the list of strings + */ + public final List getList() { + return list.getList(); + } + + /** + * Set the new list of strings to display. + * + * @param list new list of strings + */ + public final void setList(final List list) { + this.list.setList(list); + field.setText(""); + } + } diff --git a/src/jexer/TCommand.java b/src/jexer/TCommand.java index b6a0411..49b7e67 100644 --- a/src/jexer/TCommand.java +++ b/src/jexer/TCommand.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -170,11 +170,11 @@ public class TCommand { // ------------------------------------------------------------------------ /** - * Protected constructor. Subclasses can be used to define new commands. + * Public constructor. * * @param type the Type of command, one of EXIT, CASCADE, etc. */ - protected TCommand(final int type) { + public TCommand(final int type) { this.type = type; } diff --git a/src/jexer/TDesktop.java b/src/jexer/TDesktop.java index 23edfb8..20e6fd0 100644 --- a/src/jexer/TDesktop.java +++ b/src/jexer/TDesktop.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TDirectoryList.java b/src/jexer/TDirectoryList.java index 9ff0b6b..e00d14f 100644 --- a/src/jexer/TDirectoryList.java +++ b/src/jexer/TDirectoryList.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -30,8 +30,9 @@ package jexer; import java.io.File; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * TDirectoryList shows the files within a directory. @@ -45,13 +46,18 @@ public class TDirectoryList extends TList { /** * Files in the directory. */ - private List files; + private Map files; /** * Root path containing files to display. */ private File path; + /** + * The list of filters that a file must match in order to be displayed. + */ + private List filters; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -69,7 +75,7 @@ public class TDirectoryList extends TList { public TDirectoryList(final TWidget parent, final String path, final int x, final int y, final int width, final int height) { - this(parent, path, x, y, width, height, null); + this(parent, path, x, y, width, height, null, null, null); } /** @@ -81,13 +87,61 @@ public class TDirectoryList extends TList { * @param y row relative to parent * @param width width of text area * @param height height of text area - * @param action action to perform when an item is selected + * @param action action to perform when an item is selected (enter or + * double-click) */ public TDirectoryList(final TWidget parent, final String path, final int x, final int y, final int width, final int height, final TAction action) { + this(parent, path, x, y, width, height, action, null, null); + } + + /** + * Public constructor. + * + * @param parent parent widget + * @param path directory path, must be a directory + * @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 action action to perform when an item is selected (enter or + * double-click) + * @param singleClickAction action to perform when an item is selected + * (single-click) + */ + public TDirectoryList(final TWidget parent, final String path, final int x, + final int y, final int width, final int height, final TAction action, + final TAction singleClickAction) { + + this(parent, path, x, y, width, height, action, singleClickAction, + null); + } + + /** + * Public constructor. + * + * @param parent parent widget + * @param path directory path, must be a directory + * @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 action action to perform when an item is selected (enter or + * double-click) + * @param singleClickAction action to perform when an item is selected + * (single-click) + * @param filters a list of strings that files must match to be displayed + */ + public TDirectoryList(final TWidget parent, final String path, final int x, + final int y, final int width, final int height, final TAction action, + final TAction singleClickAction, final List filters) { + super(parent, null, x, y, width, height, action); - files = new ArrayList(); + files = new HashMap(); + this.filters = filters; + this.singleClickAction = singleClickAction; + setPath(path); } @@ -120,11 +174,29 @@ public class TDirectoryList extends TList { if (newFiles[i].isDirectory()) { continue; } - files.add(newFiles[i]); - newStrings.add(renderFile(files.size() - 1)); + if (filters != null) { + for (String pattern: filters) { + + /* + System.err.println("newFiles[i] " + + newFiles[i].getName() + " " + pattern + + " " + newFiles[i].getName().matches(pattern)); + */ + + if (newFiles[i].getName().matches(pattern)) { + String key = renderFile(newFiles[i]); + files.put(key, newFiles[i]); + newStrings.add(key); + break; + } + } + } else { + String key = renderFile(newFiles[i]); + files.put(key, newFiles[i]); + newStrings.add(key); + } } } - Collections.sort(newStrings); setList(newStrings); // Select the first entry @@ -139,18 +211,17 @@ public class TDirectoryList extends TList { * @return the path */ public File getPath() { - path = files.get(getSelectedIndex()); + path = files.get(getSelected()); return path; } /** * Format one of the entries for drawing on the screen. * - * @param index index into files + * @param file the File * @return the line to draw */ - private String renderFile(final int index) { - File file = files.get(index); + private String renderFile(final File file) { String name = file.getName(); if (name.length() > 20) { name = name.substring(0, 17) + "..."; diff --git a/src/jexer/TEditColorThemeWindow.java b/src/jexer/TEditColorThemeWindow.java index bc3712b..55be8aa 100644 --- a/src/jexer/TEditColorThemeWindow.java +++ b/src/jexer/TEditColorThemeWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -219,16 +219,15 @@ public class TEditColorThemeWindow extends TWindow { CellAttributes background = getWindow().getBackground(); CellAttributes attr = new CellAttributes(); - getScreen().drawBox(0, 0, getWidth(), getHeight(), border, - background, 1, false); + drawBox(0, 0, getWidth(), getHeight(), border, background, 1, + false); attr.setTo(getTheme().getColor("twindow.background.modal")); if (isActive()) { attr.setForeColor(getTheme().getColor("tlabel").getForeColor()); attr.setBold(getTheme().getColor("tlabel").isBold()); } - getScreen().putStringXY(1, 0, i18n.getString("foregroundLabel"), - attr); + putStringXY(1, 0, i18n.getString("foregroundLabel"), attr); // Have to draw the colors manually because the int value matches // SGR, not CGA. @@ -275,12 +274,11 @@ public class TEditColorThemeWindow extends TWindow { // Use white-on-black for black. All other colors use // black-on-whatever. attr.reset(); - getScreen().putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], - attr); + putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr); } else { attr.setForeColor(color); attr.setBold(bold); - getScreen().putCharXY(dotX, dotY, '\u25D8', attr); + putCharXY(dotX, dotY, '\u25D8', attr); } } @@ -493,16 +491,15 @@ public class TEditColorThemeWindow extends TWindow { CellAttributes background = getWindow().getBackground(); CellAttributes attr = new CellAttributes(); - getScreen().drawBox(0, 0, getWidth(), getHeight(), border, - background, 1, false); + drawBox(0, 0, getWidth(), getHeight(), border, background, 1, + false); attr.setTo(getTheme().getColor("twindow.background.modal")); if (isActive()) { attr.setForeColor(getTheme().getColor("tlabel").getForeColor()); attr.setBold(getTheme().getColor("tlabel").isBold()); } - getScreen().putStringXY(1, 0, i18n.getString("backgroundLabel"), - attr); + putStringXY(1, 0, i18n.getString("backgroundLabel"), attr); // Have to draw the colors manually because the int value matches // SGR, not CGA. @@ -531,11 +528,10 @@ public class TEditColorThemeWindow extends TWindow { // Use white-on-black for black. All other colors use // black-on-whatever. attr.reset(); - getScreen().putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], - attr); + putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr); } else { attr.setForeColor(color); - getScreen().putCharXY(dotX, dotY, '\u25D8', attr); + putCharXY(dotX, dotY, '\u25D8', attr); } } @@ -739,16 +735,16 @@ public class TEditColorThemeWindow extends TWindow { attr.setForeColor(getTheme().getColor("tlabel").getForeColor()); attr.setBold(getTheme().getColor("tlabel").isBold()); } - getScreen().putStringXY(3, 2, i18n.getString("colorName"), attr); + putStringXY(3, 2, i18n.getString("colorName"), attr); // Draw the sample text box attr.reset(); attr.setForeColor(foreground.color); attr.setBold(foreground.bold); attr.setBackColor(background.color); - getScreen().putStringXY(getWidth() - 17, getHeight() - 6, + putStringXY(getWidth() - 17, getHeight() - 6, i18n.getString("textTextText"), attr); - getScreen().putStringXY(getWidth() - 17, getHeight() - 5, + putStringXY(getWidth() - 17, getHeight() - 5, i18n.getString("textTextText"), attr); } diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 94f1a3b..f65ba6b 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TEditorWindow.java b/src/jexer/TEditorWindow.java index 4a638fc..4951311 100644 --- a/src/jexer/TEditorWindow.java +++ b/src/jexer/TEditorWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TField.java b/src/jexer/TField.java index aa0d18f..b4330b4 100644 --- a/src/jexer/TField.java +++ b/src/jexer/TField.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -358,9 +358,8 @@ public class TField extends TWidget { if (end > text.length()) { end = text.length(); } - getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); - getScreen().putStringXY(0, 0, text.substring(windowStart, end), - fieldColor); + hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); + putStringXY(0, 0, text.substring(windowStart, end), fieldColor); // Fix the cursor, it will be rendered by TApplication.drawAll(). updateCursor(); @@ -385,6 +384,7 @@ public class TField extends TWidget { * @param text the new field text */ public void setText(final String text) { + assert (text != null); this.text = text; position = 0; windowStart = 0; diff --git a/src/jexer/TFileOpenBox.java b/src/jexer/TFileOpenBox.java index 6f46a03..ac23cfd 100644 --- a/src/jexer/TFileOpenBox.java +++ b/src/jexer/TFileOpenBox.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -30,8 +30,10 @@ package jexer; import java.io.File; import java.io.IOException; +import java.util.List; import java.util.ResourceBundle; +import jexer.backend.SwingTerminal; import jexer.bits.GraphicsChars; import jexer.event.TKeypressEvent; import jexer.ttree.TDirectoryTreeItem; @@ -77,7 +79,12 @@ public class TFileOpenBox extends TWindow { /** * Button will be labeled "Save". */ - SAVE + SAVE, + + /** + * Button will be labeled "Select". + */ + SELECT } // ------------------------------------------------------------------------ @@ -114,6 +121,11 @@ public class TFileOpenBox extends TWindow { */ private TButton openButton; + /** + * The type of box this is (OPEN, SAVE, or SELECT). + */ + private Type type = Type.OPEN; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -129,6 +141,21 @@ public class TFileOpenBox extends TWindow { public TFileOpenBox(final TApplication application, final String path, final Type type) throws IOException { + this(application, path, type, null); + } + + /** + * Public constructor. The file open box will be centered on screen. + * + * @param application the TApplication that manages this window + * @param path path of selected file + * @param type one of the Type constants + * @param filters a list of strings that files must match to be displayed + * @throws IOException of a java.io operation throws + */ + public TFileOpenBox(final TApplication application, final String path, + final Type type, final List filters) throws IOException { + // Register with the TApplication super(application, "", 0, 0, 76, 22, MODAL); @@ -140,7 +167,11 @@ public class TFileOpenBox extends TWindow { try { checkFilename(entryField.getText()); } catch (IOException e) { - e.printStackTrace(); + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } } } }, null); @@ -154,10 +185,16 @@ public class TFileOpenBox extends TWindow { File selectedDir = ((TDirectoryTreeItem) item).getFile(); try { directoryList.setPath(selectedDir.getCanonicalPath()); - openButton.setEnabled(false); + if (type == Type.OPEN) { + openButton.setEnabled(false); + } activate(treeView); } catch (IOException e) { - e.printStackTrace(); + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } } } } @@ -174,12 +211,34 @@ public class TFileOpenBox extends TWindow { entryField.onKeypress(new TKeypressEvent(kbEnd)); openButton.setEnabled(true); activate(entryField); + checkFilename(entryField.getText()); } catch (IOException e) { - e.printStackTrace(); + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } } } - } - ); + }, + new TAction() { + public void DO() { + try { + File newPath = directoryList.getPath(); + entryField.setText(newPath.getCanonicalPath()); + entryField.onKeypress(new TKeypressEvent(kbEnd)); + openButton.setEnabled(true); + activate(entryField); + } catch (IOException e) { + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } + } + } + }, + filters); String openLabel = ""; switch (type) { @@ -191,9 +250,14 @@ public class TFileOpenBox extends TWindow { openLabel = i18n.getString("saveButton"); setTitle(i18n.getString("saveTitle")); break; + case SELECT: + openLabel = i18n.getString("selectButton"); + setTitle(i18n.getString("selectTitle")); + break; default: throw new IllegalArgumentException("Invalid type: " + type); } + this.type = type; // Setup button actions openButton = addButton(openLabel, this.getWidth() - 12, 3, @@ -202,12 +266,18 @@ public class TFileOpenBox extends TWindow { try { checkFilename(entryField.getText()); } catch (IOException e) { - e.printStackTrace(); + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } } } } ); - openButton.setEnabled(false); + if (type == Type.OPEN) { + openButton.setEnabled(false); + } addButton(i18n.getString("cancelButton"), getWidth() - 12, 5, new TAction() { @@ -265,10 +335,16 @@ public class TFileOpenBox extends TWindow { File selectedDir = ((TDirectoryTreeItem) item).getFile(); try { directoryList.setPath(selectedDir.getCanonicalPath()); - openButton.setEnabled(false); + if (type == Type.OPEN) { + openButton.setEnabled(false); + } activate(treeView); } catch (IOException e) { - e.printStackTrace(); + // If the backend is Swing, we can emit the stack trace + // to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } } return; } @@ -288,7 +364,7 @@ public class TFileOpenBox extends TWindow { @Override public void draw() { super.draw(); - getScreen().vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE, + vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE, getBackground()); } @@ -324,9 +400,15 @@ public class TFileOpenBox extends TWindow { treeViewRoot = new TDirectoryTreeItem(treeView, newFilename, true); treeView.setTreeRoot(treeViewRoot, true); - openButton.setEnabled(false); + if (type == Type.OPEN) { + openButton.setEnabled(false); + } directoryList.setPath(newFilename); } + } else if (type != Type.OPEN) { + filename = newFilename; + getApplication().closeWindow(this); + return; } } diff --git a/src/jexer/TFileOpenBox.properties b/src/jexer/TFileOpenBox.properties index 1dee738..ef40e86 100644 --- a/src/jexer/TFileOpenBox.properties +++ b/src/jexer/TFileOpenBox.properties @@ -3,3 +3,5 @@ openTitle=Open File... saveButton=\ &Save\ saveTitle=Save File... cancelButton=&Cancel +selectButton=S&elect +selectTitle=Select File... diff --git a/src/jexer/THScroller.java b/src/jexer/THScroller.java index eab1a15..a07bcd7 100644 --- a/src/jexer/THScroller.java +++ b/src/jexer/THScroller.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -216,19 +216,15 @@ public class THScroller extends TWidget { public void draw() { CellAttributes arrowColor = getTheme().getColor("tscroller.arrows"); CellAttributes barColor = getTheme().getColor("tscroller.bar"); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor); - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10], - arrowColor); + putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor); + putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10], arrowColor); // Place the box if (rightValue > leftValue) { - getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], - barColor); - getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX, - arrowColor); + hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], barColor); + putCharXY(boxPosition(), 0, GraphicsChars.BOX, arrowColor); } else { - getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, - barColor); + hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, barColor); } } diff --git a/src/jexer/TImage.java b/src/jexer/TImage.java new file mode 100644 index 0000000..1a9aa9e --- /dev/null +++ b/src/jexer/TImage.java @@ -0,0 +1,499 @@ +/* + * 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.awt.image.BufferedImage; + +import jexer.backend.ECMA48Terminal; +import jexer.backend.MultiScreen; +import jexer.backend.SwingTerminal; +import jexer.bits.Cell; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * TImage renders a piece of a bitmap image on screen. + */ +public class TImage extends TWidget { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The action to perform when the user clicks on the image. + */ + private TAction clickAction; + + /** + * The image to display. + */ + private BufferedImage image; + + /** + * The original image from construction time. + */ + private BufferedImage originalImage; + + /** + * The current scaling factor for the image. + */ + private double scaleFactor = 1.0; + + /** + * The current clockwise rotation for the image. + */ + private int clockwise = 0; + + /** + * Left column of the image. 0 is the left-most column. + */ + private int left; + + /** + * Top row of the image. 0 is the top-most row. + */ + private int top; + + /** + * The cells containing the broken up image pieces. + */ + private Cell cells[][]; + + /** + * The number of rows in cells[]. + */ + private int cellRows; + + /** + * The number of columns in cells[]. + */ + private int cellColumns; + + /** + * Last text width value. + */ + private int lastTextWidth = -1; + + /** + * Last text height value. + */ + private int lastTextHeight = -1; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width number of text cells for width of the image + * @param height number of text cells for height of the image + * @param image the image to display + * @param left left column of the image. 0 is the left-most column. + * @param top top row of the image. 0 is the top-most row. + * @param clickAction function to call when mouse is pressed + */ + public TImage(final TWidget parent, final int x, final int y, + final int width, final int height, + final BufferedImage image, final int left, final int top, + final TAction clickAction) { + + // Set parent and window + super(parent, x, y, width, height); + + setCursorVisible(false); + this.originalImage = image; + this.left = left; + this.top = top; + this.clickAction = clickAction; + + sizeToImage(true); + + getApplication().addImage(this); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Subclasses should override this method to cleanup resources. This is + * called by TWindow.onClose(). + */ + @Override + protected void close() { + getApplication().removeImage(this); + super.close(); + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (clickAction != null) { + clickAction.DO(); + return; + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (!keypress.getKey().isFnKey()) { + if (keypress.getKey().getChar() == '+') { + // Make the image bigger. + scaleFactor *= 1.25; + image = null; + sizeToImage(true); + return; + } + if (keypress.getKey().getChar() == '-') { + // Make the image smaller. + scaleFactor *= 0.80; + image = null; + sizeToImage(true); + return; + } + } + if (keypress.equals(kbAltUp)) { + // Make the image bigger. + scaleFactor *= 1.25; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltDown)) { + // Make the image smaller. + scaleFactor *= 0.80; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltRight)) { + // Rotate clockwise. + clockwise++; + clockwise %= 4; + image = null; + sizeToImage(true); + return; + } + if (keypress.equals(kbAltLeft)) { + // Rotate counter-clockwise. + clockwise--; + if (clockwise < 0) { + clockwise = 3; + } + image = null; + sizeToImage(true); + return; + } + + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the image. + */ + @Override + public void draw() { + sizeToImage(false); + + // We have already broken the image up, just draw the last set of + // cells. + for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) { + if ((left + x) * lastTextWidth > image.getWidth()) { + continue; + } + + for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) { + if ((top + y) * lastTextHeight > image.getHeight()) { + continue; + } + assert (x + left < cellColumns); + assert (y + top < cellRows); + + getWindow().putCharXY(x, y, cells[x + left][y + top]); + } + } + + } + + // ------------------------------------------------------------------------ + // TImage ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Size cells[][] according to the screen font size. + * + * @param always if true, always resize the cells + */ + private void sizeToImage(final boolean always) { + int textWidth = 16; + int textHeight = 20; + + if (getScreen() instanceof SwingTerminal) { + SwingTerminal terminal = (SwingTerminal) getScreen(); + + textWidth = terminal.getTextWidth(); + textHeight = terminal.getTextHeight(); + } if (getScreen() instanceof MultiScreen) { + MultiScreen terminal = (MultiScreen) getScreen(); + + textWidth = terminal.getTextWidth(); + textHeight = terminal.getTextHeight(); + } else if (getScreen() instanceof ECMA48Terminal) { + ECMA48Terminal terminal = (ECMA48Terminal) getScreen(); + + textWidth = terminal.getTextWidth(); + textHeight = terminal.getTextHeight(); + } + + if (image == null) { + image = scaleImage(originalImage, scaleFactor); + image = rotateImage(image, clockwise); + } + + if ((always == true) || + ((textWidth > 0) + && (textWidth != lastTextWidth) + && (textHeight > 0) + && (textHeight != lastTextHeight)) + ) { + cellColumns = image.getWidth() / textWidth; + if (cellColumns * textWidth < image.getWidth()) { + cellColumns++; + } + cellRows = image.getHeight() / textHeight; + if (cellRows * textHeight < image.getHeight()) { + cellRows++; + } + + // Break the image up into an array of cells. + cells = new Cell[cellColumns][cellRows]; + + for (int x = 0; x < cellColumns; x++) { + for (int y = 0; y < cellRows; y++) { + + int width = textWidth; + if ((x + 1) * textWidth > image.getWidth()) { + width = image.getWidth() - (x * textWidth); + } + int height = textHeight; + if ((y + 1) * textHeight > image.getHeight()) { + height = image.getHeight() - (y * textHeight); + } + + Cell cell = new Cell(); + cell.setImage(image.getSubimage(x * textWidth, + y * textHeight, width, height)); + + cells[x][y] = cell; + } + } + + lastTextWidth = textWidth; + lastTextHeight = textHeight; + } + + if ((left + getWidth()) > cellColumns) { + left = cellColumns - getWidth(); + } + if (left < 0) { + left = 0; + } + if ((top + getHeight()) > cellRows) { + top = cellRows - getHeight(); + } + if (top < 0) { + top = 0; + } + } + + /** + * Get the top corner to render. + * + * @return the top row + */ + public int getTop() { + return top; + } + + /** + * Set the top corner to render. + * + * @param top the new top row + */ + public void setTop(final int top) { + this.top = top; + if (this.top > cellRows - getHeight()) { + this.top = cellRows - getHeight(); + } + if (this.top < 0) { + this.top = 0; + } + } + + /** + * Get the left corner to render. + * + * @return the left column + */ + public int getLeft() { + return left; + } + + /** + * Set the left corner to render. + * + * @param left the new left column + */ + public void setLeft(final int left) { + this.left = left; + if (this.left > cellColumns - getWidth()) { + this.left = cellColumns - getWidth(); + } + if (this.left < 0) { + this.left = 0; + } + } + + /** + * Get the number of text cell rows for this image. + * + * @return the number of rows + */ + public int getRows() { + return cellRows; + } + + /** + * Get the number of text cell columns for this image. + * + * @return the number of columns + */ + public int getColumns() { + return cellColumns; + } + + /** + * Scale an image by to be scaleFactor size. + * + * @param image the image to scale + * @param factor the scale to make the new image + */ + private BufferedImage scaleImage(final BufferedImage image, + final double factor) { + + if (Math.abs(factor - 1.0) < 0.03) { + // If we are within 3% of 1.0, just return the original image. + return image; + } + + int width = (int) (image.getWidth() * factor); + int height = (int) (image.getHeight() * factor); + + BufferedImage newImage = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + + java.awt.Graphics gr = newImage.createGraphics(); + gr.drawImage(image, 0, 0, width, height, null); + gr.dispose(); + + return newImage; + } + + /** + * Rotate an image either clockwise or counterclockwise. + * + * @param image the image to scale + * @param clockwise number of turns clockwise + */ + private BufferedImage rotateImage(final BufferedImage image, + final int clockwise) { + + if (clockwise % 4 == 0) { + return image; + } + + BufferedImage newImage = null; + + if (clockwise % 4 == 1) { + // 90 degrees clockwise + newImage = new BufferedImage(image.getHeight(), image.getWidth(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(y, x, + image.getRGB(x, image.getHeight() - 1 - y)); + } + } + } else if (clockwise % 4 == 2) { + // 180 degrees clockwise + newImage = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(x, y, + image.getRGB(image.getWidth() - 1 - x, + image.getHeight() - 1 - y)); + } + } + } else if (clockwise % 4 == 3) { + // 270 degrees clockwise + newImage = new BufferedImage(image.getHeight(), image.getWidth(), + BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + newImage.setRGB(y, x, + image.getRGB(image.getWidth() - 1 - x, y)); + } + } + } + + return newImage; + } + +} diff --git a/src/jexer/TImageWindow.java b/src/jexer/TImageWindow.java new file mode 100644 index 0000000..d38cd25 --- /dev/null +++ b/src/jexer/TImageWindow.java @@ -0,0 +1,280 @@ +/* + * 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.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import static jexer.TKeypress.*; + +/** + * TImageWindow shows an image with scrollbars. + */ +public class TImageWindow extends TScrollableWindow { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The number of lines to scroll on mouse wheel up/down. + */ + private static final int wheelScrollSize = 3; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Hang onto the TImage so I can resize it with the window. + */ + private TImage imageField; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor opens a file. + * + * @param parent the main application + * @param file the file to open + * @throws IOException if a java.io operation throws + */ + public TImageWindow(final TApplication parent, + final File file) throws IOException { + + this(parent, file, 0, 0, parent.getScreen().getWidth(), + parent.getScreen().getHeight() - 2); + } + + /** + * Public constructor opens a file. + * + * @param parent the main application + * @param file the file to open + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + * @throws IOException if a java.io operation throws + */ + public TImageWindow(final TApplication parent, final File file, + final int x, final int y, final int width, + final int height) throws IOException { + + super(parent, file.getName(), x, y, width, height, RESIZABLE); + + BufferedImage image = ImageIO.read(file); + + imageField = new TImage(this, 0, 0, getWidth() - 2, getHeight() - 2, + image, 0, 0, null); + setTitle(file.getName()); + + setupAfterImage(); + } + + /** + * Setup other fields after the image is created. + */ + private void setupAfterImage() { + if (imageField.getRows() < getHeight() - 2) { + imageField.setHeight(imageField.getRows()); + setHeight(imageField.getRows() + 2); + } + if (imageField.getColumns() < getWidth() - 2) { + imageField.setWidth(imageField.getColumns()); + setWidth(imageField.getColumns() + 2); + } + + hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20); + vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2); + setTopValue(0); + setBottomValue(imageField.getRows() - imageField.getHeight()); + setLeftValue(0); + setRightValue(imageField.getColumns() - imageField.getWidth()); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + // Use TWidget's code to pass the event to the children. + super.onMouseDown(mouse); + + if (mouse.isMouseWheelUp()) { + imageField.setTop(imageField.getTop() - wheelScrollSize); + } else if (mouse.isMouseWheelDown()) { + imageField.setTop(imageField.getTop() + wheelScrollSize); + } + setVerticalValue(imageField.getTop()); + } + + /** + * Handle mouse release events. + * + * @param mouse mouse button release event + */ + @Override + public void onMouseUp(final TMouseEvent mouse) { + // Use TWidget's code to pass the event to the children. + super.onMouseUp(mouse); + + if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) { + // Clicked/dragged on vertical scrollbar + imageField.setTop(getVerticalValue()); + } + if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) { + // Clicked/dragged on horizontal scrollbar + imageField.setLeft(getHorizontalValue()); + } + } + + /** + * Method that subclasses can override to handle mouse movements. + * + * @param mouse mouse motion event + */ + @Override + public void onMouseMotion(final TMouseEvent mouse) { + // Use TWidget's code to pass the event to the children. + super.onMouseMotion(mouse); + + if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) { + // Clicked/dragged on vertical scrollbar + imageField.setTop(getVerticalValue()); + } + if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) { + // Clicked/dragged on horizontal scrollbar + imageField.setLeft(getHorizontalValue()); + } + } + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + if (event.getType() == TResizeEvent.Type.WIDGET) { + // Resize the image field + TResizeEvent imageSize = new TResizeEvent(TResizeEvent.Type.WIDGET, + event.getWidth() - 2, event.getHeight() - 2); + imageField.onResize(imageSize); + + // Have TScrollableWindow handle the scrollbars + super.onResize(event); + return; + } + + // Pass to children instead + for (TWidget widget: getChildren()) { + widget.onResize(event); + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbUp)) { + verticalDecrement(); + imageField.setTop(getVerticalValue()); + return; + } + if (keypress.equals(kbDown)) { + verticalIncrement(); + imageField.setTop(getVerticalValue()); + return; + } + if (keypress.equals(kbPgUp)) { + bigVerticalDecrement(); + imageField.setTop(getVerticalValue()); + return; + } + if (keypress.equals(kbPgDn)) { + bigVerticalIncrement(); + imageField.setTop(getVerticalValue()); + return; + } + if (keypress.equals(kbRight)) { + horizontalIncrement(); + imageField.setLeft(getHorizontalValue()); + return; + } + if (keypress.equals(kbLeft)) { + horizontalDecrement(); + imageField.setLeft(getHorizontalValue()); + return; + } + + // We did not take it, let the TImage instance see it. + super.onKeypress(keypress); + + setVerticalValue(imageField.getTop()); + setBottomValue(imageField.getRows() - imageField.getHeight()); + setHorizontalValue(imageField.getLeft()); + setRightValue(imageField.getColumns() - imageField.getWidth()); + } + + // ------------------------------------------------------------------------ + // TWindow ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the window. + */ + @Override + public void draw() { + // Draw as normal. + super.draw(); + + // We have to get the scrollbar values after we have let the image + // try to draw. + setBottomValue(imageField.getRows() - imageField.getHeight()); + setRightValue(imageField.getColumns() - imageField.getWidth()); + } + +} diff --git a/src/jexer/TInputBox.java b/src/jexer/TInputBox.java index 487664a..813af88 100644 --- a/src/jexer/TInputBox.java +++ b/src/jexer/TInputBox.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -110,17 +110,20 @@ public class TInputBox extends TMessageBox { setHeight(getHeight() + 2); field = addField(1, getHeight() - 6, getWidth() - 4, false, text); + // Set the secondaryThread to run me + getApplication().enableSecondaryEventReceiver(this); + // Yield to the secondary thread. When I come back from the // constructor response will already be set. getApplication().yield(); } // ------------------------------------------------------------------------ - // TInputBox -------------------------------------------------------------- + // TMessageBox ------------------------------------------------------------ // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ - // TMessageBox ------------------------------------------------------------ + // TInputBox -------------------------------------------------------------- // ------------------------------------------------------------------------ /** diff --git a/src/jexer/TKeypress.java b/src/jexer/TKeypress.java index e0aa3b3..5808545 100644 --- a/src/jexer/TKeypress.java +++ b/src/jexer/TKeypress.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -796,6 +796,11 @@ public class TKeypress { */ @Override public String toString() { + // Special case: Enter is " " + if (equals(kbEnter)) { + return "\u25C0\u2500\u2518"; + } + if (isFunctionKey) { switch (keyCode) { case F1: diff --git a/src/jexer/TLabel.java b/src/jexer/TLabel.java index 71530f0..69e1efd 100644 --- a/src/jexer/TLabel.java +++ b/src/jexer/TLabel.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -124,7 +124,7 @@ public class TLabel extends TWidget { CellAttributes background = getWindow().getBackground(); color.setBackColor(background.getBackColor()); } - getScreen().putStringXY(0, 0, label, color); + putStringXY(0, 0, label, color); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TList.java b/src/jexer/TList.java index 46c9307..da60af1 100644 --- a/src/jexer/TList.java +++ b/src/jexer/TList.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -29,6 +29,7 @@ package jexer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import jexer.bits.CellAttributes; @@ -61,14 +62,20 @@ public class TList extends TScrollableWidget { private int maxLineWidth; /** - * The action to perform when the user selects an item (clicks or enter). + * The action to perform when the user selects an item (double-clicks or + * enter). */ - private TAction enterAction = null; + protected TAction enterAction = null; + + /** + * The action to perform when the user selects an item (single-click). + */ + protected TAction singleClickAction = null; /** * The action to perform when the user navigates with keyboard. */ - private TAction moveAction = null; + protected TAction moveAction = null; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- @@ -170,9 +177,11 @@ public class TList extends TScrollableWidget { } if ((mouse.getX() < getWidth() - 1) - && (mouse.getY() < getHeight() - 1)) { + && (mouse.getY() < getHeight() - 1) + ) { if (getVerticalValue() + mouse.getY() < strings.size()) { selectedString = getVerticalValue() + mouse.getY(); + dispatchSingleClick(); } return; } @@ -189,7 +198,8 @@ public class TList extends TScrollableWidget { @Override public void onMouseDoubleClick(final TMouseEvent mouse) { if ((mouse.getX() < getWidth() - 1) - && (mouse.getY() < getHeight() - 1)) { + && (mouse.getY() < getHeight() - 1) + ) { if (getVerticalValue() + mouse.getY() < strings.size()) { selectedString = getVerticalValue() + mouse.getY(); dispatchEnter(); @@ -329,7 +339,7 @@ public class TList extends TScrollableWidget { } /** - * Draw the files list. + * Draw the list. */ @Override public void draw() { @@ -355,8 +365,7 @@ public class TList extends TScrollableWidget { color = getTheme().getColor("tlist.inactive"); } String formatString = "%-" + Integer.toString(getWidth() - 1) + "s"; - getScreen().putStringXY(0, topY, String.format(formatString, line), - color); + putStringXY(0, topY, String.format(formatString, line), color); topY++; if (topY >= getHeight() - 1) { break; @@ -371,7 +380,7 @@ public class TList extends TScrollableWidget { // Pad the rest with blank lines for (int i = topY; i < getHeight() - 1; i++) { - getScreen().hLineXY(0, i, getWidth() - 1, ' ', color); + hLineXY(0, i, getWidth() - 1, ' ', color); } } @@ -418,6 +427,15 @@ public class TList extends TScrollableWidget { return strings.size() - 1; } + /** + * Get a copy of the list of strings to display. + * + * @return the list of strings + */ + public final List getList() { + return new ArrayList(strings); + } + /** * Set the new list of strings to display. * @@ -451,4 +469,15 @@ public class TList extends TScrollableWidget { } } + /** + * Perform single-click action. + */ + public void dispatchSingleClick() { + assert (selectedString >= 0); + assert (selectedString < strings.size()); + if (singleClickAction != null) { + singleClickAction.DO(); + } + } + } diff --git a/src/jexer/TMessageBox.java b/src/jexer/TMessageBox.java index 6104d9f..1eff8a7 100644 --- a/src/jexer/TMessageBox.java +++ b/src/jexer/TMessageBox.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -178,7 +178,7 @@ public class TMessageBox extends TWindow { protected TMessageBox(final TApplication application, final String title, final String caption, final Type type, final boolean yield) { - // Start as 50x50 at (1, 1). These will be changed later. + // Start as 100x100 at (1, 1). These will be changed later. super(application, title, 1, 1, 100, 100, CENTERED | MODAL); // Hang onto type so that we can provide more convenience in @@ -327,13 +327,14 @@ public class TMessageBox extends TWindow { break; default: - throw new IllegalArgumentException("Invalid message box type: " + type); + throw new IllegalArgumentException("Invalid message box type: " + + type); } - // Set the secondaryThread to run me - getApplication().enableSecondaryEventReceiver(this); - if (yield) { + // Set the secondaryThread to run me + getApplication().enableSecondaryEventReceiver(this); + // Yield to the secondary thread. When I come back from the // constructor response will already be set. getApplication().yield(); @@ -422,4 +423,40 @@ public class TMessageBox extends TWindow { return result; } + /** + * See if the user clicked YES. + * + * @return true if the user clicked YES + */ + public final boolean isYes() { + return (result == Result.YES); + } + + /** + * See if the user clicked NO. + * + * @return true if the user clicked NO + */ + public final boolean isNo() { + return (result == Result.NO); + } + + /** + * See if the user clicked OK. + * + * @return true if the user clicked OK + */ + public final boolean isOk() { + return (result == Result.OK); + } + + /** + * See if the user clicked CANCEL. + * + * @return true if the user clicked CANCEL + */ + public final boolean isCancel() { + return (result == Result.CANCEL); + } + } diff --git a/src/jexer/TPasswordField.java b/src/jexer/TPasswordField.java index 696598b..ea88dfa 100644 --- a/src/jexer/TPasswordField.java +++ b/src/jexer/TPasswordField.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -115,13 +115,11 @@ public class TPasswordField extends TField { end = text.length(); } - getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); + hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); if (showStars) { - getScreen().hLineXY(0, 0, getWidth() - 2, '*', - fieldColor); + hLineXY(0, 0, getWidth() - 2, '*', fieldColor); } else { - getScreen().putStringXY(0, 0, text.substring(windowStart, end), - fieldColor); + putStringXY(0, 0, text.substring(windowStart, end), fieldColor); } // Fix the cursor, it will be rendered by TApplication.drawAll(). diff --git a/src/jexer/TProgressBar.java b/src/jexer/TProgressBar.java index 3947a94..b144fd2 100644 --- a/src/jexer/TProgressBar.java +++ b/src/jexer/TProgressBar.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -98,26 +98,23 @@ public class TProgressBar extends TWidget { int progressInt = (int)(progress * 100); int progressUnit = 100 / (getWidth() - 2); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0xC3], incompleteColor); + putCharXY(0, 0, GraphicsChars.CP437[0xC3], incompleteColor); for (int i = 0; i < getWidth() - 2; i++) { float iProgress = (float)i / (getWidth() - 2); int iProgressInt = (int)(iProgress * 100); if (iProgressInt <= progressInt - progressUnit) { - getScreen().putCharXY(i + 1, 0, GraphicsChars.BOX, - completeColor); + putCharXY(i + 1, 0, GraphicsChars.BOX, completeColor); } else { - getScreen().putCharXY(i + 1, 0, GraphicsChars.SINGLE_BAR, - incompleteColor); + putCharXY(i + 1, 0, GraphicsChars.SINGLE_BAR, incompleteColor); } } if (value >= maxValue) { - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.BOX, - completeColor); + putCharXY(getWidth() - 2, 0, GraphicsChars.BOX, completeColor); } else { - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.SINGLE_BAR, + putCharXY(getWidth() - 2, 0, GraphicsChars.SINGLE_BAR, incompleteColor); } - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], + putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], incompleteColor); } diff --git a/src/jexer/TRadioButton.java b/src/jexer/TRadioButton.java index da07703..b4170ba 100644 --- a/src/jexer/TRadioButton.java +++ b/src/jexer/TRadioButton.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -158,15 +158,14 @@ public class TRadioButton extends TWidget { radioButtonColor = getTheme().getColor("tradiobutton.inactive"); } - getScreen().putCharXY(0, 0, '(', radioButtonColor); + putCharXY(0, 0, '(', radioButtonColor); if (selected) { - getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07], - radioButtonColor); + putCharXY(1, 0, GraphicsChars.CP437[0x07], radioButtonColor); } else { - getScreen().putCharXY(1, 0, ' ', radioButtonColor); + putCharXY(1, 0, ' ', radioButtonColor); } - getScreen().putCharXY(2, 0, ')', radioButtonColor); - getScreen().putStringXY(4, 0, label, radioButtonColor); + putCharXY(2, 0, ')', radioButtonColor); + putStringXY(4, 0, label, radioButtonColor); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TRadioGroup.java b/src/jexer/TRadioGroup.java index 6e6f39d..d57d864 100644 --- a/src/jexer/TRadioGroup.java +++ b/src/jexer/TRadioGroup.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -87,11 +87,11 @@ public class TRadioGroup extends TWidget { radioGroupColor = getTheme().getColor("tradiogroup.inactive"); } - getScreen().drawBox(0, 0, getWidth(), getHeight(), - radioGroupColor, radioGroupColor, 3, false); + drawBox(0, 0, getWidth(), getHeight(), radioGroupColor, radioGroupColor, + 3, false); - getScreen().hLineXY(1, 0, label.length() + 2, ' ', radioGroupColor); - getScreen().putStringXY(2, 0, label, radioGroupColor); + hLineXY(1, 0, label.length() + 2, ' ', radioGroupColor); + putStringXY(2, 0, label, radioGroupColor); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TScrollableWidget.java b/src/jexer/TScrollableWidget.java index 85be9ed..7d15b28 100644 --- a/src/jexer/TScrollableWidget.java +++ b/src/jexer/TScrollableWidget.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TScrollableWindow.java b/src/jexer/TScrollableWindow.java index c5f122b..6817f40 100644 --- a/src/jexer/TScrollableWindow.java +++ b/src/jexer/TScrollableWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TSpinner.java b/src/jexer/TSpinner.java index cdc5c0f..881e1a7 100644 --- a/src/jexer/TSpinner.java +++ b/src/jexer/TSpinner.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -162,10 +162,8 @@ public class TSpinner extends TWidget { spinnerColor = getTheme().getColor("tspinner.inactive"); } - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW, - spinnerColor); - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, - spinnerColor); + putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW, spinnerColor); + putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, spinnerColor); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TStatusBar.java b/src/jexer/TStatusBar.java index f3b8038..72a0ec6 100644 --- a/src/jexer/TStatusBar.java +++ b/src/jexer/TStatusBar.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -259,35 +259,34 @@ public class TStatusBar extends TWidget { int row = getScreen().getHeight() - 1; int width = getScreen().getWidth(); - getScreen().hLineXY(0, row, width, ' ', barColor); + hLineXY(0, row, width, ' ', barColor); int col = 0; for (TStatusBarKey key: keys) { String keyStr = key.key.toString(); if (key.selected) { - getScreen().putCharXY(col++, row, ' ', selectedColor); - getScreen().putStringXY(col, row, keyStr, selectedColor); + putCharXY(col++, row, ' ', selectedColor); + putStringXY(col, row, keyStr, selectedColor); col += keyStr.length(); - getScreen().putCharXY(col++, row, ' ', selectedColor); - getScreen().putStringXY(col, row, key.label, selectedColor); + putCharXY(col++, row, ' ', selectedColor); + putStringXY(col, row, key.label, selectedColor); col += key.label.length(); - getScreen().putCharXY(col++, row, ' ', selectedColor); + putCharXY(col++, row, ' ', selectedColor); } else { - getScreen().putCharXY(col++, row, ' ', barColor); - getScreen().putStringXY(col, row, keyStr, keyColor); + putCharXY(col++, row, ' ', barColor); + putStringXY(col, row, keyStr, keyColor); col += keyStr.length() + 1; - getScreen().putStringXY(col, row, key.label, barColor); + putStringXY(col, row, key.label, barColor); col += key.label.length(); - getScreen().putCharXY(col++, row, ' ', barColor); + putCharXY(col++, row, ' ', barColor); } } if (text.length() > 0) { if (keys.size() > 0) { - getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, - barColor); + putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, barColor); } - getScreen().putCharXY(col++, row, ' ', barColor); - getScreen().putStringXY(col, row, text, barColor); + putCharXY(col++, row, ' ', barColor); + putStringXY(col, row, text, barColor); } } diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 74f71ed..a624e6c 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -31,7 +31,7 @@ package jexer; import java.io.IOException; import java.lang.reflect.Field; import java.text.MessageFormat; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; @@ -77,10 +77,15 @@ public class TTerminalWindow extends TScrollableWindow /** * If true, we are using the ptypipe utility to support dynamic window * resizing. ptypipe is available at - * https://github.com/klamonte/ptypipe . + * https://gitlab.com/klamonte/ptypipe . */ private boolean ptypipe = false; + /** + * If true, close the window when the shell exits. + */ + private boolean closeOnExit = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -96,7 +101,25 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final String commandLine) { - this(application, x, y, RESIZABLE, commandLine.split("\\s")); + this(application, x, y, RESIZABLE, commandLine.split("\\s"), + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } + + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final String commandLine, final boolean closeOnExit) { + + this(application, x, y, RESIZABLE, commandLine.split("\\s"), + closeOnExit); } /** @@ -111,9 +134,30 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final int flags, final String [] command) { + this(application, x, y, flags, command, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } + + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags, final String [] command, + final boolean closeOnExit) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String [] fullCommand; // Spawn a shell and pass its I/O to the other constructor. @@ -161,9 +205,29 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final int flags) { + this(application, x, y, flags, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + + } + + /** + * Public constructor spawns a shell. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param closeOnExit if true, close the window when the shell exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags, final boolean closeOnExit) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String cmdShellWindows = "cmd.exe"; // You cannot run a login shell in a bare Process interactively, due @@ -224,7 +288,7 @@ public class TTerminalWindow extends TScrollableWindow + getVerticalValue(); assert (visibleBottom >= 0); - List preceedingBlankLines = new LinkedList(); + List preceedingBlankLines = new ArrayList(); int visibleTop = visibleBottom - visibleHeight; if (visibleTop < 0) { for (int i = visibleTop; i < 0; i++) { @@ -234,11 +298,11 @@ public class TTerminalWindow extends TScrollableWindow } assert (visibleTop >= 0); - List displayLines = new LinkedList(); + List displayLines = new ArrayList(); displayLines.addAll(scrollback); displayLines.addAll(display); - List visibleLines = new LinkedList(); + List visibleLines = new ArrayList(); visibleLines.addAll(preceedingBlankLines); visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom)); @@ -277,10 +341,10 @@ public class TTerminalWindow extends TScrollableWindow } } if (line.isDoubleWidth()) { - getScreen().putCharXY((i * 2) + 1, row, newCell); - getScreen().putCharXY((i * 2) + 2, row, ' ', newCell); + putCharXY((i * 2) + 1, row, newCell); + putCharXY((i * 2) + 2, row, ' ', newCell); } else { - getScreen().putCharXY(i + 1, row, newCell); + putCharXY(i + 1, row, newCell); } } row++; @@ -292,8 +356,7 @@ public class TTerminalWindow extends TScrollableWindow CellAttributes background = new CellAttributes(); // Fill in the blank lines on bottom for (int i = 0; i < visibleHeight; i++) { - getScreen().hLineXY(1, i + row, getWidth() - 2, ' ', - background); + hLineXY(1, i + row, getWidth() - 2, ' ', background); } } // synchronized (emulator) @@ -711,6 +774,9 @@ public class TTerminalWindow extends TScrollableWindow * Hook for subclasses to be notified of the shell termination. */ public void onShellExit() { + if (closeOnExit) { + close(); + } getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } diff --git a/src/jexer/TText.java b/src/jexer/TText.java index 44176a4..60f0e58 100644 --- a/src/jexer/TText.java +++ b/src/jexer/TText.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,6 +28,7 @@ */ package jexer; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -57,6 +58,12 @@ public class TText extends TScrollableWidget { * Available text justifications. */ public enum Justification { + + /** + * Not justified at all, use spacing as provided by the client. + */ + NONE, + /** * Left-justified text. */ @@ -183,8 +190,7 @@ public class TText extends TScrollableWidget { line = ""; } String formatString = "%-" + Integer.toString(getWidth() - 1) + "s"; - getScreen().putStringXY(0, topY, String.format(formatString, line), - color); + putStringXY(0, topY, String.format(formatString, line), color); topY++; if (topY >= (getHeight() - 1)) { @@ -194,7 +200,7 @@ public class TText extends TScrollableWidget { // Pad the rest with blank lines for (int i = topY; i < (getHeight() - 1); i++) { - getScreen().hLineXY(0, i, getWidth() - 1, ' ', color); + hLineXY(0, i, getWidth() - 1, ' ', color); } } @@ -260,6 +266,9 @@ public class TText extends TScrollableWidget { String[] paragraphs = text.split("\n\n"); for (String p : paragraphs) { switch (justification) { + case NONE: + lines.addAll(Arrays.asList(p.split("\n"))); + break; case LEFT: lines.addAll(jexer.bits.StringUtils.left(p, getWidth() - 1)); diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java index 3ec9dbd..8007153 100644 --- a/src/jexer/TTimer.java +++ b/src/jexer/TTimer.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/TVScroller.java b/src/jexer/TVScroller.java index 9b99cfc..444e058 100644 --- a/src/jexer/TVScroller.java +++ b/src/jexer/TVScroller.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -212,21 +212,16 @@ public class TVScroller extends TWidget { public void draw() { CellAttributes arrowColor = getTheme().getColor("tscroller.arrows"); CellAttributes barColor = getTheme().getColor("tscroller.bar"); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor); - getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F], - arrowColor); + putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor); + putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F], arrowColor); // Place the box if (bottomValue > topValue) { - getScreen().vLineXY(0, 1, getHeight() - 2, - GraphicsChars.CP437[0xB1], barColor); - getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX, - arrowColor); + vLineXY(0, 1, getHeight() - 2, GraphicsChars.CP437[0xB1], barColor); + putCharXY(0, boxPosition(), GraphicsChars.BOX, arrowColor); } else { - getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, - barColor); + vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, barColor); } - } // ------------------------------------------------------------------------ diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 6d5d147..4a4ba2c 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -33,6 +33,8 @@ import java.util.List; import java.util.ArrayList; import jexer.backend.Screen; +import jexer.bits.Cell; +import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; @@ -180,8 +182,9 @@ public abstract class TWidget implements Comparable { this.window = parent.window; children = new ArrayList(); - // Do not add TStatusBars, they are drawn by TApplication + // Do not add TStatusBars, they are drawn by TApplication. if (this instanceof TStatusBar) { + // NOP } else { parent.addChild(this); } @@ -205,8 +208,9 @@ public abstract class TWidget implements Comparable { this.window = parent.window; children = new ArrayList(); - // Do not add TStatusBars, they are drawn by TApplication + // Do not add TStatusBars, they are drawn by TApplication. if (this instanceof TStatusBar) { + // NOP } else { parent.addChild(this); } @@ -241,6 +245,17 @@ public abstract class TWidget implements Comparable { // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Subclasses should override this method to cleanup resources. This is + * called by TWindow.onClose(). + */ + protected void close() { + // Default: call close() on children. + for (TWidget w: getChildren()) { + w.close(); + } + } + /** * Check if a mouse press/release event coordinate is contained in this * widget. @@ -280,19 +295,20 @@ public abstract class TWidget implements Comparable { if ((children.size() == 0) || (this instanceof TTreeView) || (this instanceof TText) + || (this instanceof TComboBox) ) { // Defaults: // tab / shift-tab - switch to next/previous widget // left-arrow or up-arrow: same as shift-tab if ((keypress.equals(kbTab)) - || (keypress.equals(kbDown)) + || (keypress.equals(kbDown) && !(this instanceof TComboBox)) ) { parent.switchWidget(true); return; } else if ((keypress.equals(kbShiftTab)) || (keypress.equals(kbBackTab)) - || (keypress.equals(kbUp)) + || (keypress.equals(kbUp) && !(this instanceof TComboBox)) ) { parent.switchWidget(false); return; @@ -349,6 +365,17 @@ public abstract class TWidget implements Comparable { */ public void onMouseDown(final TMouseEvent mouse) { // Default: do nothing, pass to children instead + if (activeChild != null) { + if (activeChild.mouseWouldHit(mouse)) { + // Dispatch to the active child + + // Set x and y relative to the child's coordinates + mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX()); + mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY()); + activeChild.onMouseDown(mouse); + return; + } + } for (int i = children.size() - 1 ; i >= 0 ; i--) { TWidget widget = children.get(i); if (widget.mouseWouldHit(mouse)) { @@ -371,6 +398,17 @@ public abstract class TWidget implements Comparable { */ public void onMouseUp(final TMouseEvent mouse) { // Default: do nothing, pass to children instead + if (activeChild != null) { + if (activeChild.mouseWouldHit(mouse)) { + // Dispatch to the active child + + // Set x and y relative to the child's coordinates + mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX()); + mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY()); + activeChild.onMouseUp(mouse); + return; + } + } for (int i = children.size() - 1 ; i >= 0 ; i--) { TWidget widget = children.get(i); if (widget.mouseWouldHit(mouse)) { @@ -410,6 +448,17 @@ public abstract class TWidget implements Comparable { */ public void onMouseDoubleClick(final TMouseEvent mouse) { // Default: do nothing, pass to children instead + if (activeChild != null) { + if (activeChild.mouseWouldHit(mouse)) { + // Dispatch to the active child + + // Set x and y relative to the child's coordinates + mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX()); + mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY()); + activeChild.onMouseDoubleClick(mouse); + return; + } + } for (int i = children.size() - 1 ; i >= 0 ; i--) { TWidget widget = children.get(i); if (widget.mouseWouldHit(mouse)) { @@ -931,7 +980,7 @@ public abstract class TWidget implements Comparable { * * @return the ColorTheme */ - public final ColorTheme getTheme() { + protected final ColorTheme getTheme() { return window.getApplication().getTheme(); } @@ -944,9 +993,9 @@ public abstract class TWidget implements Comparable { } /** - * Called by parent to render to TWindow. + * Called by parent to render to TWindow. Note package private access. */ - public final void drawChildren() { + final void drawChildren() { // Set my clipping rectangle assert (window != null); assert (getScreen() != null); @@ -993,18 +1042,22 @@ public abstract class TWidget implements Comparable { // Draw me draw(); - // Continue down the chain + // Continue down the chain. Draw the active child last so that it + // is on top. for (TWidget widget: children) { - if (widget.isVisible()) { + if (widget.isVisible() && (widget != activeChild)) { widget.drawChildren(); } } + if (activeChild != null) { + activeChild.drawChildren(); + } } /** * Repaint the screen on the next update. */ - public final void doRepaint() { + protected final void doRepaint() { window.getApplication().doRepaint(); } @@ -1157,6 +1210,204 @@ public abstract class TWidget implements Comparable { return this; } + // ------------------------------------------------------------------------ + // Passthru for Screen functions ------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Get the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return attributes at (x, y) + */ + protected final CellAttributes getAttrXY(final int x, final int y) { + return getScreen().getAttrXY(x, y); + } + + /** + * Set the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void putAttrXY(final int x, final int y, + final CellAttributes attr) { + + getScreen().putAttrXY(x, y, attr); + } + + /** + * Set the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param attr attributes to use (bold, foreColor, backColor) + * @param clip if true, honor clipping/offset + */ + protected final void putAttrXY(final int x, final int y, + final CellAttributes attr, final boolean clip) { + + getScreen().putAttrXY(x, y, attr, clip); + } + + /** + * Fill the entire screen with one character with attributes. + * + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void putAll(final char ch, final CellAttributes attr) { + getScreen().putAll(ch, attr); + } + + /** + * Render one character with attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character + attributes to draw + */ + protected final void putCharXY(final int x, final int y, final Cell ch) { + getScreen().putCharXY(x, y, ch); + } + + /** + * Render one character with attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void putCharXY(final int x, final int y, final char ch, + final CellAttributes attr) { + + getScreen().putCharXY(x, y, ch, attr); + } + + /** + * Render one character without changing the underlying attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character to draw + */ + protected final void putCharXY(final int x, final int y, final char ch) { + getScreen().putCharXY(x, y, ch); + } + + /** + * Render a string. Does not wrap if the string exceeds the line. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param str string to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void putStringXY(final int x, final int y, final String str, + final CellAttributes attr) { + + getScreen().putStringXY(x, y, str, attr); + } + + /** + * Render a string without changing the underlying attribute. Does not + * wrap if the string exceeds the line. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param str string to draw + */ + protected final void putStringXY(final int x, final int y, final String str) { + getScreen().putStringXY(x, y, str); + } + + /** + * Draw a vertical line from (x, y) to (x, y + n). + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param n number of characters to draw + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void vLineXY(final int x, final int y, final int n, + final char ch, final CellAttributes attr) { + + getScreen().vLineXY(x, y, n, ch, attr); + } + + /** + * Draw a horizontal line from (x, y) to (x + n, y). + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param n number of characters to draw + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + protected final void hLineXY(final int x, final int y, final int n, + final char ch, final CellAttributes attr) { + + getScreen().hLineXY(x, y, n, ch, attr); + } + + /** + * Draw a box with a border and empty background. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + * @param border attributes to use for the border + * @param background attributes to use for the background + */ + protected final void drawBox(final int left, final int top, + final int right, final int bottom, + final CellAttributes border, final CellAttributes background) { + + getScreen().drawBox(left, top, right, bottom, border, background); + } + + /** + * Draw a box with a border and empty background. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + * @param border attributes to use for the border + * @param background attributes to use for the background + * @param borderType if 1, draw a single-line border; if 2, draw a + * double-line border; if 3, draw double-line top/bottom edges and + * single-line left/right edges (like Qmodem) + * @param shadow if true, draw a "shadow" on the box + */ + protected final void drawBox(final int left, final int top, + final int right, final int bottom, + final CellAttributes border, final CellAttributes background, + final int borderType, final boolean shadow) { + + getScreen().drawBox(left, top, right, bottom, border, background, + borderType, shadow); + } + + /** + * Draw a box shadow. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + */ + protected final void drawBoxShadow(final int left, final int top, + final int right, final int bottom) { + + getScreen().drawBoxShadow(left, top, right, bottom); + } + // ------------------------------------------------------------------------ // Other TWidget constructors --------------------------------------------- // ------------------------------------------------------------------------ @@ -1579,6 +1830,17 @@ public abstract class TWidget implements Comparable { return getApplication().fileOpenBox(path); } + /** + * Convenience function to spawn a file save box. + * + * @param path path of selected file + * @return the result of the new file open box + * @throws IOException if a java.io operation throws + */ + public final String fileSaveBox(final String path) throws IOException { + return getApplication().fileOpenBox(path, TFileOpenBox.Type.SAVE); + } + /** * Convenience function to spawn a file open box. * @@ -1592,6 +1854,41 @@ public abstract class TWidget implements Comparable { return getApplication().fileOpenBox(path, type); } + + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filter a string that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, final String filter) throws IOException { + + ArrayList filters = new ArrayList(); + filters.add(filter); + + return getApplication().fileOpenBox(path, type, filters); + } + + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filters a list of strings that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, + final List filters) throws IOException { + + return getApplication().fileOpenBox(path, type, filters); + } + /** * Convenience function to add a directory list to this container/window. * @@ -1616,7 +1913,8 @@ public abstract class TWidget implements Comparable { * @param y row relative to parent * @param width width of text area * @param height height of text area - * @param action action to perform when an item is selected + * @param action action to perform when an item is selected (enter or + * double-click) * @return the new directory list */ public final TDirectoryList addDirectoryList(final String path, final int x, @@ -1628,6 +1926,51 @@ public abstract class TWidget implements Comparable { /** * Convenience function to add a directory list to this container/window. * + * @param path directory path, must be a directory + * @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 action action to perform when an item is selected (enter or + * double-click) + * @param singleClickAction action to perform when an item is selected + * (single-click) + * @return the new directory list + */ + public final TDirectoryList addDirectoryList(final String path, final int x, + final int y, final int width, final int height, final TAction action, + final TAction singleClickAction) { + + return new TDirectoryList(this, path, x, y, width, height, action, + singleClickAction); + } + + /** + * Convenience function to add a directory list to this container/window. + * + * @param path directory path, must be a directory + * @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 action action to perform when an item is selected (enter or + * double-click) + * @param singleClickAction action to perform when an item is selected + * (single-click) + * @param filters a list of strings that files must match to be displayed + * @return the new directory list + */ + public final TDirectoryList addDirectoryList(final String path, final int x, + final int y, final int width, final int height, final TAction action, + final TAction singleClickAction, final List filters) { + + return new TDirectoryList(this, path, x, y, width, height, action, + singleClickAction, filters); + } + + /** + * Convenience function to add a list to this container/window. + * * @param strings list of strings to show * @param x column relative to parent * @param y row relative to parent @@ -1642,7 +1985,7 @@ public abstract class TWidget implements Comparable { } /** - * Convenience function to add a directory list to this container/window. + * Convenience function to add a list to this container/window. * * @param strings list of strings to show * @param x column relative to parent @@ -1660,7 +2003,7 @@ public abstract class TWidget implements Comparable { } /** - * Convenience function to add a directory list to this container/window. + * Convenience function to add a list to this container/window. * * @param strings list of strings to show * @param x column relative to parent diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 140a38a..a4a9c23 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -337,19 +337,32 @@ public class TWindow extends TWidget { 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(). */ - public void onClose() { - // Default: do nothing + 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(). */ - public void onFocus() { + protected void onFocus() { // Default: do nothing } @@ -357,21 +370,21 @@ public class TWindow extends TWidget { * Called by application.switchWindow() when another window gets the * focus. */ - public void onUnfocus() { + protected void onUnfocus() { // Default: do nothing } /** * Called by application.hideWindow(). */ - public void onHide() { + protected void onHide() { // Default: do nothing } /** * Called by application.showWindow(). */ - public void onShow() { + protected void onShow() { // Default: do nothing } @@ -385,6 +398,8 @@ public class TWindow extends TWidget { this.mouse = mouse; inKeyboardResize = false; + inWindowMove = false; + inWindowResize = false; if ((mouse.getAbsoluteY() == getY()) && mouse.isMouse1() @@ -854,8 +869,8 @@ public class TWindow extends TWidget { CellAttributes background = getBackground(); int borderType = getBorderType(); - getScreen().drawBox(0, 0, getWidth(), getHeight(), border, - background, borderType, true); + drawBox(0, 0, getWidth(), getHeight(), border, background, borderType, + true); // Draw the title int titleLeft = (getWidth() - title.length() - 2) / 2; @@ -1264,7 +1279,15 @@ public class TWindow extends TWidget { if (!isModal() && (inWindowMove || inWindowResize || inKeyboardResize) ) { - assert (isActive()); + 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()); @@ -1323,148 +1346,4 @@ public class TWindow extends TWidget { } } - // ------------------------------------------------------------------------ - // Passthru for Screen functions ------------------------------------------ - // ------------------------------------------------------------------------ - - /** - * Get the attributes at one location. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @return attributes at (x, y) - */ - public final CellAttributes getAttrXY(final int x, final int y) { - return getScreen().getAttrXY(x, y); - } - - /** - * Set the attributes at one location. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void putAttrXY(final int x, final int y, - final CellAttributes attr) { - - getScreen().putAttrXY(x, y, attr); - } - - /** - * Set the attributes at one location. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param attr attributes to use (bold, foreColor, backColor) - * @param clip if true, honor clipping/offset - */ - public final void putAttrXY(final int x, final int y, - final CellAttributes attr, final boolean clip) { - - getScreen().putAttrXY(x, y, attr, clip); - } - - /** - * Fill the entire screen with one character with attributes. - * - * @param ch character to draw - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void putAll(final char ch, final CellAttributes attr) { - getScreen().putAll(ch, attr); - } - - /** - * Render one character with attributes. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param ch character + attributes to draw - */ - public final void putCharXY(final int x, final int y, final Cell ch) { - getScreen().putCharXY(x, y, ch); - } - - /** - * Render one character with attributes. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param ch character to draw - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void putCharXY(final int x, final int y, final char ch, - final CellAttributes attr) { - - getScreen().putCharXY(x, y, ch, attr); - } - - /** - * Render one character without changing the underlying attributes. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param ch character to draw - */ - public final void putCharXY(final int x, final int y, final char ch) { - getScreen().putCharXY(x, y, ch); - } - - /** - * Render a string. Does not wrap if the string exceeds the line. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param str string to draw - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void putStringXY(final int x, final int y, final String str, - final CellAttributes attr) { - - getScreen().putStringXY(x, y, str, attr); - } - - /** - * Render a string without changing the underlying attribute. Does not - * wrap if the string exceeds the line. - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param str string to draw - */ - public final void putStringXY(final int x, final int y, final String str) { - getScreen().putStringXY(x, y, str); - } - - /** - * Draw a vertical line from (x, y) to (x, y + n). - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param n number of characters to draw - * @param ch character to draw - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void vLineXY(final int x, final int y, final int n, - final char ch, final CellAttributes attr) { - - getScreen().vLineXY(x, y, n, ch, attr); - } - - /** - * Draw a horizontal line from (x, y) to (x + n, y). - * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param n number of characters to draw - * @param ch character to draw - * @param attr attributes to use (bold, foreColor, backColor) - */ - public final void hLineXY(final int x, final int y, final int n, - final char ch, final CellAttributes attr) { - - getScreen().hLineXY(x, y, n, ch, attr); - } - } diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java index 20efa7e..8bd1816 100644 --- a/src/jexer/backend/Backend.java +++ b/src/jexer/backend/Backend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index 7aad3a5..0614e17 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 84f6528..1dc3957 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,6 +28,7 @@ */ package jexer.backend; +import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -40,9 +41,13 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.LinkedList; +import java.util.Map; +import jexer.TImage; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Color; @@ -76,6 +81,12 @@ public class ECMA48Terminal extends LogicalScreen MOUSE_SGR, } + /** + * Number of colors in the sixel palette. Xterm 335 defines the max as + * 1024. + */ + private static final int MAX_COLOR_REGISTERS = 1024; + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -162,6 +173,31 @@ public class ECMA48Terminal extends LogicalScreen */ private TResizeEvent windowResize = null; + /** + * Window width in pixels. Used for sixel support. + */ + private int widthPixels = 640; + + /** + * Window height in pixels. Used for sixel support. + */ + private int heightPixels = 400; + + /** + * If true, emit image data via sixel. + */ + private boolean sixel = true; + + /** + * The sixel palette handler. + */ + private SixelPalette palette = null; + + /** + * The sixel post-rendered string cache. + */ + private SixelCache sixelCache = null; + /** * If true, then we changed System.in and need to change it back. */ @@ -194,6 +230,753 @@ public class ECMA48Terminal extends LogicalScreen */ private Object listener; + /** + * SixelPalette is used to manage the conversion of images between 24-bit + * RGB color and a palette of MAX_COLOR_REGISTERS colors. + */ + private class SixelPalette { + + /** + * Color palette for sixel output, sorted low to high. + */ + private List rgbColors = new ArrayList(); + + /** + * Map of color palette index for sixel output, from the order it was + * generated by makePalette() to rgbColors. + */ + private int [] rgbSortedIndex = new int[MAX_COLOR_REGISTERS]; + + /** + * The color palette, organized by hue, saturation, and luminance. + * This is used for a fast color match. + */ + private ArrayList>> hslColors; + + /** + * Number of bits for hue. + */ + private int hueBits = -1; + + /** + * Number of bits for saturation. + */ + private int satBits = -1; + + /** + * Number of bits for luminance. + */ + private int lumBits = -1; + + /** + * Step size for hue bins. + */ + private int hueStep = -1; + + /** + * Step size for saturation bins. + */ + private int satStep = -1; + + /** + * Cached RGB to HSL result. + */ + private int hsl[] = new int[3]; + + /** + * ColorIdx records a RGB color and its palette index. + */ + private class ColorIdx { + /** + * The 24-bit RGB color. + */ + public int color; + + /** + * The palette index for this color. + */ + public int index; + + /** + * Public constructor. + * + * @param color the 24-bit RGB color + * @param index the palette index for this color + */ + public ColorIdx(final int color, final int index) { + this.color = color; + this.index = index; + } + } + + /** + * Public constructor. + */ + public SixelPalette() { + makePalette(); + } + + /** + * Find the nearest match for a color in the palette. + * + * @param color the RGB color + * @return the index in rgbColors that is closest to color + */ + public int matchColor(final int color) { + + assert (color >= 0); + + /* + * matchColor() is a critical performance bottleneck. To make it + * decent, we do the following: + * + * 1. Find the nearest two hues that bracket this color. + * + * 2. Find the nearest two saturations that bracket this color. + * + * 3. Iterate within these four bands of luminance values, + * returning the closest color by Euclidean distance. + * + * This strategy reduces the search space by about 97%. + */ + int red = (color >>> 16) & 0xFF; + int green = (color >>> 8) & 0xFF; + int blue = color & 0xFF; + + rgbToHsl(red, green, blue, hsl); + int hue = hsl[0]; + int sat = hsl[1]; + int lum = hsl[2]; + // System.err.printf("%d %d %d\n", hue, sat, lum); + + double diff = Double.MAX_VALUE; + int idx = -1; + + int hue1 = hue / (360/hueStep); + int hue2 = hue1 + 1; + if (hue1 >= hslColors.size() - 1) { + // Bracket pure red from above. + hue1 = hslColors.size() - 1; + hue2 = 0; + } else if (hue1 == 0) { + // Bracket pure red from below. + hue2 = hslColors.size() - 1; + } + + for (int hI = hue1; hI != -1;) { + ArrayList> sats = hslColors.get(hI); + if (hI == hue1) { + hI = hue2; + } else if (hI == hue2) { + hI = -1; + } + + int sMin = (sat / satStep) - 1; + int sMax = sMin + 1; + if (sMin < 0) { + sMin = 0; + sMax = 1; + } else if (sMin == sats.size() - 1) { + sMax = sMin; + sMin--; + } + assert (sMin >= 0); + assert (sMax - sMin == 1); + + // int sMin = 0; + // int sMax = sats.size() - 1; + + for (int sI = sMin; sI <= sMax; sI++) { + ArrayList lums = sats.get(sI); + + // True 3D colorspace match for the remaining values + for (ColorIdx c: lums) { + int rgbColor = c.color; + double newDiff = 0; + int red2 = (rgbColor >>> 16) & 0xFF; + int green2 = (rgbColor >>> 8) & 0xFF; + int blue2 = rgbColor & 0xFF; + newDiff += Math.pow(red2 - red, 2); + newDiff += Math.pow(green2 - green, 2); + newDiff += Math.pow(blue2 - blue, 2); + if (newDiff < diff) { + idx = rgbSortedIndex[c.index]; + diff = newDiff; + } + } + } + } + + if (((red * red) + (green * green) + (blue * blue)) < diff) { + // Black is a closer match. + idx = 0; + } else if ((((255 - red) * (255 - red)) + + ((255 - green) * (255 - green)) + + ((255 - blue) * (255 - blue))) < diff) { + + // White is a closer match. + idx = MAX_COLOR_REGISTERS - 1; + } + assert (idx != -1); + return idx; + } + + /** + * Clamp an int value to [0, 255]. + * + * @param x the int value + * @return an int between 0 and 255. + */ + private int clamp(final int x) { + if (x < 0) { + return 0; + } + if (x > 255) { + return 255; + } + return x; + } + + /** + * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered + * image cells will contain indexes into the palette. + * + * @param image the image to dither + * @return the dithered image. Every pixel is an index into the + * palette. + */ + public BufferedImage ditherImage(final BufferedImage image) { + + BufferedImage ditheredImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + int [] rgbArray = image.getRGB(0, 0, image.getWidth(), + image.getHeight(), null, 0, image.getWidth()); + ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(), + rgbArray, 0, image.getWidth()); + + for (int imageY = 0; imageY < image.getHeight(); imageY++) { + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + int oldPixel = ditheredImage.getRGB(imageX, + imageY) & 0xFFFFFF; + int colorIdx = matchColor(oldPixel); + assert (colorIdx >= 0); + assert (colorIdx < MAX_COLOR_REGISTERS); + int newPixel = rgbColors.get(colorIdx); + ditheredImage.setRGB(imageX, imageY, colorIdx); + + int oldRed = (oldPixel >>> 16) & 0xFF; + int oldGreen = (oldPixel >>> 8) & 0xFF; + int oldBlue = oldPixel & 0xFF; + + int newRed = (newPixel >>> 16) & 0xFF; + int newGreen = (newPixel >>> 8) & 0xFF; + int newBlue = newPixel & 0xFF; + + int redError = (oldRed - newRed) / 16; + int greenError = (oldGreen - newGreen) / 16; + int blueError = (oldBlue - newBlue) / 16; + + int red, green, blue; + if (imageX < image.getWidth() - 1) { + int pXpY = ditheredImage.getRGB(imageX + 1, imageY); + red = (int) ((pXpY >>> 16) & 0xFF) + (7 * redError); + green = (int) ((pXpY >>> 8) & 0xFF) + (7 * greenError); + blue = (int) ( pXpY & 0xFF) + (7 * blueError); + red = clamp(red); + green = clamp(green); + blue = clamp(blue); + pXpY = ((red & 0xFF) << 16); + pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF); + ditheredImage.setRGB(imageX + 1, imageY, pXpY); + + if (imageY < image.getHeight() - 1) { + int pXpYp = ditheredImage.getRGB(imageX + 1, + imageY + 1); + red = (int) ((pXpYp >>> 16) & 0xFF) + redError; + green = (int) ((pXpYp >>> 8) & 0xFF) + greenError; + blue = (int) ( pXpYp & 0xFF) + blueError; + red = clamp(red); + green = clamp(green); + blue = clamp(blue); + pXpYp = ((red & 0xFF) << 16); + pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF); + ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp); + } + } else if (imageY < image.getHeight() - 1) { + int pXmYp = ditheredImage.getRGB(imageX - 1, + imageY + 1); + int pXYp = ditheredImage.getRGB(imageX, + imageY + 1); + + red = (int) ((pXmYp >>> 16) & 0xFF) + (3 * redError); + green = (int) ((pXmYp >>> 8) & 0xFF) + (3 * greenError); + blue = (int) ( pXmYp & 0xFF) + (3 * blueError); + red = clamp(red); + green = clamp(green); + blue = clamp(blue); + pXmYp = ((red & 0xFF) << 16); + pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF); + ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp); + + red = (int) ((pXYp >>> 16) & 0xFF) + (5 * redError); + green = (int) ((pXYp >>> 8) & 0xFF) + (5 * greenError); + blue = (int) ( pXYp & 0xFF) + (5 * blueError); + red = clamp(red); + green = clamp(green); + blue = clamp(blue); + pXYp = ((red & 0xFF) << 16); + pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF); + ditheredImage.setRGB(imageX, imageY + 1, pXYp); + } + } // for (int imageY = 0; imageY < image.getHeight(); imageY++) + } // for (int imageX = 0; imageX < image.getWidth(); imageX++) + + return ditheredImage; + } + + /** + * Convert an RGB color to HSL. + * + * @param red red color, between 0 and 255 + * @param green green color, between 0 and 255 + * @param blue blue color, between 0 and 255 + * @param hsl the hsl color as [hue, saturation, luminance] + */ + private void rgbToHsl(final int red, final int green, + final int blue, final int [] hsl) { + + assert ((red >= 0) && (red <= 255)); + assert ((green >= 0) && (green <= 255)); + assert ((blue >= 0) && (blue <= 255)); + + double R = red / 255.0; + double G = green / 255.0; + double B = blue / 255.0; + boolean Rmax = false; + boolean Gmax = false; + boolean Bmax = false; + double min = (R < G ? R : G); + min = (min < B ? min : B); + double max = 0; + if ((R >= G) && (R >= B)) { + max = R; + Rmax = true; + } else if ((G >= R) && (G >= B)) { + max = G; + Gmax = true; + } else if ((B >= G) && (B >= R)) { + max = B; + Bmax = true; + } + + double L = (min + max) / 2.0; + double H = 0.0; + double S = 0.0; + if (min != max) { + if (L < 0.5) { + S = (max - min) / (max + min); + } else { + S = (max - min) / (2.0 - max - min); + } + } + if (Rmax) { + assert (Gmax == false); + assert (Bmax == false); + H = (G - B) / (max - min); + } else if (Gmax) { + assert (Rmax == false); + assert (Bmax == false); + H = 2.0 + (B - R) / (max - min); + } else if (Bmax) { + assert (Rmax == false); + assert (Gmax == false); + H = 4.0 + (R - G) / (max - min); + } + if (H < 0.0) { + H += 6.0; + } + hsl[0] = (int) (H * 60.0); + hsl[1] = (int) (S * 100.0); + hsl[2] = (int) (L * 100.0); + + assert ((hsl[0] >= 0) && (hsl[0] <= 360)); + assert ((hsl[1] >= 0) && (hsl[1] <= 100)); + assert ((hsl[2] >= 0) && (hsl[2] <= 100)); + } + + /** + * Convert a HSL color to RGB. + * + * @param hue hue, between 0 and 359 + * @param sat saturation, between 0 and 100 + * @param lum luminance, between 0 and 100 + * @return the rgb color as 0x00RRGGBB + */ + private int hslToRgb(final int hue, final int sat, final int lum) { + assert ((hue >= 0) && (hue <= 360)); + assert ((sat >= 0) && (sat <= 100)); + assert ((lum >= 0) && (lum <= 100)); + + double S = sat / 100.0; + double L = lum / 100.0; + double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S; + double Hp = hue / 60.0; + double X = C * (1.0 - Math.abs((Hp % 2) - 1.0)); + double Rp = 0.0; + double Gp = 0.0; + double Bp = 0.0; + if (Hp <= 1.0) { + Rp = C; + Gp = X; + } else if (Hp <= 2.0) { + Rp = X; + Gp = C; + } else if (Hp <= 3.0) { + Gp = C; + Bp = X; + } else if (Hp <= 4.0) { + Gp = X; + Bp = C; + } else if (Hp <= 5.0) { + Rp = X; + Bp = C; + } else if (Hp <= 6.0) { + Rp = C; + Bp = X; + } + double m = L - (C / 2.0); + int red = ((int) ((Rp + m) * 255.0)) << 16; + int green = ((int) ((Gp + m) * 255.0)) << 8; + int blue = (int) ((Bp + m) * 255.0); + + return (red | green | blue); + } + + /** + * Create the sixel palette. + */ + private void makePalette() { + // Generate the sixel palette. Because we have no idea at this + // layer which image(s) will be shown, we have to use a common + // palette with MAX_COLOR_REGISTERS colors for everything, and + // map the BufferedImage colors to their nearest neighbor in RGB + // space. + + // We build a palette using the Hue-Saturation-Luminence model, + // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for + // Luminance. We convert these colors to 24-bit RGB, sort them + // ascending, and steal the first index for pure black and the + // last for pure white. The 8-bit final palette favors bright + // colors, somewhere between pastel and classic television + // technicolor. 9- and 10-bit palettes are more uniform. + + // Default at 256 colors. + hueBits = 5; + satBits = 2; + lumBits = 1; + + assert (MAX_COLOR_REGISTERS >= 256); + assert ((MAX_COLOR_REGISTERS == 256) + || (MAX_COLOR_REGISTERS == 512) + || (MAX_COLOR_REGISTERS == 1024) + || (MAX_COLOR_REGISTERS == 2048)); + + switch (MAX_COLOR_REGISTERS) { + case 512: + hueBits = 5; + satBits = 2; + lumBits = 2; + break; + case 1024: + hueBits = 5; + satBits = 2; + lumBits = 3; + break; + case 2048: + hueBits = 5; + satBits = 3; + lumBits = 3; + break; + } + hueStep = (int) (Math.pow(2, hueBits)); + satStep = (int) (100 / Math.pow(2, satBits)); + // 1 bit for luminance: 40 and 70. + int lumBegin = 40; + int lumStep = 30; + switch (lumBits) { + case 2: + // 2 bits: 20, 40, 60, 80 + lumBegin = 20; + lumStep = 20; + break; + case 3: + // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92 + lumBegin = 8; + lumStep = 12; + break; + } + + // System.err.printf("\n"); + // Hue is evenly spaced around the wheel. + hslColors = new ArrayList>>(); + + final boolean DEBUG = false; + ArrayList rawRgbList = new ArrayList(); + + for (int hue = 0; hue < (360 - (360 % hueStep)); + hue += (360/hueStep)) { + + ArrayList> satList = null; + satList = new ArrayList>(); + hslColors.add(satList); + + // Saturation is linearly spaced between pastel and pure. + for (int sat = satStep; sat <= 100; sat += satStep) { + + ArrayList lumList = new ArrayList(); + satList.add(lumList); + + // Luminance brackets the pure color, but leaning toward + // lighter. + for (int lum = lumBegin; lum < 100; lum += lumStep) { + /* + System.err.printf("=\n"); + */ + int rgbColor = hslToRgb(hue, sat, lum); + rgbColors.add(rgbColor); + ColorIdx colorIdx = new ColorIdx(rgbColor, + rgbColors.size() - 1); + lumList.add(colorIdx); + + rawRgbList.add(rgbColor); + if (DEBUG) { + int red = (rgbColor >>> 16) & 0xFF; + int green = (rgbColor >>> 8) & 0xFF; + int blue = rgbColor & 0xFF; + int [] backToHsl = new int[3]; + rgbToHsl(red, green, blue, backToHsl); + System.err.printf("%d [%d] %d [%d] %d [%d]\n", + hue, backToHsl[0], sat, backToHsl[1], + lum, backToHsl[2]); + } + } + } + } + // System.err.printf("\n\n"); + + assert (rgbColors.size() == MAX_COLOR_REGISTERS); + + /* + * We need to sort rgbColors, so that toSixel() can know where + * BLACK and WHITE are in it. But we also need to be able to + * find the sorted values using the old unsorted indexes. So we + * will sort it, put all the indexes into a HashMap, and then + * build rgbSortedIndex[]. + */ + Collections.sort(rgbColors); + HashMap rgbColorIndices = null; + rgbColorIndices = new HashMap(); + for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + rgbColorIndices.put(rgbColors.get(i), i); + } + for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + int rawColor = rawRgbList.get(i); + rgbSortedIndex[i] = rgbColorIndices.get(rawColor); + } + if (DEBUG) { + for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + assert (rawRgbList != null); + int idx = rgbSortedIndex[i]; + int rgbColor = rgbColors.get(idx); + if ((idx != 0) && (idx != MAX_COLOR_REGISTERS - 1)) { + /* + System.err.printf("%d %06x --> %d %06x\n", + i, rawRgbList.get(i), idx, rgbColors.get(idx)); + */ + assert (rgbColor == rawRgbList.get(i)); + } + } + } + + // Set the dimmest color as true black, and the brightest as true + // white. + rgbColors.set(0, 0); + rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF); + + /* + System.err.printf("\n"); + for (Integer rgb: rgbColors) { + System.err.printf("=\n"); + } + System.err.printf("\n\n"); + */ + + } + + /** + * Emit the sixel palette. + * + * @param sb the StringBuilder to append to + * @param used array of booleans set to true for each color actually + * used in this cell, or null to emit the entire palette + * @return the string to emit to an ANSI / ECMA-style terminal + */ + public String emitPalette(final StringBuilder sb, + final boolean [] used) { + + for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + if (((used != null) && (used[i] == true)) || (used == null)) { + int rgbColor = rgbColors.get(i); + sb.append(String.format("#%d;2;%d;%d;%d", i, + ((rgbColor >>> 16) & 0xFF) * 100 / 255, + ((rgbColor >>> 8) & 0xFF) * 100 / 255, + ( rgbColor & 0xFF) * 100 / 255)); + } + } + return sb.toString(); + } + } + + /** + * SixelCache is a least-recently-used cache that hangs on to the + * post-rendered sixel string for a particular set of cells. + */ + private class SixelCache { + + /** + * Maximum size of the cache. + */ + private int maxSize = 100; + + /** + * The entries stored in the cache. + */ + private HashMap cache = null; + + /** + * CacheEntry is one entry in the cache. + */ + private class CacheEntry { + /** + * The cache key. + */ + public String key; + + /** + * The cache data. + */ + public String data; + + /** + * The last time this entry was used. + */ + public long millis = 0; + + /** + * Public constructor. + * + * @param key the cache entry key + * @param data the cache entry data + */ + public CacheEntry(final String key, final String data) { + this.key = key; + this.data = data; + this.millis = System.currentTimeMillis(); + } + } + + /** + * Public constructor. + * + * @param maxSize the maximum size of the cache + */ + public SixelCache(final int maxSize) { + this.maxSize = maxSize; + cache = new HashMap(); + } + + /** + * Make a unique key for a list of cells. + * + * @param cells the cells + * @return the key + */ + private String makeKey(final ArrayList cells) { + StringBuilder sb = new StringBuilder(); + for (Cell cell: cells) { + sb.append(cell.hashCode()); + } + return sb.toString(); + } + + /** + * Get an entry from the cache. + * + * @param cells the list of cells that are the cache key + * @return the sixel string representing these cells, or null if this + * list of cells is not in the cache + */ + public String get(final ArrayList cells) { + CacheEntry entry = cache.get(makeKey(cells)); + if (entry == null) { + return null; + } + entry.millis = System.currentTimeMillis(); + return entry.data; + } + + /** + * Put an entry into the cache. + * + * @param cells the list of cells that are the cache key + * @param data the sixel string representing these cells + */ + public void put(final ArrayList cells, final String data) { + String key = makeKey(cells); + + // System.err.println("put() " + key + " size " + cache.size()); + + assert (!cache.containsKey(key)); + + assert (cache.size() <= maxSize); + if (cache.size() == maxSize) { + // Cache is at limit, evict oldest entry. + long oldestTime = Long.MAX_VALUE; + String keyToRemove = null; + for (CacheEntry entry: cache.values()) { + if ((entry.millis < oldestTime) || (keyToRemove == null)) { + keyToRemove = entry.key; + oldestTime = entry.millis; + } + } + /* + System.err.println("put() remove key = " + keyToRemove + + " size " + cache.size()); + */ + assert (keyToRemove != null); + cache.remove(keyToRemove); + /* + System.err.println("put() removed, size " + cache.size()); + */ + } + assert (cache.size() <= maxSize); + CacheEntry entry = new CacheEntry(key, data); + assert (key.equals(entry.key)); + cache.put(key, entry); + /* + System.err.println("put() added key " + key + " " + + " size " + cache.size()); + */ + } + + } + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -286,6 +1069,9 @@ public class ECMA48Terminal extends LogicalScreen "UTF-8")); } + // Request xterm report window dimensions in pixels + this.output.printf("%s", xtermReportWindowPixelDimensions()); + // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); @@ -299,7 +1085,7 @@ public class ECMA48Terminal extends LogicalScreen windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - // Permit RGB colors only if externally requested + // Permit RGB colors only if externally requested. if (System.getProperty("jexer.ECMA48.rgbColor") != null) { if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) { doRgbColor = true; @@ -308,6 +1094,15 @@ public class ECMA48Terminal extends LogicalScreen } } + // Pull the system properties for sixel output. + if (System.getProperty("jexer.ECMA48.sixel") != null) { + if (System.getProperty("jexer.ECMA48.sixel").equals("true")) { + sixel = true; + } else { + sixel = false; + } + } + // Spin up the input reader eventQueue = new LinkedList(); readerThread = new Thread(this); @@ -376,6 +1171,9 @@ public class ECMA48Terminal extends LogicalScreen this.output = writer; + // Request xterm report window dimensions in pixels + this.output.printf("%s", xtermReportWindowPixelDimensions()); + // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); @@ -398,6 +1196,15 @@ public class ECMA48Terminal extends LogicalScreen } } + // Pull the system properties for sixel output. + if (System.getProperty("jexer.ECMA48.sixel") != null) { + if (System.getProperty("jexer.ECMA48.sixel").equals("true")) { + sixel = true; + } else { + sixel = false; + } + } + // Spin up the input reader eventQueue = new LinkedList(); readerThread = new Thread(this); @@ -445,19 +1252,21 @@ public class ECMA48Terminal extends LogicalScreen */ @Override public void flushPhysical() { - String result = flushString(); + StringBuilder sb = new StringBuilder(); if ((cursorVisible) && (cursorY >= 0) && (cursorX >= 0) && (cursorY <= height - 1) && (cursorX <= width - 1) ) { - result += cursor(true); - result += gotoXY(cursorX, cursorY); + flushString(sb); + sb.append(cursor(true)); + sb.append(gotoXY(cursorX, cursorY)); } else { - result += cursor(false); + sb.append(cursor(false)); + flushString(sb); } - output.write(result); + output.write(sb.toString()); flush(); } @@ -504,7 +1313,9 @@ public class ECMA48Terminal extends LogicalScreen try { readerThread.join(); } catch (InterruptedException e) { - e.printStackTrace(); + if (debugToStderr) { + e.printStackTrace(); + } } // Disable mouse reporting and show cursor. Defensive null check @@ -521,17 +1332,17 @@ public class ECMA48Terminal extends LogicalScreen } else { // Shut down the streams, this should wake up the reader thread // and make it exit. - try { - if (input != null) { + if (input != null) { + try { input.close(); - input = null; - } - if (output != null) { - output.close(); - output = null; + } catch (IOException e) { + // SQUASH } - } catch (IOException e) { - e.printStackTrace(); + input = null; + } + if (output != null) { + output.close(); + output = null; } } } @@ -641,6 +1452,24 @@ public class ECMA48Terminal extends LogicalScreen // ECMA48Terminal --------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Get the width of a character cell in pixels. + * + * @return the width in pixels of a character cell + */ + public int getTextWidth() { + return (widthPixels / sessionInfo.getWindowWidth()); + } + + /** + * Get the height of a character cell in pixels. + * + * @return the height in pixels of a character cell + */ + public int getTextHeight() { + return (heightPixels / sessionInfo.getWindowHeight()); + } + /** * Getter for sessionInfo. * @@ -713,7 +1542,9 @@ public class ECMA48Terminal extends LogicalScreen process.waitFor(); break; } catch (InterruptedException e) { - e.printStackTrace(); + if (debugToStderr) { + e.printStackTrace(); + } } } int rc = process.exitValue(); @@ -756,6 +1587,8 @@ public class ECMA48Terminal extends LogicalScreen // DEBUG // reallyCleared = true; + boolean hasImage = false; + for (int x = 0; x < width; x++) { Cell lCell = logical[x][y]; Cell pCell = physical[x][y]; @@ -798,6 +1631,25 @@ public class ECMA48Terminal extends LogicalScreen return; } + // Image cell: bypass the rest of the loop, it is not + // rendered here. + if (lCell.isImage()) { + hasImage = true; + + // Save the last rendered cell + lastX = x; + + // Physical is always updated + physical[x][y].setTo(lCell); + continue; + } + + assert (!lCell.isImage()); + if (hasImage) { + hasImage = false; + sb.append(gotoXY(x, y)); + } + // Now emit only the modified attributes if ((lCell.getForeColor() != lastAttr.getForeColor()) && (lCell.getBackColor() != lastAttr.getBackColor()) @@ -970,18 +1822,70 @@ public class ECMA48Terminal extends LogicalScreen * Render the screen to a string that can be emitted to something that * knows how to process ECMA-48/ANSI X3.64 escape sequences. * + * @param sb StringBuilder to write escape sequences to * @return escape sequences string that provides the updates to the * physical screen */ - private String flushString() { + private String flushString(final StringBuilder sb) { CellAttributes attr = null; - StringBuilder sb = new StringBuilder(); if (reallyCleared) { attr = new CellAttributes(); sb.append(clearAll()); } + /* + * For sixel support, draw all of the sixel output first, and then + * draw everything else afterwards. This works OK, but performance + * is still a drag on larger pictures. + */ + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // If physical had non-image data that is now image data, the + // entire row must be redrawn. + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + if (lCell.isImage() && !pCell.isImage()) { + unsetImageRow(y); + break; + } + } + } + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + + if (!lCell.isImage()) { + continue; + } + + int left = x; + int right = x; + while ((right < width) + && (logical[right][y].isImage()) + && (!logical[right][y].equals(physical[right][y]) + || reallyCleared) + ) { + right++; + } + ArrayList cellsToDraw = new ArrayList(); + for (int i = 0; i < (right - x); i++) { + assert (logical[x + i][y].isImage()); + cellsToDraw.add(logical[x + i][y]); + + // Physical is always updated. + physical[x + i][y].setTo(lCell); + } + if (cellsToDraw.size() > 0) { + sb.append(toSixel(x, y, cellsToDraw)); + } + + x = right; + } + } + + // Draw the text part now. for (int y = 0; y < height; y++) { flushLine(y, sb, attr); } @@ -1335,6 +2239,10 @@ public class ECMA48Terminal extends LogicalScreen newWidth + " x " + newHeight); } + // Request xterm report window dimensions in pixels again. + this.output.printf("%s", xtermReportWindowPixelDimensions()); + this.output.flush(); + TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, newWidth, newHeight); windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, @@ -1697,6 +2605,31 @@ public class ECMA48Terminal extends LogicalScreen events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift)); resetParser(); return; + case 't': + // windowOps + if ((params.size() > 2) && (params.get(0).equals("4"))) { + if (debugToStderr) { + System.err.printf("windowOp pixels: " + + "height %s width %s\n", + params.get(1), params.get(2)); + } + try { + widthPixels = Integer.parseInt(params.get(2)); + heightPixels = Integer.parseInt(params.get(1)); + } catch (NumberFormatException e) { + if (debugToStderr) { + e.printStackTrace(); + } + } + if (widthPixels <= 0) { + widthPixels = 640; + } + if (heightPixels <= 0) { + heightPixels = 400; + } + } + resetParser(); + return; default: break; } @@ -1723,6 +2656,15 @@ public class ECMA48Terminal extends LogicalScreen return; } + /** + * Request (u)xterm to report the current window size dimensions. + * + * @return the string to emit to xterm + */ + private String xtermReportWindowPixelDimensions() { + return "\033[14t"; + } + /** * Tell (u)xterm that we want alt- keystrokes to send escape + character * rather than set the 8th bit. Anyone who wants UTF8 should want this @@ -1748,6 +2690,264 @@ public class ECMA48Terminal extends LogicalScreen return "\033]2;" + title + "\007"; } + // ------------------------------------------------------------------------ + // Sixel output support --------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Start a sixel string for display one row's worth of bitmap data. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return the string to emit to an ANSI / ECMA-style terminal + */ + private String startSixel(final int x, final int y) { + StringBuilder sb = new StringBuilder(); + + assert (sixel == true); + + // Place the cursor + sb.append(gotoXY(x, y)); + + // DCS + sb.append("\033Pq"); + + if (palette == null) { + palette = new SixelPalette(); + } + + return sb.toString(); + } + + /** + * End a sixel string for display one row's worth of bitmap data. + * + * @return the string to emit to an ANSI / ECMA-style terminal + */ + private String endSixel() { + assert (sixel == true); + + // ST + return ("\033\\"); + } + + /** + * Create a sixel string representing a row of several cells containing + * bitmap data. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param cells the cells containing the bitmap data + * @return the string to emit to an ANSI / ECMA-style terminal + */ + private String toSixel(final int x, final int y, + final ArrayList cells) { + + StringBuilder sb = new StringBuilder(); + + assert (sixel == true); + assert (cells != null); + assert (cells.size() > 0); + assert (cells.get(0).getImage() != null); + + if (sixelCache == null) { + sixelCache = new SixelCache(height * 10); + } + + // Save and get rows to/from the cache that do NOT have inverted + // cells. + boolean saveInCache = true; + for (Cell cell: cells) { + if (cell.isInvertedImage()) { + saveInCache = false; + } + } + if (saveInCache) { + String cachedResult = sixelCache.get(cells); + if (cachedResult != null) { + // System.err.println("CACHE HIT"); + sb.append(startSixel(x, y)); + sb.append(cachedResult); + sb.append(endSixel()); + return sb.toString(); + } + // System.err.println("CACHE MISS"); + } + + int imageWidth = cells.get(0).getImage().getWidth(); + int imageHeight = cells.get(0).getImage().getHeight(); + + // cells.get(x).getImage() has a dithered bitmap containing indexes + // into the color palette. Piece these together into one larger + // image for final rendering. + int totalWidth = 0; + int fullWidth = cells.size() * getTextWidth(); + int fullHeight = getTextHeight(); + for (int i = 0; i < cells.size(); i++) { + totalWidth += cells.get(i).getImage().getWidth(); + } + + BufferedImage image = new BufferedImage(fullWidth, + fullHeight, BufferedImage.TYPE_INT_ARGB); + + int [] rgbArray; + for (int i = 0; i < cells.size() - 1; i++) { + if (cells.get(i).isInvertedImage()) { + rgbArray = new int[imageWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + rgbArray = cells.get(i).getImage().getRGB(0, 0, + imageWidth, imageHeight, null, 0, imageWidth); + } + image.setRGB(i * imageWidth, 0, imageWidth, imageHeight, + rgbArray, 0, imageWidth); + if (imageHeight < fullHeight) { + int backgroundColor = cells.get(i).getBackground().getRGB(); + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = imageHeight; imageY < fullHeight; + imageY++) { + + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + } + totalWidth -= ((cells.size() - 1) * imageWidth); + if (cells.get(cells.size() - 1).isInvertedImage()) { + rgbArray = new int[totalWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, + totalWidth, imageHeight, null, 0, totalWidth); + } + image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, + imageHeight, rgbArray, 0, totalWidth); + + if (totalWidth < getTextWidth()) { + int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); + + for (int imageX = image.getWidth() - totalWidth; + imageX < image.getWidth(); imageX++) { + + for (int imageY = 0; imageY < fullHeight; imageY++) { + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + + // Dither the image. It is ok to lose the original here. + if (palette == null) { + palette = new SixelPalette(); + } + image = palette.ditherImage(image); + + // Emit the palette, but only for the colors actually used by these + // cells. + boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS]; + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = 0; imageY < image.getHeight(); imageY++) { + usedColors[image.getRGB(imageX, imageY)] = true; + } + } + palette.emitPalette(sb, usedColors); + + // Render the entire row of cells. + for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) { + int [][] sixels = new int[image.getWidth()][6]; + + // See which colors are actually used in this band of sixels. + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = 0; + (imageY < 6) && (imageY + currentRow < fullHeight); + imageY++) { + + int colorIdx = image.getRGB(imageX, imageY + currentRow); + assert (colorIdx >= 0); + assert (colorIdx < MAX_COLOR_REGISTERS); + + sixels[imageX][imageY] = colorIdx; + } + } + + for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + boolean isUsed = false; + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int j = 0; j < 6; j++) { + if (sixels[imageX][j] == i) { + isUsed = true; + } + } + } + if (isUsed == false) { + continue; + } + + // Set to the beginning of scan line for the next set of + // colored pixels, and select the color. + sb.append(String.format("$#%d", i)); + + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + + // Add up all the pixels that match this color. + int data = 0; + for (int j = 0; + (j < 6) && (currentRow + j < fullHeight); + j++) { + + if (sixels[imageX][j] == i) { + switch (j) { + case 0: + data += 1; + break; + case 1: + data += 2; + break; + case 2: + data += 4; + break; + case 3: + data += 8; + break; + case 4: + data += 16; + break; + case 5: + data += 32; + break; + } + } + } + assert (data >= 0); + assert (data < 127); + data += 63; + sb.append((char) data); + } // for (int imageX = 0; imageX < image.getWidth(); imageX++) + } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++) + + // Advance to the next scan line. + sb.append("-"); + + } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6) + + // Kill the very last "-", because it is unnecessary. + sb.deleteCharAt(sb.length() - 1); + + if (saveInCache) { + // This row is OK to save into the cache. + sixelCache.put(cells, sb.toString()); + } + + return (startSixel(x, y) + sb.toString() + endSixel()); + } + + // ------------------------------------------------------------------------ + // End sixel output support ----------------------------------------------- + // ------------------------------------------------------------------------ + /** * Create a SGR parameter sequence for a single color change. * @@ -1773,9 +2973,9 @@ public class ECMA48Terminal extends LogicalScreen */ private String colorRGB(final int colorRGB, final boolean foreground) { - int colorRed = (colorRGB >> 16) & 0xFF; - int colorGreen = (colorRGB >> 8) & 0xFF; - int colorBlue = colorRGB & 0xFF; + int colorRed = (colorRGB >>> 16) & 0xFF; + int colorGreen = (colorRGB >>> 8) & 0xFF; + int colorBlue = colorRGB & 0xFF; StringBuilder sb = new StringBuilder(); if (foreground) { @@ -1797,12 +2997,12 @@ public class ECMA48Terminal extends LogicalScreen * e.g. "\033[42m" */ private String colorRGB(final int foreColorRGB, final int backColorRGB) { - int foreColorRed = (foreColorRGB >> 16) & 0xFF; - int foreColorGreen = (foreColorRGB >> 8) & 0xFF; - int foreColorBlue = foreColorRGB & 0xFF; - int backColorRed = (backColorRGB >> 16) & 0xFF; - int backColorGreen = (backColorRGB >> 8) & 0xFF; - int backColorBlue = backColorRGB & 0xFF; + int foreColorRed = (foreColorRGB >>> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >>> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >>> 16) & 0xFF; + int backColorGreen = (backColorRGB >>> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; StringBuilder sb = new StringBuilder(); sb.append(String.format("\033[38;2;%d;%d;%dm", @@ -2051,12 +3251,12 @@ public class ECMA48Terminal extends LogicalScreen final boolean bold, final boolean reverse, final boolean blink, final boolean underline) { - int foreColorRed = (foreColorRGB >> 16) & 0xFF; - int foreColorGreen = (foreColorRGB >> 8) & 0xFF; - int foreColorBlue = foreColorRGB & 0xFF; - int backColorRed = (backColorRGB >> 16) & 0xFF; - int backColorGreen = (backColorRGB >> 8) & 0xFF; - int backColorBlue = backColorRGB & 0xFF; + int foreColorRed = (foreColorRGB >>> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >>> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >>> 16) & 0xFF; + int backColorGreen = (backColorRGB >>> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; StringBuilder sb = new StringBuilder(); if ( bold && reverse && blink && !underline ) { diff --git a/src/jexer/backend/GenericBackend.java b/src/jexer/backend/GenericBackend.java index bb9ae9f..908be1e 100644 --- a/src/jexer/backend/GenericBackend.java +++ b/src/jexer/backend/GenericBackend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index c24703e..b764831 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -322,11 +322,8 @@ public class LogicalScreen implements Screen { // If this happens to be the cursor position, make the position // dirty. if ((cursorX == X) && (cursorY == Y)) { - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } + physical[cursorX][cursorY].unset(); + unsetImageRow(cursorY); } } } @@ -354,7 +351,35 @@ public class LogicalScreen implements Screen { * @param ch character + attributes to draw */ public final void putCharXY(final int x, final int y, final Cell ch) { - putCharXY(x, y, ch.getChar(), ch); + if ((x < clipLeft) + || (x >= clipRight) + || (y < clipTop) + || (y >= clipBottom) + ) { + return; + } + + int X = x + offsetX; + int Y = y + offsetY; + + // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch); + + if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) { + + // Do not put control characters on the display + if (!ch.isImage()) { + assert (ch.getChar() >= 0x20); + assert (ch.getChar() != 0x7F); + } + logical[X][Y].setTo(ch); + + // If this happens to be the cursor position, make the position + // dirty. + if ((cursorX == X) && (cursorY == Y)) { + physical[cursorX][cursorY].unset(); + unsetImageRow(cursorY); + } + } } /** @@ -393,11 +418,8 @@ public class LogicalScreen implements Screen { // If this happens to be the cursor position, make the position // dirty. if ((cursorX == X) && (cursorY == Y)) { - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } + physical[cursorX][cursorY].unset(); + unsetImageRow(cursorY); } } } @@ -430,11 +452,8 @@ public class LogicalScreen implements Screen { // If this happens to be the cursor position, make the position // dirty. if ((cursorX == X) && (cursorY == Y)) { - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } + physical[cursorX][cursorY].unset(); + unsetImageRow(cursorY); } } } @@ -600,7 +619,7 @@ public class LogicalScreen implements Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -617,7 +636,7 @@ public class LogicalScreen implements Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -702,7 +721,7 @@ public class LogicalScreen implements Screen { /** * Draw a box shadow. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -719,12 +738,10 @@ public class LogicalScreen implements Screen { // Shadows do not honor clipping but they DO honor offset. int oldClipRight = clipRight; int oldClipBottom = clipBottom; - /* - clipRight = boxWidth + 2; - clipBottom = boxHeight + 1; - */ - clipRight = width; - clipBottom = height; + // When offsetX or offsetY go negative, we need to increase the clip + // bounds. + clipRight = width - offsetX; + clipBottom = height - offsetY; for (int i = 0; i < boxHeight; i++) { putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr); @@ -756,11 +773,8 @@ public class LogicalScreen implements Screen { && (cursorX <= width - 1) ) { // Make the current cursor position dirty - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } + physical[cursorX][cursorY].unset(); + unsetImageRow(cursorY); } cursorVisible = visible; @@ -863,7 +877,21 @@ public class LogicalScreen implements Screen { public final void clearPhysical() { for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { - physical[col][row].reset(); + physical[col][row].unset(); + } + } + } + + /** + * Unset every image cell on one row of the physical screen, forcing + * images on that row to be redrawn. + * + * @param y row coordinate. 0 is the top-most row. + */ + public final void unsetImageRow(final int y) { + for (int x = 0; x < width; x++) { + if (logical[x][y].isImage()) { + physical[x][y].unset(); } } } diff --git a/src/jexer/backend/MultiBackend.java b/src/jexer/backend/MultiBackend.java index 4e82d4e..5e4d3ca 100644 --- a/src/jexer/backend/MultiBackend.java +++ b/src/jexer/backend/MultiBackend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java index 7768873..0208410 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -396,7 +396,14 @@ public class MultiScreen implements Screen { * @return current screen height */ public int getHeight() { - return screens.get(0).getHeight(); + // Return the smallest height of the screens. + int height = screens.get(0).getHeight(); + for (Screen screen: screens) { + if (screen.getHeight() < height) { + height = screen.getHeight(); + } + } + return height; } /** @@ -405,7 +412,14 @@ public class MultiScreen implements Screen { * @return current screen width */ public int getWidth() { - return screens.get(0).getWidth(); + // Return the smallest width of the screens. + int width = screens.get(0).getWidth(); + for (Screen screen: screens) { + if (screen.getWidth() < width) { + width = screen.getWidth(); + } + } + return width; } /** @@ -496,6 +510,27 @@ public class MultiScreen implements Screen { } } + /** + * Clear the physical screen. + */ + public void clearPhysical() { + for (Screen screen: screens) { + screen.clearPhysical(); + } + } + + /** + * Unset every image cell on one row of the physical screen, forcing + * images on that row to be redrawn. + * + * @param y row coordinate. 0 is the top-most row. + */ + public final void unsetImageRow(final int y) { + for (Screen screen: screens) { + screen.unsetImageRow(y); + } + } + /** * Classes must provide an implementation to push the logical screen to * the physical device. @@ -590,4 +625,50 @@ public class MultiScreen implements Screen { } } + /** + * Get the width of a character cell in pixels. + * + * @return the width in pixels of a character cell + */ + public int getTextWidth() { + int textWidth = 16; + for (Screen screen: screens) { + int newTextWidth = textWidth; + if (screen instanceof MultiScreen) { + newTextWidth = ((MultiScreen) screen).getTextWidth(); + } else if (screen instanceof ECMA48Terminal) { + newTextWidth = ((ECMA48Terminal) screen).getTextWidth(); + } else if (screen instanceof SwingTerminal) { + newTextWidth = ((SwingTerminal) screen).getTextWidth(); + } + if (newTextWidth < textWidth) { + textWidth = newTextWidth; + } + } + return textWidth; + } + + /** + * Get the height of a character cell in pixels. + * + * @return the height in pixels of a character cell + */ + public int getTextHeight() { + int textHeight = 20; + for (Screen screen: screens) { + int newTextHeight = textHeight; + if (screen instanceof MultiScreen) { + newTextHeight = ((MultiScreen) screen).getTextHeight(); + } else if (screen instanceof ECMA48Terminal) { + newTextHeight = ((ECMA48Terminal) screen).getTextHeight(); + } else if (screen instanceof SwingTerminal) { + newTextHeight = ((SwingTerminal) screen).getTextHeight(); + } + if (newTextHeight < textHeight) { + textHeight = newTextHeight; + } + } + return textHeight; + } + } diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java index 0fbbea4..41c0756 100644 --- a/src/jexer/backend/Screen.java +++ b/src/jexer/backend/Screen.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -334,6 +334,19 @@ public interface Screen { public void drawBoxShadow(final int left, final int top, final int right, final int bottom); + /** + * Clear the physical screen. + */ + public void clearPhysical(); + + /** + * Unset every image cell on one row of the physical screen, forcing + * images on that row to be redrawn. + * + * @param y row coordinate. 0 is the top-most row. + */ + public void unsetImageRow(final int y); + /** * Classes must provide an implementation to push the logical screen to * the physical device. diff --git a/src/jexer/backend/SessionInfo.java b/src/jexer/backend/SessionInfo.java index 5c2d034..8a29ce0 100644 --- a/src/jexer/backend/SessionInfo.java +++ b/src/jexer/backend/SessionInfo.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index 797caa8..8a342b6 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java index 56eb8bf..92fd1d8 100644 --- a/src/jexer/backend/SwingComponent.java +++ b/src/jexer/backend/SwingComponent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -74,13 +74,13 @@ class SwingComponent { /** * An optional border in pixels to add. */ - private static final int BORDER = 5; + private static final int BORDER = 1; /** * Adjustable Insets for this component. This has the effect of adding a * black border around the drawing area. */ - Insets adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); + Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER); // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- @@ -366,11 +366,11 @@ class SwingComponent { // Figure out the thickness of borders and use that to set the final // size. if (frame != null) { - Insets insets = frame.getInsets(); + Insets insets = getInsets(); frame.setSize(width + insets.left + insets.right, height + insets.top + insets.bottom); } else { - Insets insets = component.getInsets(); + Insets insets = getInsets(); component.setSize(width + insets.left + insets.right, height + insets.top + insets.bottom); } diff --git a/src/jexer/backend/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java index 6d1c644..28668fd 100644 --- a/src/jexer/backend/SwingSessionInfo.java +++ b/src/jexer/backend/SwingSessionInfo.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -187,7 +187,6 @@ public class SwingSessionInfo implements SessionInfo { swing.getWidth(), swing.getHeight(), windowWidth, windowHeight); */ - } // ------------------------------------------------------------------------ diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index 3f5b58e..bab8f82 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -398,14 +398,16 @@ public class SwingTerminal extends LogicalScreen SwingTerminal.this.textHeight, windowWidth, windowHeight); - SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); + SwingTerminal.this.setDimensions(sessionInfo. + getWindowWidth(), sessionInfo.getWindowHeight()); SwingTerminal.this.resizeToScreen(); SwingTerminal.this.swing.setVisible(true); } }); - } catch (Exception e) { + } catch (java.lang.reflect.InvocationTargetException e) { + e.printStackTrace(); + } catch (InterruptedException e) { e.printStackTrace(); } @@ -527,7 +529,9 @@ public class SwingTerminal extends LogicalScreen SwingTerminal.this.textHeight); } }); - } catch (Exception e) { + } catch (java.lang.reflect.InvocationTargetException e) { + e.printStackTrace(); + } catch (InterruptedException e) { e.printStackTrace(); } @@ -643,6 +647,24 @@ public class SwingTerminal extends LogicalScreen // SwingTerminal ---------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Get the width of a character cell in pixels. + * + * @return the width in pixels of a character cell + */ + public int getTextWidth() { + return textWidth; + } + + /** + * Get the height of a character cell in pixels. + * + * @return the height in pixels of a character cell + */ + public int getTextHeight() { + return textHeight; + } + /** * Setup Swing colors to match DOS color palette. */ @@ -727,7 +749,10 @@ public class SwingTerminal extends LogicalScreen Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize); gotTerminus = true; font = terminus; - } catch (Exception e) { + } catch (java.awt.FontFormatException e) { + e.printStackTrace(); + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); + } catch (java.io.IOException e) { e.printStackTrace(); font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); } @@ -964,7 +989,41 @@ public class SwingTerminal extends LogicalScreen * Resize to font dimensions. */ public void resizeToScreen() { - swing.setDimensions(textWidth * width, textHeight * height); + swing.setDimensions(textWidth * (width + 1), textHeight * (height + 1)); + } + + /** + * Draw one cell's image to the screen. + * + * @param gr the Swing Graphics context + * @param cell the Cell to draw + * @param xPixel the x-coordinate to render to. 0 means the + * left-most pixel column. + * @param yPixel the y-coordinate to render to. 0 means the top-most + * pixel row. + */ + private void drawImage(final Graphics gr, final Cell cell, + final int xPixel, final int yPixel) { + + /* + System.err.println("drawImage(): " + xPixel + " " + yPixel + + " " + cell); + */ + + // Draw the background rectangle, then the foreground character. + assert (cell.isImage()); + gr.setColor(cell.getBackground()); + gr.fillRect(xPixel, yPixel, textWidth, textHeight); + + BufferedImage image = cell.getImage(); + if (image != null) { + if (swing.getFrame() != null) { + gr.drawImage(image, xPixel, yPixel, swing.getFrame()); + } else { + gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + } + return; + } } /** @@ -1190,7 +1249,11 @@ public class SwingTerminal extends LogicalScreen || reallyCleared || (swing.getFrame() == null)) { - drawGlyph(gr, lCell, xPixel, yPixel); + if (lCell.isImage()) { + drawImage(gr, lCell, xPixel, yPixel); + } else { + drawGlyph(gr, lCell, xPixel, yPixel); + } // Physical is always updated physical[x][y].setTo(lCell); @@ -1260,7 +1323,11 @@ public class SwingTerminal extends LogicalScreen && cursorVisible) || (lCell.isBlink()) ) { - drawGlyph(gr, lCell, xPixel, yPixel); + if (lCell.isImage()) { + drawImage(gr, lCell, xPixel, yPixel); + } else { + drawGlyph(gr, lCell, xPixel, yPixel); + } physical[x][y].setTo(lCell); } } diff --git a/src/jexer/backend/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java index 11c0faa..ccddce4 100644 --- a/src/jexer/backend/TSessionInfo.java +++ b/src/jexer/backend/TSessionInfo.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java index e6fa57a..d7f5bc8 100644 --- a/src/jexer/backend/TTYSessionInfo.java +++ b/src/jexer/backend/TTYSessionInfo.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -213,7 +213,7 @@ public class TTYSessionInfo implements SessionInfo { process.waitFor(); break; } catch (InterruptedException e) { - e.printStackTrace(); + // SQUASH } } int rc = process.exitValue(); diff --git a/src/jexer/backend/TWindowBackend.java b/src/jexer/backend/TWindowBackend.java index 41809cf..20b9d7d 100644 --- a/src/jexer/backend/TWindowBackend.java +++ b/src/jexer/backend/TWindowBackend.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/TerminalReader.java b/src/jexer/backend/TerminalReader.java index 6900732..8edadbf 100644 --- a/src/jexer/backend/TerminalReader.java +++ b/src/jexer/backend/TerminalReader.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/backend/package-info.java b/src/jexer/backend/package-info.java index dbaa6da..46d8ba1 100644 --- a/src/jexer/backend/package-info.java +++ b/src/jexer/backend/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/bits/Cell.java b/src/jexer/bits/Cell.java index f7b71a5..d4c816f 100644 --- a/src/jexer/bits/Cell.java +++ b/src/jexer/bits/Cell.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,11 +28,24 @@ */ package jexer.bits; +import java.awt.Color; +import java.awt.image.BufferedImage; + /** - * This class represents a single text cell on the screen. + * This class represents a single text cell or bit of image on the screen. */ public final class Cell extends CellAttributes { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The special "this cell is unset" (null) value. This is the Unicode + * "not a character" value. + */ + private static final char UNSET_VALUE = (char) 65535; + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -42,6 +55,34 @@ public final class Cell extends CellAttributes { */ private char ch; + /** + * The image at this cell. + */ + private BufferedImage image = null; + + /** + * The image at this cell, inverted. + */ + private BufferedImage invertedImage = null; + + /** + * The background color used for the area the image portion might not + * cover. + */ + private Color background = null; + + /** + * hashCode() needs to call image.hashCode(), which can get quite + * expensive. + */ + private int imageHashCode = 0; + + /** + * hashCode() needs to call background.hashCode(), which can get quite + * expensive. + */ + private int backgroundHashCode = 0; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -72,6 +113,98 @@ public final class Cell extends CellAttributes { // Cell ------------------------------------------------------------------- // ------------------------------------------------------------------------ + + /** + * Set the image data for this cell. + * + * @param image the image for this cell + */ + public void setImage(final BufferedImage image) { + this.image = image; + imageHashCode = image.hashCode(); + } + + /** + * Get the image data for this cell. + * + * @return the image for this cell + */ + public BufferedImage getImage() { + if (invertedImage != null) { + return invertedImage; + } + return image; + } + + /** + * Get the bitmap image background color for this cell. + * + * @return the bitmap image background color + */ + public Color getBackground() { + return background; + } + + /** + * If true, this cell has image data. + * + * @return true if this cell is an image rather than a character with + * attributes + */ + public boolean isImage() { + if (image != null) { + return true; + } + return false; + } + + /** + * Restore the image in this cell to its normal version, if it has one. + */ + public void restoreImage() { + invertedImage = null; + } + + /** + * If true, this cell has image data, and that data is inverted. + * + * @return true if this cell is an image rather than a character with + * attributes, and the data is inverted + */ + public boolean isInvertedImage() { + if ((image != null) && (invertedImage != null)) { + return true; + } + return false; + } + + /** + * Invert the image in this cell, if it has one. + */ + public void invertImage() { + if (image == null) { + return; + } + if (invertedImage == null) { + invertedImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + int [] rgbArray = image.getRGB(0, 0, + image.getWidth(), image.getHeight(), null, 0, image.getWidth()); + + for (int i = 0; i < rgbArray.length; i++) { + // Set the colors to fully inverted. + if (rgbArray[i] != 0x00FFFFFF) { + rgbArray[i] ^= 0x00FFFFFF; + } + // Also set alpha to non-transparent. + rgbArray[i] |= 0xFF000000; + } + invertedImage.setRGB(0, 0, image.getWidth(), image.getHeight(), + rgbArray, 0, image.getWidth()); + } + } + /** * Getter for cell character. * @@ -97,6 +230,25 @@ public final class Cell extends CellAttributes { public void reset() { super.reset(); ch = ' '; + image = null; + imageHashCode = 0; + invertedImage = null; + background = Color.BLACK; + backgroundHashCode = 0; + } + + /** + * UNset this cell. It will not be equal to any other cell until it has + * been assigned attributes and a character. + */ + public void unset() { + super.reset(); + ch = UNSET_VALUE; + image = null; + imageHashCode = 0; + invertedImage = null; + background = Color.BLACK; + backgroundHashCode = 0; } /** @@ -107,6 +259,9 @@ public final class Cell extends CellAttributes { * @return true if this cell has default attributes. */ public boolean isBlank() { + if ((ch == UNSET_VALUE) || (image != null)) { + return false; + } if ((getForeColor().equals(Color.WHITE)) && (getBackColor().equals(Color.BLACK)) && !isBold() @@ -115,6 +270,7 @@ public final class Cell extends CellAttributes { && !isUnderline() && !isProtect() && !isRGB() + && !isImage() && (ch == ' ') ) { return true; @@ -137,6 +293,40 @@ public final class Cell extends CellAttributes { Cell that = (Cell) rhs; + // Unsetted cells can never be equal. + if ((ch == UNSET_VALUE) || (that.ch == UNSET_VALUE)) { + return false; + } + + // If this or rhs has an image and the other doesn't, these are not + // equal. + if ((image != null) && (that.image == null)) { + return false; + } + if ((image == null) && (that.image != null)) { + return false; + } + // If this and rhs have images, both must match. + if ((image != null) && (that.image != null)) { + if ((invertedImage == null) && (that.invertedImage != null)) { + return false; + } + if ((invertedImage != null) && (that.invertedImage == null)) { + return false; + } + // Either both objects have their image inverted, or neither do. + // Now if the images are identical the cells are the same + // visually. + if (image.equals(that.image) + && (background.equals(that.background)) + ) { + return true; + } else { + return false; + } + } + + // Normal case: character and attributes must match. if (ch == that.ch) { return super.equals(rhs); } @@ -155,6 +345,17 @@ public final class Cell extends CellAttributes { int hash = A; hash = (B * hash) + super.hashCode(); hash = (B * hash) + (int)ch; + if (image != null) { + /* + hash = (B * hash) + image.hashCode(); + hash = (B * hash) + background.hashCode(); + */ + hash = (B * hash) + imageHashCode; + hash = (B * hash) + backgroundHashCode; + } + if (invertedImage != null) { + hash = (B * hash) + invertedImage.hashCode(); + } return hash; } @@ -167,11 +368,19 @@ public final class Cell extends CellAttributes { public void setTo(final Object rhs) { // Let this throw a ClassCastException CellAttributes thatAttr = (CellAttributes) rhs; + this.image = null; + this.imageHashCode = 0; + this.backgroundHashCode = 0; super.setTo(thatAttr); if (rhs instanceof Cell) { Cell that = (Cell) rhs; this.ch = that.ch; + this.image = that.image; + this.invertedImage = that.invertedImage; + this.background = that.background; + this.imageHashCode = that.imageHashCode; + this.backgroundHashCode = that.backgroundHashCode; } } @@ -181,6 +390,7 @@ public final class Cell extends CellAttributes { * @param that a CellAttributes instance */ public void setAttr(final CellAttributes that) { + image = null; super.setTo(that); } diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java index a4528c4..f60fd30 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/src/jexer/bits/CellAttributes.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/bits/Color.java b/src/jexer/bits/Color.java index 120ff64..4defed5 100644 --- a/src/jexer/bits/Color.java +++ b/src/jexer/bits/Color.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index 1c4670a..83f09f4 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -78,7 +78,7 @@ public class ColorTheme { * @return color associated with name, e.g. bold yellow on blue */ public CellAttributes getColor(final String name) { - CellAttributes attr = (CellAttributes) colors.get(name); + CellAttributes attr = colors.get(name); return attr; } @@ -153,7 +153,8 @@ public class ColorTheme { try { foreColorRGB = Integer.parseInt(tokenizer.nextToken(), 16); } catch (NumberFormatException e) { - e.printStackTrace(); + // Default to white on black + foreColorRGB = 0xFFFFFF; } // "on" @@ -167,7 +168,7 @@ public class ColorTheme { try { backColorRGB = Integer.parseInt(tokenizer.nextToken(), 16); } catch (NumberFormatException e) { - e.printStackTrace(); + backColorRGB = 0; } CellAttributes color = new CellAttributes(); @@ -371,14 +372,14 @@ public class ColorTheme { // TField text color = new CellAttributes(); - color.setForeColor(Color.WHITE); - color.setBackColor(Color.BLUE); + color.setForeColor(Color.BLACK); + color.setBackColor(Color.WHITE); color.setBold(false); colors.put("tfield.inactive", color); color = new CellAttributes(); - color.setForeColor(Color.YELLOW); - color.setBackColor(Color.BLACK); - color.setBold(true); + color.setForeColor(Color.BLACK); + color.setBackColor(Color.CYAN); + color.setBold(false); colors.put("tfield.active", color); // TCheckBox diff --git a/src/jexer/bits/GraphicsChars.java b/src/jexer/bits/GraphicsChars.java index b571639..58be231 100644 --- a/src/jexer/bits/GraphicsChars.java +++ b/src/jexer/bits/GraphicsChars.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -145,6 +145,8 @@ public final class GraphicsChars { public static final char WINDOW_RIGHT_BOTTOM_DOUBLE = CP437[0xBC]; public static final char VERTICAL_BAR = CP437[0xB3]; public static final char OCTOSTAR = CP437[0x0F]; + public static final char DOWNARROWLEFT = CP437[0xDD]; + public static final char DOWNARROWRIGHT = CP437[0xDE]; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- diff --git a/src/jexer/bits/MnemonicString.java b/src/jexer/bits/MnemonicString.java index f35eb50..5977ed5 100644 --- a/src/jexer/bits/MnemonicString.java +++ b/src/jexer/bits/MnemonicString.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/bits/StringUtils.java b/src/jexer/bits/StringUtils.java index 79b9110..1a2079d 100644 --- a/src/jexer/bits/StringUtils.java +++ b/src/jexer/bits/StringUtils.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/bits/package-info.java b/src/jexer/bits/package-info.java index f8531dd..cffe10e 100644 --- a/src/jexer/bits/package-info.java +++ b/src/jexer/bits/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/demos/Demo1.java b/src/jexer/demos/Demo1.java index d82a385..97088d2 100644 --- a/src/jexer/demos/Demo1.java +++ b/src/jexer/demos/Demo1.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/demos/Demo2.java b/src/jexer/demos/Demo2.java index b6572af..e6112f6 100644 --- a/src/jexer/demos/Demo2.java +++ b/src/jexer/demos/Demo2.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,8 +28,12 @@ */ package jexer.demos; -import java.net.*; -import jexer.net.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import jexer.net.TelnetServerSocket; /** * This class is the main driver for a simple demonstration of Jexer's @@ -38,6 +42,11 @@ import jexer.net.*; */ public class Demo2 { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo2.class.getName()); + /** * Main entry point. * @@ -47,7 +56,7 @@ public class Demo2 { ServerSocket server = null; try { if (args.length == 0) { - System.err.printf("USAGE: java -cp jexer.jar jexer.demos.Demo2 port\n"); + System.err.println(i18n.getString("usageString")); return; } @@ -55,12 +64,14 @@ public class Demo2 { server = new TelnetServerSocket(port); while (true) { Socket socket = server.accept(); - System.out.printf("New connection: %s\n", socket); + System.out.println(MessageFormat. + format(i18n.getString("newConnection"), socket)); DemoApplication app = new DemoApplication(socket.getInputStream(), socket.getOutputStream()); - System.out.printf(" language: %s\n", + System.out.println(MessageFormat. + format(i18n.getString("language"), ((jexer.net.TelnetInputStream) socket.getInputStream()). - getLanguage()); + getLanguage())); (new Thread(app)).start(); } } catch (Exception e) { diff --git a/src/jexer/demos/Demo2.properties b/src/jexer/demos/Demo2.properties new file mode 100644 index 0000000..0d93747 --- /dev/null +++ b/src/jexer/demos/Demo2.properties @@ -0,0 +1,3 @@ +usageString=USAGE: java -cp jexer.jar jexer.demos.Demo2 port +newConnection=New connection: {0} +language=\ \ \ language: {0} diff --git a/src/jexer/demos/Demo3.java b/src/jexer/demos/Demo3.java index b95f668..f370f8f 100644 --- a/src/jexer/demos/Demo3.java +++ b/src/jexer/demos/Demo3.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/demos/Demo4.java b/src/jexer/demos/Demo4.java index 5de0bed..edbc2c0 100644 --- a/src/jexer/demos/Demo4.java +++ b/src/jexer/demos/Demo4.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/demos/Demo5.java b/src/jexer/demos/Demo5.java index d383261..e63abc1 100644 --- a/src/jexer/demos/Demo5.java +++ b/src/jexer/demos/Demo5.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -31,6 +31,7 @@ package jexer.demos; import java.awt.Font; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; +import java.util.ResourceBundle; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JSplitPane; @@ -44,6 +45,11 @@ import jexer.backend.SwingBackend; */ public class Demo5 implements WindowListener { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo5.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -133,7 +139,7 @@ public class Demo5 implements WindowListener { // ------------------------------------------------------------------------ // Demo5 ------------------------------------------------------------------ // ------------------------------------------------------------------------ - + /** * Run two demo applications in separate panes. */ @@ -196,7 +202,7 @@ public class Demo5 implements WindowListener { mainPane.setBorder(null); frame.setContentPane(mainPane); - frame.setTitle("Two Jexer Apps In One Swing UI"); + frame.setTitle(i18n.getString("frameTitle")); frame.setSize(1000, 640); frame.setVisible(true); } diff --git a/src/jexer/demos/Demo5.properties b/src/jexer/demos/Demo5.properties new file mode 100644 index 0000000..56b419d --- /dev/null +++ b/src/jexer/demos/Demo5.properties @@ -0,0 +1 @@ +frameTitle=Two Jexer Apps In One Swing UI diff --git a/src/jexer/demos/Demo6.java b/src/jexer/demos/Demo6.java index 9f94913..236e7a2 100644 --- a/src/jexer/demos/Demo6.java +++ b/src/jexer/demos/Demo6.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -30,6 +30,7 @@ package jexer.demos; import jexer.TApplication; import jexer.backend.*; +import jexer.demos.DemoApplication; /** * This class shows off the use of MultiBackend and MultiScreen. diff --git a/src/jexer/demos/DemoApplication.java b/src/jexer/demos/DemoApplication.java index a9b3468..18d47f3 100644 --- a/src/jexer/demos/DemoApplication.java +++ b/src/jexer/demos/DemoApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,11 +28,22 @@ */ package jexer.demos; -import java.io.*; - -import jexer.*; -import jexer.event.*; -import jexer.menu.*; +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.ResourceBundle; + +import jexer.TApplication; +import jexer.TEditColorThemeWindow; +import jexer.TEditorWindow; +import jexer.event.TMenuEvent; +import jexer.menu.TMenu; +import jexer.menu.TMenuItem; +import jexer.menu.TSubMenu; import jexer.backend.Backend; import jexer.backend.SwingTerminal; @@ -41,6 +52,11 @@ import jexer.backend.SwingTerminal; */ public class DemoApplication extends TApplication { + /** + * Translated strings. + */ + private static ResourceBundle i18n = ResourceBundle.getBundle(DemoApplication.class.getName()); + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -63,7 +79,7 @@ public class DemoApplication extends TApplication { super(input, output); addAllWidgets(); - getBackend().setTitle("Jexer Demo Application"); + getBackend().setTitle(i18n.getString("applicationTitle")); } /** @@ -83,7 +99,7 @@ public class DemoApplication extends TApplication { super(input, reader, writer, setRawMode); addAllWidgets(); - getBackend().setTitle("Jexer Demo Application"); + getBackend().setTitle(i18n.getString("applicationTitle")); } /** @@ -121,7 +137,7 @@ public class DemoApplication extends TApplication { public DemoApplication(final BackendType backendType) throws Exception { super(backendType); addAllWidgets(); - getBackend().setTitle("Jexer Demo Application"); + getBackend().setTitle(i18n.getString("applicationTitle")); } // ------------------------------------------------------------------------ @@ -190,33 +206,33 @@ public class DemoApplication extends TApplication { addFileMenu(); addEditMenu(); - TMenu demoMenu = addMenu("&Demo"); - TMenuItem item = demoMenu.addItem(2000, "&Checkable"); + TMenu demoMenu = addMenu(i18n.getString("demo")); + TMenuItem item = demoMenu.addItem(2000, i18n.getString("checkable")); item.setCheckable(true); - item = demoMenu.addItem(2001, "Disabled"); + item = demoMenu.addItem(2001, i18n.getString("disabled")); item.setEnabled(false); - item = demoMenu.addItem(2002, "&Normal"); - TSubMenu subMenu = demoMenu.addSubMenu("Sub-&Menu"); - item = demoMenu.addItem(2010, "N&ormal A&&D"); - item = demoMenu.addItem(2050, "Co&lors..."); + item = demoMenu.addItem(2002, i18n.getString("normal")); + TSubMenu subMenu = demoMenu.addSubMenu(i18n.getString("subMenu")); + item = demoMenu.addItem(2010, i18n.getString("normal")); + item = demoMenu.addItem(2050, i18n.getString("colors")); - item = subMenu.addItem(2000, "&Checkable (sub)"); + item = subMenu.addItem(2000, i18n.getString("checkableSub")); item.setCheckable(true); - item = subMenu.addItem(2001, "Disabled (sub)"); + item = subMenu.addItem(2001, i18n.getString("disabledSub")); item.setEnabled(false); - item = subMenu.addItem(2002, "&Normal (sub)"); + item = subMenu.addItem(2002, i18n.getString("normalSub")); - subMenu = subMenu.addSubMenu("Sub-&Menu"); - item = subMenu.addItem(2000, "&Checkable (sub)"); + subMenu = subMenu.addSubMenu(i18n.getString("subMenu")); + item = subMenu.addItem(2000, i18n.getString("checkableSub")); item.setCheckable(true); - item = subMenu.addItem(2001, "Disabled (sub)"); + item = subMenu.addItem(2001, i18n.getString("disabledSub")); item.setEnabled(false); - item = subMenu.addItem(2002, "&Normal (sub)"); + item = subMenu.addItem(2002, i18n.getString("normalSub")); if (getScreen() instanceof SwingTerminal) { - TMenu swingMenu = addMenu("Swin&g"); - item = swingMenu.addItem(3000, "&Bigger +2"); - item = swingMenu.addItem(3001, "&Smaller -2"); + TMenu swingMenu = addMenu(i18n.getString("swing")); + item = swingMenu.addItem(3000, i18n.getString("bigger")); + item = swingMenu.addItem(3001, i18n.getString("smaller")); } addWindowMenu(); diff --git a/src/jexer/demos/DemoApplication.properties b/src/jexer/demos/DemoApplication.properties new file mode 100644 index 0000000..95d8603 --- /dev/null +++ b/src/jexer/demos/DemoApplication.properties @@ -0,0 +1,15 @@ +applicationTitle=Demo Application + +demo=&Demo +checkable=&Checkable +disabled=Disabled +normal=&Normal +subMenu=Sub-&Menu +normal=N&ormal A&&D +colors=Co&lors... +checkableSub=&Checkable (sub) +disabledSub=Disabled (sub) +normalSub=&Normal (sub) +swing=Swin&g +bigger=&Bigger +2 +smaller=&Smaller -2 diff --git a/src/jexer/demos/DemoCheckBoxWindow.java b/src/jexer/demos/DemoCheckBoxWindow.java index 4668051..c982244 100644 --- a/src/jexer/demos/DemoCheckBoxWindow.java +++ b/src/jexer/demos/DemoCheckBoxWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,10 +28,17 @@ */ package jexer.demos; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; - -import jexer.*; +import java.util.ResourceBundle; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TComboBox; +import jexer.TMessageBox; +import jexer.TRadioGroup; +import jexer.TWindow; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -41,6 +48,11 @@ import static jexer.TKeypress.*; */ public class DemoCheckBoxWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoCheckBoxWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -73,50 +85,49 @@ public class DemoCheckBoxWindow extends TWindow { DemoCheckBoxWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it will be // centered on screen. - super(parent, "Radiobuttons, CheckBoxes, and ComboBox", - 0, 0, 60, 17, flags); + super(parent, i18n.getString("windowTitle"), 0, 0, 60, 17, flags); int row = 1; // Add some widgets - addLabel("Check box example 1", 1, row); - addCheckBox(35, row++, "CheckBox 1", false); - addLabel("Check box example 2", 1, row); - addCheckBox(35, row++, "CheckBox 2", true); + addLabel(i18n.getString("checkBoxLabel1"), 1, row); + addCheckBox(35, row++, i18n.getString("checkBoxText1"), false); + addLabel(i18n.getString("checkBoxLabel2"), 1, row); + addCheckBox(35, row++, i18n.getString("checkBoxText2"), true); row += 2; - TRadioGroup group = addRadioGroup(1, row, "Group 1"); - group.addRadioButton("Radio option 1"); - group.addRadioButton("Radio option 2"); - group.addRadioButton("Radio option 3"); + TRadioGroup group = addRadioGroup(1, row, + i18n.getString("radioGroupTitle")); + group.addRadioButton(i18n.getString("radioOption1")); + group.addRadioButton(i18n.getString("radioOption2")); + group.addRadioButton(i18n.getString("radioOption3")); List comboValues = new ArrayList(); - comboValues.add("String 0"); - comboValues.add("String 1"); - comboValues.add("String 2"); - comboValues.add("String 3"); - comboValues.add("String 4"); - comboValues.add("String 5"); - comboValues.add("String 6"); - comboValues.add("String 7"); - comboValues.add("String 8"); - comboValues.add("String 9"); - comboValues.add("String 10"); + comboValues.add(i18n.getString("comboBoxString0")); + comboValues.add(i18n.getString("comboBoxString1")); + comboValues.add(i18n.getString("comboBoxString2")); + comboValues.add(i18n.getString("comboBoxString3")); + comboValues.add(i18n.getString("comboBoxString4")); + comboValues.add(i18n.getString("comboBoxString5")); + comboValues.add(i18n.getString("comboBoxString6")); + comboValues.add(i18n.getString("comboBoxString7")); + comboValues.add(i18n.getString("comboBoxString8")); + comboValues.add(i18n.getString("comboBoxString9")); + comboValues.add(i18n.getString("comboBoxString10")); comboBox = addComboBox(35, row, 12, comboValues, 2, 6, new TAction() { public void DO() { - getApplication().messageBox("ComboBox", - "You selected the following value:\n" + - "\n" + - comboBox.getText() + - "\n", + getApplication().messageBox(i18n.getString("messageBoxTitle"), + MessageFormat.format(i18n.getString("messageBoxPrompt"), + comboBox.getText()), TMessageBox.Type.OK); } } ); - addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4, + addButton(i18n.getString("closeWindow"), + (getWidth() - 14) / 2, getHeight() - 4, new TAction() { public void DO() { DemoCheckBoxWindow.this.getApplication() @@ -125,11 +136,15 @@ public class DemoCheckBoxWindow extends TWindow { } ); - statusBar = newStatusBar("Radiobuttons and checkboxes"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } } diff --git a/src/jexer/demos/DemoCheckBoxWindow.properties b/src/jexer/demos/DemoCheckBoxWindow.properties new file mode 100644 index 0000000..61210ce --- /dev/null +++ b/src/jexer/demos/DemoCheckBoxWindow.properties @@ -0,0 +1,30 @@ +windowTitle=Radiobuttons, CheckBoxes, and ComboBox + +statusBar=Radiobuttons and checkboxes +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +checkBoxLabel1=Check box example 1 +checkBoxText1=CheckBox 1 +checkBoxLabel2=Check box example 2 +checkBoxText2=CheckBox 2 +radioGroupTitle=Group 1 +radioOption1=Radio option 1 +radioOption2=Radio option 2 +radioOption3=Radio option 3 +comboBoxString0=String 0 +comboBoxString1=String 1 +comboBoxString2=String 2 +comboBoxString3=String 3 +comboBoxString4=String 4 +comboBoxString5=String 5 +comboBoxString6=String 6 +comboBoxString7=String 7 +comboBoxString8=String 8 +comboBoxString9=String 9 +comboBoxString10=String 10 +messageBoxTitle=ComboBox +messageBoxPrompt=You selected the following value:\n\n{0}\n +closeWindow=&Close Window diff --git a/src/jexer/demos/DemoEditorWindow.java b/src/jexer/demos/DemoEditorWindow.java index 1047159..c63ce0d 100644 --- a/src/jexer/demos/DemoEditorWindow.java +++ b/src/jexer/demos/DemoEditorWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,8 +28,13 @@ */ package jexer.demos; -import jexer.*; -import jexer.event.*; +import java.util.ResourceBundle; + +import jexer.TApplication; +import jexer.TEditorWidget; +import jexer.TWidget; +import jexer.TWindow; +import jexer.event.TResizeEvent; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -38,6 +43,11 @@ import static jexer.TKeypress.*; */ public class DemoEditorWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoEditorWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -62,12 +72,15 @@ public class DemoEditorWindow extends TWindow { final String text) { super(parent, title, 0, 0, 44, 22, RESIZABLE); - editField = addEditor(text, 0, 0, 42, 20); + editField = new TEditorWidget(this, text, 0, 0, 42, 20); - statusBar = newStatusBar("Editable text demo window"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } /** @@ -76,7 +89,7 @@ public class DemoEditorWindow extends TWindow { * @param parent the main application */ public DemoEditorWindow(final TApplication parent) { - this(parent, "Editor", + this(parent, i18n.getString("windowTitle"), "This is an example of an editable text field. Some example text follows.\n" + "\n" + "This library implements a text-based windowing system loosely\n" + diff --git a/src/jexer/demos/DemoEditorWindow.properties b/src/jexer/demos/DemoEditorWindow.properties new file mode 100644 index 0000000..3fa3212 --- /dev/null +++ b/src/jexer/demos/DemoEditorWindow.properties @@ -0,0 +1,6 @@ +windowTitle=Editor + +statusBar=Editable text demo window +statusBarHelp=Help +statusBarShell=Shell +statusBarExit=Exit diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index f1cbe21..1ae35f0 100644 --- a/src/jexer/demos/DemoMainWindow.java +++ b/src/jexer/demos/DemoMainWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,11 +28,24 @@ */ package jexer.demos; -import java.io.*; -import java.util.*; - -import jexer.*; -import jexer.event.*; +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.ResourceBundle; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TEditColorThemeWindow; +import jexer.TEditorWindow; +import jexer.TLabel; +import jexer.TProgressBar; +import jexer.TTimer; +import jexer.TWidget; +import jexer.TWindow; +import jexer.event.TCommandEvent; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -42,6 +55,11 @@ import static jexer.TKeypress.*; */ public class DemoMainWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMainWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -101,13 +119,13 @@ public class DemoMainWindow extends TWindow { private DemoMainWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it will be // centered on screen. - super(parent, "Demo Window", 0, 0, 64, 23, flags); + super(parent, i18n.getString("windowTitle"), 0, 0, 64, 23, flags); int row = 1; // Add some widgets - addLabel("Message Boxes", 1, row); - TWidget first = addButton("&MessageBoxes", 35, row, + addLabel(i18n.getString("messageBoxLabel"), 1, row); + TWidget first = addButton(i18n.getString("messageBoxButton"), 35, row, new TAction() { public void DO() { new DemoMsgBoxWindow(getApplication()); @@ -116,8 +134,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Open me as modal", 1, row); - addButton("W&indow", 35, row, + addLabel(i18n.getString("openModalLabel"), 1, row); + addButton(i18n.getString("openModalButton"), 35, row, new TAction() { public void DO() { new DemoMainWindow(getApplication(), MODAL); @@ -126,8 +144,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Text fields and calendar", 1, row); - addButton("Field&s", 35, row, + addLabel(i18n.getString("textFieldLabel"), 1, row); + addButton(i18n.getString("textFieldButton"), 35, row, new TAction() { public void DO() { new DemoTextFieldWindow(getApplication()); @@ -136,8 +154,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Radio buttons, check and combobox", 1, row); - addButton("&CheckBoxes", 35, row, + addLabel(i18n.getString("radioButtonLabel"), 1, row); + addButton(i18n.getString("radioButtonButton"), 35, row, new TAction() { public void DO() { new DemoCheckBoxWindow(getApplication()); @@ -146,15 +164,15 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Editor window", 1, row); - addButton("&1 Widget", 35, row, + addLabel(i18n.getString("editorLabel"), 1, row); + addButton(i18n.getString("editorButton1"), 35, row, new TAction() { public void DO() { new DemoEditorWindow(getApplication()); } } ); - addButton("&2 Window", 48, row, + addButton(i18n.getString("editorButton2"), 48, row, new TAction() { public void DO() { new TEditorWindow(getApplication()); @@ -163,8 +181,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Text areas", 1, row); - addButton("&Text", 35, row, + addLabel(i18n.getString("textAreaLabel"), 1, row); + addButton(i18n.getString("textAreaButton"), 35, row, new TAction() { public void DO() { new DemoTextWindow(getApplication()); @@ -173,8 +191,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Tree views", 1, row); - addButton("Tree&View", 35, row, + addLabel(i18n.getString("treeViewLabel"), 1, row); + addButton(i18n.getString("treeViewButton"), 35, row, new TAction() { public void DO() { try { @@ -187,8 +205,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Terminal", 1, row); - addButton("Termi&nal", 35, row, + addLabel(i18n.getString("terminalLabel"), 1, row); + addButton(i18n.getString("terminalButton"), 35, row, new TAction() { public void DO() { getApplication().openTerminal(0, 0); @@ -197,8 +215,8 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Color editor", 1, row); - addButton("Co&lors", 35, row, + addLabel(i18n.getString("colorEditorLabel"), 1, row); + addButton(i18n.getString("colorEditorButton"), 35, row, new TAction() { public void DO() { new TEditColorThemeWindow(getApplication()); @@ -209,12 +227,13 @@ public class DemoMainWindow extends TWindow { progressBar = addProgressBar(1, row, 22, 0); row++; - timerLabel = addLabel("Timer", 1, row); + timerLabel = addLabel(i18n.getString("timerLabel"), 1, row); timer = getApplication().addTimer(250, true, new TAction() { public void DO() { - timerLabel.setLabel(String.format("Timer: %d", timerI)); + timerLabel.setLabel(String.format(i18n. + getString("timerText"), timerI)); timerLabel.setWidth(timerLabel.getLabel().length()); if (timerI < 100) { timerI++; @@ -255,11 +274,15 @@ public class DemoMainWindow extends TWindow { activate(first); - statusBar = newStatusBar("Demo Main Window"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } // ------------------------------------------------------------------------ @@ -288,15 +311,18 @@ public class DemoMainWindow extends TWindow { String filename = fileOpenBox("."); if (filename != null) { try { - new TEditorWindow(getApplication(), new File(filename)); + new TEditorWindow(getApplication(), + new File(filename)); } catch (IOException e) { - messageBox("Error", "Error reading file: " + - e.getMessage()); + messageBox(i18n.getString("errorTitle"), + MessageFormat.format(i18n. + getString("errorReadingFile"), e.getMessage())); } } } catch (IOException e) { - messageBox("Error", "Error opening file dialog: " + - e.getMessage()); + messageBox(i18n.getString("errorTitle"), + MessageFormat.format(i18n. + getString("errorOpeningFile"), e.getMessage())); } return; } diff --git a/src/jexer/demos/DemoMainWindow.properties b/src/jexer/demos/DemoMainWindow.properties new file mode 100644 index 0000000..4a512cb --- /dev/null +++ b/src/jexer/demos/DemoMainWindow.properties @@ -0,0 +1,33 @@ +windowTitle=Demo Window + +statusBar=Demo Main Window +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +messageBoxLabel=Message Boxes +messageBoxButton=&MessageBoxes +openModalLabel=Open me as modal +openModalButton=W&indow +textFieldLabel=Text fields and calendar +textFieldButton=Field&s +radioButtonLabel=Radio buttons, check and combobox +radioButtonButton=&CheckBoxes +editorLabel=Editor window +editorButton1=&1 Widget +editorButton2=&2 Window +textAreaLabel=Text areas +textAreaButton=&Text +treeViewLabel=Tree views +treeViewButton=Tree&View +terminalLabel=Terminal +terminalButton=Termi&nal +colorEditorLabel=Color editor +colorEditorButton=Co&lors +timerLabel=Timer +timerText=Timer: %d + +errorTitle=Error +errorReadingFile=Error reading file: {0} +errorOpeningFile=Error opening file dialog: {0} diff --git a/src/jexer/demos/DemoMsgBoxWindow.java b/src/jexer/demos/DemoMsgBoxWindow.java index ee2bca1..c2d4ff6 100644 --- a/src/jexer/demos/DemoMsgBoxWindow.java +++ b/src/jexer/demos/DemoMsgBoxWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,7 +28,14 @@ */ package jexer.demos; -import jexer.*; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TInputBox; +import jexer.TMessageBox; +import jexer.TWindow; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -37,6 +44,11 @@ import static jexer.TKeypress.*; */ public class DemoMsgBoxWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMsgBoxWindow.class.getName()); + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -59,120 +71,101 @@ public class DemoMsgBoxWindow extends TWindow { DemoMsgBoxWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it // will be centered on screen. - super(parent, "Message Boxes", 0, 0, 64, 18, flags); + super(parent, i18n.getString("windowTitle"), 0, 0, 64, 18, flags); int row = 1; // Add some widgets - addLabel("Default OK message box", 1, row); - addButton("Open O&K MB", 35, row, + addLabel(i18n.getString("messageBoxLabel1"), 1, row); + addButton(i18n.getString("messageBoxButton1"), 35, row, new TAction() { public void DO() { - getApplication().messageBox("OK MessageBox", -"This is an example of a OK MessageBox. This is the\n" + -"default MessageBox.\n" + -"\n" + -"Note that the MessageBox text can span multiple\n" + -"lines.\n" + -"\n" + -"The default result (if someone hits the top-left\n" + -"close button) is OK.\n", + getApplication().messageBox(i18n. + getString("messageBoxTitle1"), + i18n.getString("messageBoxPrompt1"), TMessageBox.Type.OK); } } ); row += 2; - addLabel("OK/Cancel message box", 1, row); - addButton("O&pen OKC MB", 35, row, + addLabel(i18n.getString("messageBoxLabel2"), 1, row); + addButton(i18n.getString("messageBoxButton2"), 35, row, new TAction() { public void DO() { - getApplication().messageBox("OK/Cancel MessageBox", -"This is an example of a OK/Cancel MessageBox.\n" + -"\n" + -"Note that the MessageBox text can span multiple\n" + -"lines.\n" + -"\n" + -"The default result (if someone hits the top-left\n" + -"close button) is CANCEL.\n", + getApplication().messageBox(i18n. + getString("messageBoxTitle2"), + i18n.getString("messageBoxPrompt2"), TMessageBox.Type.OKCANCEL); } } ); row += 2; - addLabel("Yes/No message box", 1, row); - addButton("Open &YN MB", 35, row, + addLabel(i18n.getString("messageBoxLabel3"), 1, row); + addButton(i18n.getString("messageBoxButton3"), 35, row, new TAction() { public void DO() { - getApplication().messageBox("Yes/No MessageBox", -"This is an example of a Yes/No MessageBox.\n" + -"\n" + -"Note that the MessageBox text can span multiple\n" + -"lines.\n" + -"\n" + -"The default result (if someone hits the top-left\n" + -"close button) is NO.\n", + getApplication().messageBox(i18n. + getString("messageBoxTitle3"), + i18n.getString("messageBoxPrompt3"), TMessageBox.Type.YESNO); } } ); row += 2; - addLabel("Yes/No/Cancel message box", 1, row); - addButton("Ope&n YNC MB", 35, row, + addLabel(i18n.getString("messageBoxLabel4"), 1, row); + addButton(i18n.getString("messageBoxButton4"), 35, row, new TAction() { public void DO() { - getApplication().messageBox("Yes/No/Cancel MessageBox", -"This is an example of a Yes/No/Cancel MessageBox.\n" + -"\n" + -"Note that the MessageBox text can span multiple\n" + -"lines.\n" + -"\n" + -"The default result (if someone hits the top-left\n" + -"close button) is CANCEL.\n", + getApplication().messageBox(i18n. + getString("messageBoxTitle4"), + i18n.getString("messageBoxPrompt4"), TMessageBox.Type.YESNOCANCEL); } } ); row += 2; - addLabel("Input box 1", 1, row); - addButton("Open &input box", 35, row, + addLabel(i18n.getString("inputBoxLabel1"), 1, row); + addButton(i18n.getString("inputBoxButton1"), 35, row, new TAction() { public void DO() { - TInputBox in = getApplication().inputBox("Input Box", -"This is an example of an InputBox.\n" + -"\n" + -"Note that the InputBox text can span multiple\n" + -"lines.\n", - "some input text"); - getApplication().messageBox("Your InputBox Answer", - "You entered: " + in.getText()); + TInputBox in = getApplication().inputBox(i18n. + getString("inputBoxTitle1"), + i18n.getString("inputBoxPrompt1"), + i18n.getString("inputBoxInput1")); + getApplication().messageBox(i18n. + getString("inputBoxAnswerTitle1"), + MessageFormat.format(i18n. + getString("inputBoxAnswerPrompt1"), in.getText())); } } ); row += 2; - addLabel("Input box 2", 1, row); - addButton("Cance&llable input box", 35, row, + addLabel(i18n.getString("inputBoxLabel2"), 1, row); + addButton(i18n.getString("inputBoxButton2"), 35, row, new TAction() { public void DO() { - TInputBox in = getApplication().inputBox("Input Box", -"This is an example of an InputBox.\n" + -"\n" + -"Note that the InputBox text can span multiple\n" + -"lines.\n" + -"This one has both OK and Cancel buttons.\n", - "some input text", TInputBox.Type.OKCANCEL); - getApplication().messageBox("Your InputBox Answer", - "You entered: " + in.getText() + " and pressed " + - in.getResult()); + TInputBox in = getApplication().inputBox(i18n. + getString("inputBoxTitle2"), + i18n.getString("inputBoxPrompt2"), + i18n.getString("inputBoxInput2"), + TInputBox.Type.OKCANCEL); + getApplication().messageBox(i18n. + getString("inputBoxAnswerTitle2"), + MessageFormat.format(i18n. + getString("inputBoxAnswerPrompt2"), in.getText(), + in.getResult())); } } ); + row += 2; - addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4, + addButton(i18n.getString("closeWindow"), + (getWidth() - 14) / 2, getHeight() - 4, new TAction() { public void DO() { getApplication().closeWindow(DemoMsgBoxWindow.this); @@ -180,10 +173,14 @@ public class DemoMsgBoxWindow extends TWindow { } ); - statusBar = newStatusBar("Message boxes"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } } diff --git a/src/jexer/demos/DemoMsgBoxWindow.properties b/src/jexer/demos/DemoMsgBoxWindow.properties new file mode 100644 index 0000000..47a858a --- /dev/null +++ b/src/jexer/demos/DemoMsgBoxWindow.properties @@ -0,0 +1,45 @@ +windowTitle=Message Boxes + +statusBar=Message boxes +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +messageBoxLabel1=Default OK message box +messageBoxButton1=Open O&K MB +messageBoxTitle1=OK MessageBox +messageBoxPrompt1=This is an example of a OK MessageBox. This is the\ndefault MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is OK.\n + +messageBoxLabel2=OK/Cancel message box +messageBoxButton2=O&pen OKC MB +messageBoxTitle2=OK/Cancel MessageBox +messageBoxPrompt2=This is an example of a OK/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-leftclose button) is CANCEL.\n + +messageBoxLabel3=Yes/No message box +messageBoxButton3=Open &YN MB +messageBoxTitle3=Yes/No MessageBox +messageBoxPrompt3=This is an example of a Yes/No MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is NO.\n + +messageBoxLabel4=Yes/No/Cancel message box +messageBoxButton4=Ope&n YNC MB +messageBoxTitle4=Yes/No/Cancel MessageBox +messageBoxPrompt4=This is an example of a Yes/No/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is CANCEL.\n + +inputBoxLabel1=Input box 1 +inputBoxButton1=Open &input box +inputBoxTitle1=Input Box +inputBoxPrompt1=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\n +inputBoxInput1=some input text +inputBoxAnswerTitle1=Your InputBox Answer +inputBoxAnswerPrompt1=You entered: {0} + +inputBoxLabel2=Input box 2 +inputBoxButton2=Cance&llable input box +inputBoxTitle2=Input Box +inputBoxPrompt2=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\nThis one has both OK and Cancel buttons.\n +inputBoxInput2=some input text +inputBoxAnswerTitle2=Your InputBox Answer +inputBoxAnswerPrompt2=You entered: {0} and pressed {1} + +closeWindow=&Close Window diff --git a/src/jexer/demos/DemoTextFieldWindow.java b/src/jexer/demos/DemoTextFieldWindow.java index 59dee82..196f3b9 100644 --- a/src/jexer/demos/DemoTextFieldWindow.java +++ b/src/jexer/demos/DemoTextFieldWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,9 +28,16 @@ */ package jexer.demos; -import java.util.*; +import java.text.MessageFormat; +import java.util.Date; +import java.util.ResourceBundle; -import jexer.*; +import jexer.TAction; +import jexer.TApplication; +import jexer.TCalendar; +import jexer.TField; +import jexer.TMessageBox; +import jexer.TWindow; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -39,6 +46,11 @@ import static jexer.TKeypress.*; */ public class DemoTextFieldWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextFieldWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -71,37 +83,36 @@ public class DemoTextFieldWindow extends TWindow { DemoTextFieldWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it // will be centered on screen. - super(parent, "Text Fields", 0, 0, 60, 20, flags); + super(parent, i18n.getString("windowTitle"), 0, 0, 60, 20, flags); int row = 1; - addLabel("Variable-width text field:", 1, row); + addLabel(i18n.getString("textField1"), 1, row); addField(35, row++, 15, false, "Field text"); - addLabel("Fixed-width text field:", 1, row); + addLabel(i18n.getString("textField2"), 1, row); addField(35, row++, 15, true); - addLabel("Variable-width password:", 1, row); + addLabel(i18n.getString("textField3"), 1, row); addPasswordField(35, row++, 15, false); - addLabel("Fixed-width password:", 1, row); + addLabel(i18n.getString("textField4"), 1, row); addPasswordField(35, row++, 15, true, "hunter2"); - addLabel("Very long text field:", 1, row); + addLabel(i18n.getString("textField5"), 1, row); TField selected = addField(35, row++, 40, false, - "Very very long field text that should be outside the window"); + i18n.getString("textField6")); row += 1; calendar = addCalendar(1, row++, new TAction() { public void DO() { - getApplication().messageBox("Calendar", - "You selected the following date:\n" + - "\n" + - new Date(calendar.getValue().getTimeInMillis()) + - "\n", + getApplication().messageBox(i18n.getString("calendarTitle"), + MessageFormat.format(i18n.getString("calendarMessage"), + new Date(calendar.getValue().getTimeInMillis())), TMessageBox.Type.OK); } } ); - addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4, + addButton(i18n.getString("closeWindow"), + (getWidth() - 14) / 2, getHeight() - 4, new TAction() { public void DO() { getApplication().closeWindow(DemoTextFieldWindow.this); @@ -111,11 +122,15 @@ public class DemoTextFieldWindow extends TWindow { activate(selected); - statusBar = newStatusBar("Text fields"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } } diff --git a/src/jexer/demos/DemoTextFieldWindow.properties b/src/jexer/demos/DemoTextFieldWindow.properties new file mode 100644 index 0000000..5b42990 --- /dev/null +++ b/src/jexer/demos/DemoTextFieldWindow.properties @@ -0,0 +1,17 @@ +windowTitle=Text Fields + +statusBar=Text fields +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +textField1=Variable-width text field: +textField2=Fixed-width text field: +textField3=Variable-width password: +textField4=Fixed-width password: +textField5=Very long text field: +textField6=Very very long field text that should be outside the window +calendarTitle=Calendar +calendarMessage=You selected the following date:\n\n{0}\n +closeWindow=&Close Window diff --git a/src/jexer/demos/DemoTextWindow.java b/src/jexer/demos/DemoTextWindow.java index 969ca63..afa2a0e 100644 --- a/src/jexer/demos/DemoTextWindow.java +++ b/src/jexer/demos/DemoTextWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,9 +28,15 @@ */ package jexer.demos; -import jexer.*; -import jexer.event.*; -import jexer.menu.*; +import java.util.ResourceBundle; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TText; +import jexer.TWidget; +import jexer.TWindow; +import jexer.event.TResizeEvent; +import jexer.menu.TMenu; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -39,6 +45,11 @@ import static jexer.TKeypress.*; */ public class DemoTextWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -65,35 +76,39 @@ public class DemoTextWindow extends TWindow { super(parent, title, 0, 0, 44, 22, RESIZABLE); textField = addText(text, 1, 3, 40, 16); - addButton("&Left", 1, 1, new TAction() { + addButton(i18n.getString("left"), 1, 1, new TAction() { public void DO() { textField.leftJustify(); } }); - addButton("&Center", 10, 1, new TAction() { + addButton(i18n.getString("center"), 10, 1, new TAction() { public void DO() { textField.centerJustify(); } }); - addButton("&Right", 21, 1, new TAction() { + addButton(i18n.getString("right"), 21, 1, new TAction() { public void DO() { textField.rightJustify(); } }); - addButton("&Full", 31, 1, new TAction() { + addButton(i18n.getString("full"), 31, 1, new TAction() { public void DO() { textField.fullJustify(); } }); - statusBar = newStatusBar("Reflowable text window"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } /** @@ -102,7 +117,7 @@ public class DemoTextWindow extends TWindow { * @param parent the main application */ public DemoTextWindow(final TApplication parent) { - this(parent, "Text Area", + this(parent, i18n.getString("windowTitle"), "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" + diff --git a/src/jexer/demos/DemoTextWindow.properties b/src/jexer/demos/DemoTextWindow.properties new file mode 100644 index 0000000..873a56f --- /dev/null +++ b/src/jexer/demos/DemoTextWindow.properties @@ -0,0 +1,12 @@ +windowTitle=Text Area + +statusBar=Reflowable text window +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +left=&Left +center=&Center +right=&Right +full=&Full diff --git a/src/jexer/demos/DemoTreeViewWindow.java b/src/jexer/demos/DemoTreeViewWindow.java index e31ef76..4798951 100644 --- a/src/jexer/demos/DemoTreeViewWindow.java +++ b/src/jexer/demos/DemoTreeViewWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -29,10 +29,14 @@ package jexer.demos; import java.io.IOException; +import java.util.ResourceBundle; -import jexer.*; -import jexer.event.*; -import jexer.ttree.*; +import jexer.TApplication; +import jexer.TWidget; +import jexer.TWindow; +import jexer.event.TResizeEvent; +import jexer.ttree.TDirectoryTreeItem; +import jexer.ttree.TTreeViewWidget; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -41,6 +45,11 @@ import static jexer.TKeypress.*; */ public class DemoTreeViewWindow extends TWindow { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTreeViewWindow.class.getName()); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -61,17 +70,22 @@ public class DemoTreeViewWindow extends TWindow { * @throws IOException if a java.io operation throws */ public DemoTreeViewWindow(final TApplication parent) throws IOException { - super(parent, "Tree View", 0, 0, 44, 16, TWindow.RESIZABLE); + super(parent, i18n.getString("windowTitle"), 0, 0, 44, 16, + TWindow.RESIZABLE); // Load the treeview with "stuff" treeView = addTreeViewWidget(1, 1, 40, 12); new TDirectoryTreeItem(treeView, ".", true); - statusBar = newStatusBar("Treeview demonstration"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); - statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); - statusBar.addShortcutKeypress(kbF3, cmOpen, "Open"); - statusBar.addShortcutKeypress(kbF10, cmExit, "Exit"); + statusBar = newStatusBar(i18n.getString("statusBar")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF2, cmShell, + i18n.getString("statusBarShell")); + statusBar.addShortcutKeypress(kbF3, cmOpen, + i18n.getString("statusBarOpen")); + statusBar.addShortcutKeypress(kbF10, cmExit, + i18n.getString("statusBarExit")); } // ------------------------------------------------------------------------ diff --git a/src/jexer/demos/DemoTreeViewWindow.properties b/src/jexer/demos/DemoTreeViewWindow.properties new file mode 100644 index 0000000..d63b24e --- /dev/null +++ b/src/jexer/demos/DemoTreeViewWindow.properties @@ -0,0 +1,7 @@ +windowTitle=Tree View + +statusBar=Treeview demonstration +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit diff --git a/src/jexer/demos/DesktopDemo.java b/src/jexer/demos/DesktopDemo.java index 93e8597..520f5b0 100644 --- a/src/jexer/demos/DesktopDemo.java +++ b/src/jexer/demos/DesktopDemo.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,7 +28,6 @@ */ package jexer.demos; - import jexer.*; /** diff --git a/src/jexer/demos/DesktopDemoApplication.java b/src/jexer/demos/DesktopDemoApplication.java index 0393860..60ef7ec 100644 --- a/src/jexer/demos/DesktopDemoApplication.java +++ b/src/jexer/demos/DesktopDemoApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -28,18 +28,27 @@ */ package jexer.demos; -import java.io.*; -import java.util.*; +import java.io.File; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.Scanner; -import jexer.*; -import jexer.event.*; -import jexer.menu.*; +import jexer.TAction; +import jexer.TApplication; +import jexer.TWindow; +import jexer.event.TMenuEvent; +import jexer.menu.TMenu; /** * The demo application itself. */ public class DesktopDemoApplication extends TApplication { + /** + * Translated strings. + */ + private static ResourceBundle i18n = ResourceBundle.getBundle(DesktopDemoApplication.class.getName()); + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -53,7 +62,7 @@ public class DesktopDemoApplication extends TApplication { public DesktopDemoApplication(final BackendType backendType) throws Exception { super(backendType); addAllWidgets(); - getBackend().setTitle("Jexer Demo Application"); + getBackend().setTitle(i18n.getString("applicationTitle")); } // ------------------------------------------------------------------------ @@ -119,14 +128,14 @@ public class DesktopDemoApplication extends TApplication { final DesktopDemo desktop = new DesktopDemo(this); setDesktop(desktop); - desktop.addButton("&Remove HATCH", 2, 5, + desktop.addButton(i18n.getString("removeHatch"), 2, 5, new TAction() { public void DO() { desktop.drawHatch = false; } } ); - desktop.addButton("&Show HATCH", 2, 8, + desktop.addButton(i18n.getString("showHatch"), 2, 8, new TAction() { public void DO() { desktop.drawHatch = true; @@ -134,58 +143,60 @@ public class DesktopDemoApplication extends TApplication { } ); - final TWindow windowA = addWindow("Window A", 25, 14); - final TWindow windowB = addWindow("Window B", 25, 14); - windowA.addButton("&Show Window B", 2, 2, + final TWindow windowA = addWindow(i18n.getString("windowATitle"), + 25, 14); + final TWindow windowB = addWindow(i18n.getString("windowBTitle"), + 25, 14); + windowA.addButton(i18n.getString("showWindowB"), 2, 2, new TAction() { public void DO() { windowB.show(); } } ); - windowA.addButton("H&ide Window B", 2, 4, + windowA.addButton(i18n.getString("hideWindowB"), 2, 4, new TAction() { public void DO() { windowB.hide(); } } ); - windowA.addButton("&Maximize Window B", 2, 6, + windowA.addButton(i18n.getString("maximizeWindowB"), 2, 6, new TAction() { public void DO() { windowB.maximize(); } } ); - windowA.addButton("&Restore Window B", 2, 8, + windowA.addButton(i18n.getString("restoreWindowB"), 2, 8, new TAction() { public void DO() { windowB.restore(); } } ); - windowB.addButton("&Show Window A", 2, 2, + windowB.addButton(i18n.getString("showWindowA"), 2, 2, new TAction() { public void DO() { windowA.show(); } } ); - windowB.addButton("H&ide Window A", 2, 4, + windowB.addButton(i18n.getString("hideWindowA"), 2, 4, new TAction() { public void DO() { windowA.hide(); } } ); - windowB.addButton("&Maximize Window A", 2, 6, + windowB.addButton(i18n.getString("maximizeWindowA"), 2, 6, new TAction() { public void DO() { windowA.maximize(); } } ); - windowB.addButton("&Restore Window A", 2, 8, + windowB.addButton(i18n.getString("restoreWindowA"), 2, 8, new TAction() { public void DO() { windowA.restore(); @@ -193,40 +204,41 @@ public class DesktopDemoApplication extends TApplication { } ); - desktop.addButton("S&how Window B", 25, 2, + desktop.addButton(i18n.getString("showWindowB"), 25, 2, new TAction() { public void DO() { windowB.show(); } } ); - desktop.addButton("H&ide Window B", 25, 5, + desktop.addButton(i18n.getString("hideWindowB"), 25, 5, new TAction() { public void DO() { windowB.hide(); } } ); - desktop.addButton("Sh&ow Window A", 25, 8, + desktop.addButton(i18n.getString("showWindowA"), 25, 8, new TAction() { public void DO() { windowA.show(); } } ); - desktop.addButton("Hid&e Window A", 25, 11, + desktop.addButton(i18n.getString("hideWindowA"), 25, 11, new TAction() { public void DO() { windowA.hide(); } } ); - desktop.addButton("&Create Window C", 25, 15, + desktop.addButton(i18n.getString("createWindowC"), 25, 15, new TAction() { public void DO() { final TWindow windowC = desktop.getApplication().addWindow( - "Window C", 30, 20, TWindow.NOCLOSEBOX); - windowC.addButton("&Close Me", 5, 5, + i18n.getString("windowCTitle"), 30, 20, + TWindow.NOCLOSEBOX); + windowC.addButton(i18n.getString("closeMe"), 5, 5, new TAction() { public void DO() { windowC.close(); @@ -237,21 +249,20 @@ public class DesktopDemoApplication extends TApplication { } ); - desktop.addButton("Enable focusFollowsMouse", 25, 18, + desktop.addButton(i18n.getString("enableFFM"), 25, 18, new TAction() { public void DO() { DesktopDemoApplication.this.setFocusFollowsMouse(true); } } ); - desktop.addButton("Disable focusFollowsMouse", 25, 21, + desktop.addButton(i18n.getString("disableFFM"), 25, 21, new TAction() { public void DO() { DesktopDemoApplication.this.setFocusFollowsMouse(false); } } ); - } } diff --git a/src/jexer/demos/DesktopDemoApplication.properties b/src/jexer/demos/DesktopDemoApplication.properties new file mode 100644 index 0000000..85f7435 --- /dev/null +++ b/src/jexer/demos/DesktopDemoApplication.properties @@ -0,0 +1,19 @@ +applicationTitle=Demo Application + +removeHatch=Remove HATCH +showHatch=Show HATCH +closeMe=Close Me +createWindowC=Create Window C +disableFFM=Disable focusFollowsMouse +enableFFM=Enable focusFollowsMouse +hideWindowA=Hide Window A +hideWindowB=Hide Window B +maximizeWindowA=Maximize Window A +maximizeWindowB=Maximize Window B +restoreWindowA=Restore Window A +restoreWindowB=Restore Window B +showWindowA=Show Window A +showWindowB=Show Window B +windowATitle=Window A +windowBTitle=Window B +windowCTitle=Window C diff --git a/src/jexer/demos/package-info.java b/src/jexer/demos/package-info.java index 45ce7d4..1305cdd 100644 --- a/src/jexer/demos/package-info.java +++ b/src/jexer/demos/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TCommandEvent.java b/src/jexer/event/TCommandEvent.java index 53b9ad2..60f6385 100644 --- a/src/jexer/event/TCommandEvent.java +++ b/src/jexer/event/TCommandEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TInputEvent.java b/src/jexer/event/TInputEvent.java index d03ef01..220512f 100644 --- a/src/jexer/event/TInputEvent.java +++ b/src/jexer/event/TInputEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TKeypressEvent.java b/src/jexer/event/TKeypressEvent.java index f499369..b56f08b 100644 --- a/src/jexer/event/TKeypressEvent.java +++ b/src/jexer/event/TKeypressEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TMenuEvent.java b/src/jexer/event/TMenuEvent.java index 5117846..e2ff7c7 100644 --- a/src/jexer/event/TMenuEvent.java +++ b/src/jexer/event/TMenuEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TMouseEvent.java b/src/jexer/event/TMouseEvent.java index 8c967c0..496d8bc 100644 --- a/src/jexer/event/TMouseEvent.java +++ b/src/jexer/event/TMouseEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/TResizeEvent.java b/src/jexer/event/TResizeEvent.java index 472ef3e..ff95710 100644 --- a/src/jexer/event/TResizeEvent.java +++ b/src/jexer/event/TResizeEvent.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/event/package-info.java b/src/jexer/event/package-info.java index dce2c90..e4541a3 100644 --- a/src/jexer/event/package-info.java +++ b/src/jexer/event/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/io/ReadTimeoutException.java b/src/jexer/io/ReadTimeoutException.java index 143845a..8c6371e 100644 --- a/src/jexer/io/ReadTimeoutException.java +++ b/src/jexer/io/ReadTimeoutException.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/io/TimeoutInputStream.java b/src/jexer/io/TimeoutInputStream.java index d654262..3d8cdb0 100644 --- a/src/jexer/io/TimeoutInputStream.java +++ b/src/jexer/io/TimeoutInputStream.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -179,8 +179,11 @@ public class TimeoutInputStream extends InputStream { // available. If not, we throw ReadTimeoutException. long checkTime = System.currentTimeMillis(); while (stream.available() == 0) { - long now = System.currentTimeMillis(); + if (remaining > 0) { + return (b.length - remaining); + } + long now = System.currentTimeMillis(); synchronized (this) { if ((now - checkTime > timeoutMillis) || (cancel == true)) { if (cancel == true) { @@ -257,6 +260,10 @@ public class TimeoutInputStream extends InputStream { // available. If not, we throw ReadTimeoutException. long checkTime = System.currentTimeMillis(); while (stream.available() == 0) { + if (remaining > 0) { + return (len - remaining); + } + long now = System.currentTimeMillis(); synchronized (this) { if ((now - checkTime > timeoutMillis) || (cancel == true)) { diff --git a/src/jexer/io/package-info.java b/src/jexer/io/package-info.java index 4c04cf1..37ad2bb 100644 --- a/src/jexer/io/package-info.java +++ b/src/jexer/io/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 3120635..bfda602 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -161,6 +161,7 @@ public class TMenu extends TWindow { @Override public void onMouseDown(final TMouseEvent mouse) { this.mouse = mouse; + super.onMouseDown(mouse); // Pass to children for (TWidget widget: getChildren()) { @@ -341,7 +342,7 @@ public class TMenu extends TWindow { hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background); // Draw a shadow - getScreen().drawBoxShadow(0, 0, getWidth(), getHeight()); + drawBoxShadow(0, 0, getWidth(), getHeight()); } // ------------------------------------------------------------------------ @@ -387,6 +388,21 @@ public class TMenu extends TWindow { return addItemInternal(id, label, null); } + /** + * Convenience function to add a menu item. + * + * @param id menu item ID. Must be greater than 1024. + * @param label menu item label + * @param enabled default state for enabled + * @return the new menu item + */ + public TMenuItem addItem(final int id, final String label, + final boolean enabled) { + + assert (id >= 1024); + return addItemInternal(id, label, null, enabled); + } + /** * Convenience function to add a custom menu item. * diff --git a/src/jexer/menu/TMenuItem.java b/src/jexer/menu/TMenuItem.java index 63a5355..6e455d8 100644 --- a/src/jexer/menu/TMenuItem.java +++ b/src/jexer/menu/TMenuItem.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -220,23 +220,23 @@ public class TMenuItem extends TWidget { } char cVSide = GraphicsChars.WINDOW_SIDE; - getScreen().vLineXY(0, 0, 1, cVSide, background); - getScreen().vLineXY(getWidth() - 1, 0, 1, cVSide, background); + vLineXY(0, 0, 1, cVSide, background); + vLineXY(getWidth() - 1, 0, 1, cVSide, background); - getScreen().hLineXY(1, 0, getWidth() - 2, ' ', menuColor); - getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), menuColor); + hLineXY(1, 0, getWidth() - 2, ' ', menuColor); + putStringXY(2, 0, mnemonic.getRawLabel(), menuColor); if (key != null) { String keyLabel = key.toString(); - getScreen().putStringXY((getWidth() - keyLabel.length() - 2), 0, - keyLabel, menuColor); + putStringXY((getWidth() - keyLabel.length() - 2), 0, keyLabel, + menuColor); } if (mnemonic.getShortcutIdx() >= 0) { - getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0, - mnemonic.getShortcut(), menuMnemonicColor); + putCharXY(2 + mnemonic.getShortcutIdx(), 0, mnemonic.getShortcut(), + menuMnemonicColor); } if (checked) { assert (checkable); - getScreen().putCharXY(1, 0, GraphicsChars.CHECK, menuColor); + putCharXY(1, 0, GraphicsChars.CHECK, menuColor); } } diff --git a/src/jexer/menu/TMenuSeparator.java b/src/jexer/menu/TMenuSeparator.java index daae13a..0528e5d 100644 --- a/src/jexer/menu/TMenuSeparator.java +++ b/src/jexer/menu/TMenuSeparator.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -65,11 +65,9 @@ public class TMenuSeparator extends TMenuItem { public void draw() { CellAttributes background = getTheme().getColor("tmenu"); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0xC3], background); - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], - background); - getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR, - background); + putCharXY(0, 0, GraphicsChars.CP437[0xC3], background); + putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], background); + hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR, background); } } diff --git a/src/jexer/menu/TSubMenu.java b/src/jexer/menu/TSubMenu.java index b61ca83..547711b 100644 --- a/src/jexer/menu/TSubMenu.java +++ b/src/jexer/menu/TSubMenu.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -163,8 +163,7 @@ public class TSubMenu extends TMenuItem { } // Add the arrow - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], - menuColor); + putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], menuColor); } /** diff --git a/src/jexer/menu/package-info.java b/src/jexer/menu/package-info.java index 6781106..2c10393 100644 --- a/src/jexer/menu/package-info.java +++ b/src/jexer/menu/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/net/TelnetInputStream.java b/src/jexer/net/TelnetInputStream.java index 1764b88..056a7dc 100644 --- a/src/jexer/net/TelnetInputStream.java +++ b/src/jexer/net/TelnetInputStream.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/net/TelnetOutputStream.java b/src/jexer/net/TelnetOutputStream.java index 994655c..905c52a 100644 --- a/src/jexer/net/TelnetOutputStream.java +++ b/src/jexer/net/TelnetOutputStream.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/net/TelnetServerSocket.java b/src/jexer/net/TelnetServerSocket.java index b368d34..3c5b307 100644 --- a/src/jexer/net/TelnetServerSocket.java +++ b/src/jexer/net/TelnetServerSocket.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/net/TelnetSocket.java b/src/jexer/net/TelnetSocket.java index 34e715f..ac8a278 100644 --- a/src/jexer/net/TelnetSocket.java +++ b/src/jexer/net/TelnetSocket.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/net/package-info.java b/src/jexer/net/package-info.java index 29ff720..5d738fb 100644 --- a/src/jexer/net/package-info.java +++ b/src/jexer/net/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/package-info.java b/src/jexer/package-info.java index 46c48fa..7a5a752 100644 --- a/src/jexer/package-info.java +++ b/src/jexer/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java index bd8e91f..9b04538 100644 --- a/src/jexer/teditor/Document.java +++ b/src/jexer/teditor/Document.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/teditor/Highlighter.java b/src/jexer/teditor/Highlighter.java index 4a1ae90..4f60155 100644 --- a/src/jexer/teditor/Highlighter.java +++ b/src/jexer/teditor/Highlighter.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java index 400de9a..965c38f 100644 --- a/src/jexer/teditor/Line.java +++ b/src/jexer/teditor/Line.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java index 11fa39d..ffb11aa 100644 --- a/src/jexer/teditor/Word.java +++ b/src/jexer/teditor/Word.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/teditor/package-info.java b/src/jexer/teditor/package-info.java index 38af57d..8bf5199 100644 --- a/src/jexer/teditor/package-info.java +++ b/src/jexer/teditor/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/tterminal/DECCharacterSets.java b/src/jexer/tterminal/DECCharacterSets.java index 5110db0..bca81bb 100644 --- a/src/jexer/tterminal/DECCharacterSets.java +++ b/src/jexer/tterminal/DECCharacterSets.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/tterminal/DisplayLine.java b/src/jexer/tterminal/DisplayLine.java index 66036d7..7c76713 100644 --- a/src/jexer/tterminal/DisplayLine.java +++ b/src/jexer/tterminal/DisplayLine.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/tterminal/DisplayListener.java b/src/jexer/tterminal/DisplayListener.java index 7a47921..d0c9e2d 100644 --- a/src/jexer/tterminal/DisplayListener.java +++ b/src/jexer/tterminal/DisplayListener.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index 9376828..b08a18d 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -29,17 +29,18 @@ package jexer.tterminal; import java.io.BufferedOutputStream; +import java.io.CharArrayWriter; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import jexer.TKeypress; @@ -252,12 +253,17 @@ public class ECMA48 implements Runnable { /** * The scrollback buffer characters + attributes. */ - private volatile List scrollback; + private volatile ArrayList scrollback; /** * The raw display buffer characters + attributes. */ - private volatile List display; + private volatile ArrayList display; + + /** + * The maximum number of lines in the scrollback buffer. + */ + private int maxScrollback = 10000; /** * The terminal's input. For type == XTERM, this is an InputStreamReader @@ -585,8 +591,8 @@ public class ECMA48 implements Runnable { csiParams = new ArrayList(); tabStops = new ArrayList(); - scrollback = new LinkedList(); - display = new LinkedList(); + scrollback = new ArrayList(); + display = new ArrayList(); this.type = type; if (inputStream instanceof TimeoutInputStream) { @@ -697,7 +703,7 @@ public class ECMA48 implements Runnable { ch = readBuffer[i]; } - consume((char)ch); + consume((char) ch); } } // Permit my enclosing UI to know that I updated. @@ -707,8 +713,29 @@ public class ECMA48 implements Runnable { } // System.err.println("end while loop"); System.err.flush(); } catch (IOException e) { - e.printStackTrace(); done = true; + + // This is an unusual case. We want to see the stack trace, + // but it is related to the spawned process rather than the + // actual UI. We will generate the stack trace, and consume + // it as though it was emitted by the shell. + CharArrayWriter writer= new CharArrayWriter(); + // Send a ST and RIS to clear the emulator state. + try { + writer.write("\033\\\033c"); + writer.write("\n-----------------------------------\n"); + e.printStackTrace(new PrintWriter(writer)); + writer.write("\n-----------------------------------\n"); + } catch (IOException e2) { + // SQUASH + } + char [] stackTrace = writer.toCharArray(); + for (int i = 0; i < stackTrace.length; i++) { + if (stackTrace[i] == '\n') { + consume('\r'); + } + consume(stackTrace[i]); + } } } // while ((done == false) && (stopReaderThread == false)) @@ -889,7 +916,7 @@ public class ECMA48 implements Runnable { try { readerThread.join(1000); } catch (InterruptedException e) { - e.printStackTrace(); + // SQUASH } } @@ -1234,7 +1261,12 @@ public class ECMA48 implements Runnable { private void newDisplayLine() { // Scroll the top line off into the scrollback buffer scrollback.add(display.get(0)); + if (scrollback.size() > maxScrollback) { + scrollback.remove(0); + scrollback.trimToSize(); + } display.remove(0); + display.trimToSize(); DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); display.add(line); @@ -2415,7 +2447,7 @@ public class ECMA48 implements Runnable { display.size()); List displayMiddle = display.subList(regionBottom + 1 - remaining, regionBottom + 1); - display = new LinkedList(displayTop); + display = new ArrayList(displayTop); display.addAll(displayMiddle); for (int i = 0; i < n; i++) { DisplayLine line = new DisplayLine(currentState.attr); @@ -2456,7 +2488,7 @@ public class ECMA48 implements Runnable { display.size()); List displayMiddle = display.subList(regionTop, regionTop + remaining); - display = new LinkedList(displayTop); + display = new ArrayList(displayTop); for (int i = 0; i < n; i++) { DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); diff --git a/src/jexer/tterminal/package-info.java b/src/jexer/tterminal/package-info.java index 93e1b3c..b92d153 100644 --- a/src/jexer/tterminal/package-info.java +++ b/src/jexer/tterminal/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/ttree/TDirectoryTreeItem.java b/src/jexer/ttree/TDirectoryTreeItem.java index 8c54b38..5f265ee 100644 --- a/src/jexer/ttree/TDirectoryTreeItem.java +++ b/src/jexer/ttree/TDirectoryTreeItem.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/ttree/TTreeItem.java b/src/jexer/ttree/TTreeItem.java index 901ce85..759bfb7 100644 --- a/src/jexer/ttree/TTreeItem.java +++ b/src/jexer/ttree/TTreeItem.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), @@ -248,7 +248,7 @@ public class TTreeItem extends TWidget { } // Blank out the background - getScreen().hLineXY(0, 0, getWidth(), ' ', color); + hLineXY(0, 0, getWidth(), ' ', color); String line = prefix; if (level > 0) { @@ -264,20 +264,17 @@ public class TTreeItem extends TWidget { line += " "; } } - getScreen().putStringXY(offset, 0, line, color); + putStringXY(offset, 0, line, color); if (selected) { - getScreen().putStringXY(offset + line.length(), 0, text, - selectedColor); + putStringXY(offset + line.length(), 0, text, selectedColor); } else { - getScreen().putStringXY(offset + line.length(), 0, text, textColor); + putStringXY(offset + line.length(), 0, text, textColor); } if ((level > 0) && (expandable)) { if (expanded) { - getScreen().putCharXY(offset + getExpanderX(), 0, '-', - expanderColor); + putCharXY(offset + getExpanderX(), 0, '-', expanderColor); } else { - getScreen().putCharXY(offset + getExpanderX(), 0, '+', - expanderColor); + putCharXY(offset + getExpanderX(), 0, '+', expanderColor); } } } diff --git a/src/jexer/ttree/TTreeView.java b/src/jexer/ttree/TTreeView.java index acc8924..88abd70 100644 --- a/src/jexer/ttree/TTreeView.java +++ b/src/jexer/ttree/TTreeView.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/ttree/TTreeViewWidget.java b/src/jexer/ttree/TTreeViewWidget.java index adb9a5d..ceb50ab 100644 --- a/src/jexer/ttree/TTreeViewWidget.java +++ b/src/jexer/ttree/TTreeViewWidget.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/ttree/TTreeViewWindow.java b/src/jexer/ttree/TTreeViewWindow.java index 2c64206..e570900 100644 --- a/src/jexer/ttree/TTreeViewWindow.java +++ b/src/jexer/ttree/TTreeViewWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), diff --git a/src/jexer/ttree/package-info.java b/src/jexer/ttree/package-info.java index 72dc8da..1e1fdfd 100644 --- a/src/jexer/ttree/package-info.java +++ b/src/jexer/ttree/package-info.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * 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"), -- 2.27.0