diff --git a/lc-pips-tc/build.gradle b/lc-pips-tc/build.gradle index 4ec1eef151edcb71da7f4d165bcd2cf40f3cf271..576a68802b6d9b559fa96bcea44ea25ad6839c06 100644 --- a/lc-pips-tc/build.gradle +++ b/lc-pips-tc/build.gradle @@ -11,7 +11,7 @@ repositories { dependencies { implementation project(':lc-mecha') - implementation 'com.googlecode.lanterna:lanterna:3.1.1' + implementation project(':mabe-lanterna') testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } diff --git a/lc-pips-tc/src/main/java/lc/pips/tc/AlternativeController.java b/lc-pips-tc/src/main/java/lc/pips/tc/AlternativeController.java index 739daee19134e71376eb78a476f77ff36661bb36..068533449f5e7e7f3a38df92d34118bb825644ac 100644 --- a/lc-pips-tc/src/main/java/lc/pips/tc/AlternativeController.java +++ b/lc-pips-tc/src/main/java/lc/pips/tc/AlternativeController.java @@ -1,6 +1,9 @@ package lc.pips.tc; import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.gui2.menu.Menu; +import com.googlecode.lanterna.gui2.menu.MenuBar; +import com.googlecode.lanterna.gui2.menu.MenuItem; import com.googlecode.lanterna.screen.Screen; import com.googlecode.lanterna.screen.TerminalScreen; import com.googlecode.lanterna.terminal.Terminal; @@ -9,8 +12,8 @@ import java.io.IOException; import java.util.ArrayList; /** - * This "alternative" implementation experiments with stripping down the Lanterna GUI implementation for the - * purposes of creating a simpler rendering environment. + * This "alternative" implementation experiments with overriding the default Lanterna rendering behavior to provide + * the desired screen positional environment. * * @author Alex Leigh * @since 1.0 @@ -26,20 +29,26 @@ public class AlternativeController implements Runnable { public void run() { try { screen.startScreen(); - MultiWindowTextGUI textGUI = new MultiWindowTextGUI(screen); + WindowManager mgr = new DefaultWindowManager(new ScreenDecorationRenderer(), null); + MultiWindowTextGUI textGUI = new MultiWindowTextGUI(new SameTextGUIThread.Factory(), screen, mgr); // textGUI.setTheme(LanternaThemes.getRegisteredTheme("businessmachine")); Panel contentArea = new Panel(); - final BasicWindow window = new BasicWindow(); + final Window window = new ScreenWindow(); + window.setMenuBar(new MenuBar().add(new Menu("Net").add(new MenuItem("Quit")))); ArrayList hints = new ArrayList<>(); hints.add(Window.Hint.FULL_SCREEN); // hints.add(Window.Hint.NO_POST_RENDERING); window.setHints(hints); - contentArea.setLayoutManager(new AbsoluteLayout()); + // window.setWindowPostRenderer(new ScreenDecorationRenderer()); + + // contentArea.setLayoutManager(new AbsoluteLayout()); + contentArea.addComponent(new Label("First: ")); + contentArea.addComponent(new TextBox("Hello World")); + contentArea.addComponent(new Label("Last: ")); contentArea.addComponent(new TextBox("Hello World")); window.setComponent(contentArea); textGUI.addWindowAndWait(window); - } catch (IOException e) { throw new RuntimeException(e); } diff --git a/lc-pips-tc/src/main/java/lc/pips/tc/ScreenDecorationRenderer.java b/lc-pips-tc/src/main/java/lc/pips/tc/ScreenDecorationRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..9b9050e5ccb5e2bc61c76873b7838e80eb7534e8 --- /dev/null +++ b/lc-pips-tc/src/main/java/lc/pips/tc/ScreenDecorationRenderer.java @@ -0,0 +1,126 @@ +package lc.pips.tc; + +import com.googlecode.lanterna.Symbols; +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TerminalTextUtils; +import com.googlecode.lanterna.graphics.ThemeDefinition; +import com.googlecode.lanterna.gui2.*; + +/** + * Implementation of {@link WindowDecorationRenderer} which provides a pre-canned screen background for the UI. + * + * @author Alex Leigh + * @since 1.0 + */ +public class ScreenDecorationRenderer implements WindowDecorationRenderer { + private static final int TITLE_POSITION_WITH_PADDING = 4; + private static final int TITLE_POSITION_WITHOUT_PADDING = 3; + + public TextGUIGraphics draw(WindowBasedTextGUI textGUI, TextGUIGraphics graphics, Window window) { + String title = window.getTitle(); + if (title == null) { + title = ""; + } + + TerminalSize drawableArea = graphics.getSize(); + ThemeDefinition themeDefinition = window.getTheme().getDefinition(DefaultWindowDecorationRenderer.class); + char horizontalLine = themeDefinition.getCharacter("HORIZONTAL_LINE", Symbols.SINGLE_LINE_HORIZONTAL); + char verticalLine = themeDefinition.getCharacter("VERTICAL_LINE", Symbols.SINGLE_LINE_VERTICAL); + char bottomLeftCorner = themeDefinition.getCharacter("BOTTOM_LEFT_CORNER", Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER); + char topLeftCorner = themeDefinition.getCharacter("TOP_LEFT_CORNER", Symbols.SINGLE_LINE_TOP_LEFT_CORNER); + char bottomRightCorner = themeDefinition.getCharacter("BOTTOM_RIGHT_CORNER", Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER); + char topRightCorner = themeDefinition.getCharacter("TOP_RIGHT_CORNER", Symbols.SINGLE_LINE_TOP_RIGHT_CORNER); + char titleSeparatorLeft = themeDefinition.getCharacter("TITLE_SEPARATOR_LEFT", Symbols.SINGLE_LINE_HORIZONTAL); + char titleSeparatorRight = themeDefinition.getCharacter("TITLE_SEPARATOR_RIGHT", Symbols.SINGLE_LINE_HORIZONTAL); + boolean useTitlePadding = themeDefinition.getBooleanProperty("TITLE_PADDING", false); + boolean centerTitle = themeDefinition.getBooleanProperty("CENTER_TITLE", false); + + int titleHorizontalPosition = useTitlePadding ? TITLE_POSITION_WITH_PADDING : TITLE_POSITION_WITHOUT_PADDING; + int titleMaxColumns = drawableArea.getColumns() - titleHorizontalPosition * 2; + if (centerTitle) { + titleHorizontalPosition = (drawableArea.getColumns() / 2) - (TerminalTextUtils.getColumnWidth(title) / 2); + titleHorizontalPosition = Math.max(titleHorizontalPosition, useTitlePadding ? TITLE_POSITION_WITH_PADDING : TITLE_POSITION_WITHOUT_PADDING); + } + String actualTitle = TerminalTextUtils.fitString(title, titleMaxColumns); + int titleActualColumns = TerminalTextUtils.getColumnWidth(actualTitle); + + // Don't draw highlights on menu popup windows + if (window.getHints().contains(Window.Hint.MENU_POPUP)) { + graphics.applyThemeStyle(themeDefinition.getNormal()); + } else { + graphics.applyThemeStyle(themeDefinition.getPreLight()); + } + + graphics.drawLine(new TerminalPosition(0, drawableArea.getRows() - 2), new TerminalPosition(0, 1), verticalLine); + graphics.drawLine(new TerminalPosition(1, 0), new TerminalPosition(drawableArea.getColumns() - 2, 0), horizontalLine); + graphics.setCharacter(0, 0, topLeftCorner); + graphics.setCharacter(0, drawableArea.getRows() - 1, bottomLeftCorner); + + if (!actualTitle.isEmpty() && drawableArea.getColumns() > 8) { + int separatorOffset = 1; + if (useTitlePadding) { + graphics.setCharacter(titleHorizontalPosition - 1, 0, ' '); + graphics.setCharacter(titleHorizontalPosition + titleActualColumns, 0, ' '); + separatorOffset = 2; + } + graphics.setCharacter(titleHorizontalPosition - separatorOffset, 0, titleSeparatorLeft); + graphics.setCharacter(titleHorizontalPosition + titleActualColumns + separatorOffset - 1, 0, titleSeparatorRight); + } + + graphics.applyThemeStyle(themeDefinition.getNormal()); + graphics.drawLine( + new TerminalPosition(drawableArea.getColumns() - 1, 1), + new TerminalPosition(drawableArea.getColumns() - 1, drawableArea.getRows() - 2), + verticalLine); + graphics.drawLine( + new TerminalPosition(1, drawableArea.getRows() - 1), + new TerminalPosition(drawableArea.getColumns() - 2, drawableArea.getRows() - 1), + horizontalLine); + + graphics.setCharacter(drawableArea.getColumns() - 1, 0, topRightCorner); + graphics.setCharacter(drawableArea.getColumns() - 1, drawableArea.getRows() - 1, bottomRightCorner); + + if (!actualTitle.isEmpty()) { + if (textGUI.getActiveWindow() == window) { + graphics.applyThemeStyle(themeDefinition.getActive()); + } else { + graphics.applyThemeStyle(themeDefinition.getInsensitive()); + } + graphics.putString(titleHorizontalPosition, 0, actualTitle); + } + + graphics.putString(new TerminalPosition(0, 10), "HELLO WORLD!!!"); + + return graphics.newTextGraphics( + new TerminalPosition(1, 1), + drawableArea + // Make sure we don't make the new graphic's area smaller than 0 + .withRelativeColumns(-(Math.min(2, drawableArea.getColumns()))) + .withRelativeRows(-(Math.min(2, drawableArea.getRows())))); + } + + @Override + public TerminalSize getDecoratedSize(Window window, TerminalSize contentAreaSize) { + ThemeDefinition themeDefinition = window.getTheme().getDefinition(DefaultWindowDecorationRenderer.class); + boolean useTitlePadding = themeDefinition.getBooleanProperty("TITLE_PADDING", false); + + int titleWidth = TerminalTextUtils.getColumnWidth(window.getTitle()); + int minPadding = TITLE_POSITION_WITHOUT_PADDING * 2; + if (useTitlePadding) { + minPadding = TITLE_POSITION_WITH_PADDING * 2; + } + + return contentAreaSize + .withRelativeColumns(2) + .withRelativeRows(2) + .max(new TerminalSize(titleWidth + minPadding, 1)); //Make sure the title fits! + } + + private static final TerminalPosition OFFSET = new TerminalPosition(1, 1); + + @Override + public TerminalPosition getOffset(Window window) { + return OFFSET; + } +} diff --git a/lc-pips-tc/src/main/java/lc/pips/tc/ScreenWindow.java b/lc-pips-tc/src/main/java/lc/pips/tc/ScreenWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..b5e34e49945a3bb2bde61bcb623661a6227ecc57 --- /dev/null +++ b/lc-pips-tc/src/main/java/lc/pips/tc/ScreenWindow.java @@ -0,0 +1,28 @@ +package lc.pips.tc; + +import com.googlecode.lanterna.gui2.AbstractWindow; +import com.googlecode.lanterna.gui2.TextGUIGraphics; + +public class ScreenWindow extends AbstractWindow { + /** + * Default constructor, creates a new window with no title + */ + public ScreenWindow() { + super(); + } + + /** + * This constructor creates a window with a specific title, that is (probably) going to be displayed in the window + * decoration + * + * @param title Title of the window + */ + public ScreenWindow(String title) { + super(title); + } + + public void draw(TextGUIGraphics graphics) { + super.draw(graphics); + // graphics.putString(0,1,"HELLO WORLD!! FKLHDSFKLHDS FJKDSH FJKDSHF JKDHF JDKFH "); + } +} diff --git a/lc-pips-tc/src/main/java/lc/pips/tc/TerminalControllerService.java b/lc-pips-tc/src/main/java/lc/pips/tc/TerminalControllerService.java index 80bb9d9c0bfa68ed52c97fc26917c34e7b4f3270..b6a7e9023458451be81d25a22c29623fa0e77561 100644 --- a/lc-pips-tc/src/main/java/lc/pips/tc/TerminalControllerService.java +++ b/lc-pips-tc/src/main/java/lc/pips/tc/TerminalControllerService.java @@ -24,7 +24,7 @@ public class TerminalControllerService { try { TelnetTerminal rawTerminal = tts.acceptConnection(); System.err.println("Ok, got it!"); - new Thread(new TerminalController(rawTerminal)).start(); + new Thread(new AlternativeController(rawTerminal)).start(); } catch (Exception e) { e.printStackTrace(); } diff --git a/mabe-lanterna/.gitattributes b/mabe-lanterna/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..589c67721a7c5b890fb000739408e7779fd1cb37 --- /dev/null +++ b/mabe-lanterna/.gitattributes @@ -0,0 +1 @@ +*.java eol=lf diff --git a/mabe-lanterna/.gitignore b/mabe-lanterna/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9d0b97e6a44aac344d8ced3f4c72d9780587fe24 --- /dev/null +++ b/mabe-lanterna/.gitignore @@ -0,0 +1,7 @@ +.classpath +.project +.settings/ +.idea/ +nb-configuration.xml +target/ +*.iml diff --git a/mabe-lanterna/CHANGELOG.md b/mabe-lanterna/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..6866ccf3c73949a8c7c9f34ccc5ef8ec1d4f720d --- /dev/null +++ b/mabe-lanterna/CHANGELOG.md @@ -0,0 +1,281 @@ +# Changelog + +## Table of contents + +* [**3.0.0**](#3.0.0) +* [2.1.9](#2.1.9) +* [2.1.8](#2.1.8) +* [2.1.7](#2.1.7) +* [2.1.6](#2.1.6) +* [2.1.5](#2.1.5) +* [2.1.3](#2.1.3) +* [2.1.2](#2.1.2) +* [2.1.1](#2.1.1) +* [**2.1.0**](#2.1.0) +* [2.0.4](#2.0.4) +* [2.0.3](#2.0.3) +* [2.0.2](#2.0.3) +* [2.0.1](#2.0.1) +* [**2.0.0**](#2.0.0) + +## 3.0.0 + +Lanterna 3 is a large, and probably final, update to the Lanterna library. Many parts have been completely rewritten and +the parts not rewritten have been touched in at least some way. The reason for this major overhaul is to finally get +it 'right' and fix all those API mistakes that have been highlighted over the years since Lanterna was first published. + +This section can in no way summarize all the changes but will try to highlight some of the new features and redesigns. +Please note that Lanterna 3 is **not** API compatible with Lanterna 2.X and earlier. + +**NOTE**: Lanterna 3 is still under development. The majority of the features below are implemented but not all. + +## Added + +* Proper support for CJK characters and handling of them +* New GUI system: The old GUI system has been deprecated and a new one is replacing it, giving you much more control + over how you want your GUI to look. You can do any kind of old-school interface, not just dialog-based ones and even + things like multi-tasking windows if you like. Please note that this is currently under development. +* New `SwingTerminal`: `SwingTerminal` in Lanterna 2.X was limited in many ways. For Lanterna 3.0 some of those + limitations have been addressed. The actual class is no longer a `JFrame` but a `JComponent`, meaning you can easily + embed it into any Swing application. Furthermore, it does not require to be run in private mode anymore. You can + switch between normal and private mode as you like and it will keep track of the content. Additionally, it finally + supports a backlog history and scrolling. A helper class, `ScrollingSwingTerminal`, can easily get you started with + this. If you want the classic behaviour there is `SwingTerminalFrame` which behaves much like `SwingTerminal` used to. +* Telnet server: In addition to the terminal implementations that have been around since the earlier builds of Lanterna, + version 3 introduces a Telnet server class that allows you to program multiple terminals against clients connecting in + through standard Telnet. A small subset of the Telnet protocol is implemented so far, however, it supports features + such as window resizing, line mode setting and echo control. +* `ScreenWriter` now supports not just text and filled rectangles but also lines and both filled and unfilled triangles. + +## Changed + +* Made `Screen` an interface and cleaned up its API. The default implementation behaves like `Screen` used to with + improvements such as full color support +* The code and API more closely follows Java conventions on naming and style + +## 2.1.9 + +### Added + +* Better ESC key detection +* Enable EOF 'key' when the input stream is closed (requires setting system property ' + com.googlecode.lanterna.enable-eof' to 'true') +* `TextBox` now accepts input of non-Latin characters + +### Changed + +* Better ESC key detection +* Regression fixed with high CPU load when opening a window with no interactable components +* `KeyMappingProfile` patterns now public + +## 2.1.8 + +### Added + +* Ability to set the fill character of `TextBox` components (other than space) +* Ability to disable shadows for windows +* Added a file dialog component +* Added a method to make it easier to wrap components in a border +* Added `SwingTerminal` function key support +* Window-deriving classes can inspect which component has input focus + +### Changed + +* Input focus bug fixes +* `InputDecoder` fixes backported from master branch + +## 2.1.7 + +### Added + +* Added support for the PageUp, PageDown, Home and End keys inside `AbstractListBox` and its subclasses + +### Changed + +* Change visibility of `LayoutParameter` constructor to public, making it easier to create custom layout managers +* Fixed `TextArea` crash on pressing End when horizontal size is too big +* Miscellaneous bug fixes +* Terminals will remember if they are in private mode and will not attempt to enter twice +* `Screen` will drain the input queue upon exiting + +## 2.1.6 + +### Added + +* Added an experimental `TextArea`, a user-contributed component +* Added `Screen.updateScreenSize()` to manually check and update internal data structures, allowing you to redraw the + screen before calling `Screen.refresh()` +* Proper `Key.equals(...)` and `Key.hashCode()` methods +* Proper `TerminalPosition.equals(...)` and `TerminalPosition.hashCode()` methods + +### Changed + +* Fixed a deadlock in `GUIScreen` +* `ActionListBox` has a new parameter that closes the dialog before running the selected `Action` +* `SwingTerminal` AWT threading fixes + +## 2.1.5 + +### Added + +* Added a new method to invalidate the `Screen` buffer and force a complete redraw + +### Changed + +* Visibility changed on `GUIScreen` to make it easier to extend + +## 2.1.3 + +### Added + +* Customization of screen padding character +* More input key combinations detecting ALT down + +### Changed + +* Background color fix with `Screen` +* Expanded `Table` API +* Improved (but still incomplete) CJK character handling +* OS X input compatibility fix + +## 2.1.2 + +### Added + +* `RadioCheckBoxList.getCheckedItem()` + +### Changed + +* Enhanced restoration of the terminal control codes (especially on Solaris) +* Fixed a bug that occurred when `SwingTerminal` is reduced to 0 rows +* Fixed a bug that prevented the cursor from becoming visible again after leaving private mode +* `ActionListDialog` now increases in size as you add items +* `TextBox` can now tell you the current edit cursor position + +## 2.1.1 + +### Added + +* Re-added `GUIScreen.closeWindow()` (as deprecated) +* Re-added `Panel.setBetweenComponentsPadding(...)` (as deprecated) + +### Changed + +* Owner window can now be correctly derived from a component +* Classes extending `AbstractListBox` now follow the preferred size override correctly + +### Added + +* Added a new component, `ActivityIndicator` +* Added support for showing and hiding the text cursor +* Included ANSI colour palettes for the `SwingTerminal` to mimic the appearance of several popular terminal emulators +* Introduced the `BorderLayout` layout managed +* Support 8-bit and 24-bit colours (not supported by all terminal emulators) +* Support detection of CTRL and ALT key status +* `GUIScreen` backgrounds can now be customized + +### Changed + +* Close windows using `Window.close()` instead of `GUIScreen.closeWindow(...)` +* Generalized component alignment +* GUI windows can now be display in full-screen mode, taking up the entire terminal +* Lots of bug fixes +* Reworked GUI layout system +* Reworked the theme system +* Window size is overridable +* `SwingTerminal` now uses a new class, `TerminalAppearance`, to retrieve the visual settings, such as fonts and colours + +### Removed + +* Removed dependencies on proprietary Sun API + +## 2.1.0 + +2.1.X is **not** strictly API compatible with 2.0.X but compared to going from 1.0.X to 2.0.X there will be fewer API +breaking changes + +## 2.0.4 + +### Added + +* The PageUp, PageDown, Home, and End keys now work in the `TextArea` component + +### Changed + +* Adding rows to a `Table` will trigger the screen to redraw +* Improved API for `RadioCheckBoxList` + +## 2.0.3 + +### Added + +* Added experimental support for F1-F12 keys +* `TextArea` can now be modified (experimental feature) + +### Changed + +* Font fixes. Hopefully it will look better on Linux now +* Invisible components no longer receive focus +* The size policies are working better now but they are still somewhat mysterious. I will try to come up with something + better for the 2.1.0 release + +### What about 2.0.2? + +There is no 2.0.2. I did a mistake staging the new release and had to start over again but 2.0.2 had already been tagged +in Mercurial so I could not re-release it. Instead we skipped a number and landed on 2.0.3 + +## 2.0.1 + +### Added + +* Added `Screen.clear()` that allows resetting the content of the screen +* Added `Terminal.getTerminalSize()` to synchronously retrieve the current size of the terminal +* Added new overloads so that you can specify a separate font to use for bold text in `SwingTerminal` +* `SwingTerminal` will now render underlined text +* `SwingTerminal` will expose its internal `JFrame` through a new method `getJFrame()`, allowing you to set a custom + title, icon, image list etc. + +### Changed + +* `queryTerminalSize()` has been marked as deprecated but will still work as before +* `TextBox` and `PasswordBox` constructors that did not take a width parameter were broken, fixed and changed so that + the initial size (unless forced) will be at least 10 columns wide + +## 2.0.0 + +### Added + +* Added a new facade class, `TerminalFacade`, which provides some convenience methods for creating terminal objects +* Added experimental, but not very functional, support for Cygwin +* Expanded `Interactable.Result` and `Interactable.FocusChangeDirection` to allow focus switching in four directions + instead of only two +* Introduced `AbstractListBox` which has standardized the format and the methods of the list-based GUI elements +* Mavenized the project, will try to push it to Maven Central somehow + +### Changed + +* ~~Moved `com.googlecode.lanterna.TerminalFactory` to `com.googlecode.lanterna.terminal` where it belongs~~ +* Moved `Terminal.addInputProfile(...)` to `InputProvider` +* Moved `Terminal.Style` to an outer class in `com.googlecode.lanterna.screen` +* Moved `SwingTerminal` to `com.googlecode.lanterna.terminal.swing` +* Moved `Terminal.setCBreak(...)` and `Terminal.setEcho(...)` into `ANSITerminal`. You probably don't need to call these + directly anyway, since they are automatically called for the `UnixTerminal` when entering private mode +* Rearranged the `Terminal` hierarchy. This is mostly internal but you might have been using `CommonUnixTerminal` before + which is now known as `com.googlecode.lanterna.terminal.text.UnixTerminal` +* Renamed the project's package name from `org.lantern` to `com.googlecode.lanterna` +* Renamed `LanternException` to `LanternaException` for consistency +* `LanternaException` is now a `RuntimeException` since `IOException`s coming from stdin and stdout are quite rare +* Renamed some enums and an internal class in `Theme`. You probably will not be affected by this unless you have defined + your own theme + +### Removed + +* Removed `LanternTerminal` and `TerminalFactory` as they were quite confusing and not really necessary +* Removed `ListBox` as there is not much purpose for it in this environment +* Removed `RadioCheckBox` and `RadioCheckBoxGroup`. `RadioCheckBoxList` acts as a replacement +* Removed `TermInfo` classes (they did not really work so hopefully no one was using them) + +### Maven + +Starting with the 2.0.0 release, Lanterna has been using Maven and the Sonatype OSS repository which is synchronized +with Maven Central. Please see the [Maven information page](Maven.md) for more details diff --git a/mabe-lanterna/License.txt b/mabe-lanterna/License.txt new file mode 100644 index 0000000000000000000000000000000000000000..02bbb60bc49afc2d6a1bedf96288eab236d80fbd --- /dev/null +++ b/mabe-lanterna/License.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/mabe-lanterna/README.md b/mabe-lanterna/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cfb22eba99c4810a5e29b874f5186de42c3e55a6 --- /dev/null +++ b/mabe-lanterna/README.md @@ -0,0 +1,69 @@ +Lanterna +--- + +![Lanterna screenshot](http://mabe02.github.io/lanterna/resources/lanterna.png) + +Lanterna is a Java library allowing you to write easy semi-graphical user interfaces in a text-only environment, +very similar to the C library [curses](http://en.wikipedia.org/wiki/Curses_(programming_library)) but with more +functionality. +Lanterna is supporting xterm compatible terminals and terminal emulators such as konsole, gnome-terminal, putty, xterm +and many more. +One of the main benefits of lanterna is that it's not dependent on any native library but runs 100% in pure Java. + +Also, when running Lanterna on computers with a graphical environment (such as Windows or Xorg), a bundled terminal +emulator +written in Swing will be used rather than standard output. This way, you can develop as usual from your IDE +(most of them doesn't support ANSI control characters in their output window) and then deploy to your headless server +without changing any code. + +Lanterna is structured into three layers, each built on top of the other and you can easily choose which one fits your +needs best. + +1. The first is a low level terminal interface which gives you the most basic control of the terminal text area. + You can move around the cursor and enable special modifiers for characters put to the screen. You will find these + classes in package com.googlecode.lanterna.terminal. + +2. The second level is a full screen buffer, the whole text screen in memory and allowing you to write to this before + flushing the changes to the actual terminal. + This makes writing to the terminal screen similar to modifying a bitmap. You will find these classes in package + com.googlecode.lanterna.screen. + +3. The third level is a full GUI toolkit with windows, buttons, labels and some other components. + It's using a very simple window management system (basically all windows are modal) that is quick and easy to use. + You will find these classes in package com.googlecode.lanterna.gui2. + +Maven +--- + +Lanterna is available on [Maven Central](http://search.maven.org/), +through [Sonatype OSS hosting](http://oss.sonatype.org/). Here's what you want to use: + +```xml + + com.googlecode.lanterna + lanterna + 3.1.1 + +``` + +Discussions +--- +There is a [google group](https://groups.google.com/forum/#!forum/lanterna-discuss) for discussions and announcements +related to Lanterna. + + +Development Guide +--- +See [docs](docs/contents.md) for examples and guides. + +JavaDoc is available here: + +* http://mabe02.github.io/lanterna/apidocs/3.1/ + +The JavaDocs for the previous versions (2.1 and 3.0) are also available here: + +* http://mabe02.github.io/lanterna/apidocs/2.1/ +* http://mabe02.github.io/lanterna/apidocs/3.0/ + +There is also a development guide and some tutorials +available [right here on Github](https://github.com/mabe02/lanterna/blob/master/docs/contents.md). diff --git a/mabe-lanterna/build.gradle b/mabe-lanterna/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..e443f514627aadbbdf80c38fda2af75571932ae1 --- /dev/null +++ b/mabe-lanterna/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'java-library' +} + +group 'leigh' +version '3.1' + +repositories { + mavenCentral() +} + +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + api 'net.java.dev.jna:jna:5.5.0' + api 'net.java.dev.jna:jna-platform:5.5.0' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/mabe-lanterna/docs/GUIGuideComponents.md b/mabe-lanterna/docs/GUIGuideComponents.md new file mode 100644 index 0000000000000000000000000000000000000000..fab8115496e10450087da430293a47fc46ebeac3 --- /dev/null +++ b/mabe-lanterna/docs/GUIGuideComponents.md @@ -0,0 +1,13 @@ +# Components + +**WARNING: This page has not yet been updated to Lanterna 3 and is out-of-date** + +* ActionListBox +* Button +* CheckBox +* CheckBoxList +* Label +* Panel +* PasswordBox +* Table +* TextBox \ No newline at end of file diff --git a/mabe-lanterna/docs/GUIGuideDialogs.md b/mabe-lanterna/docs/GUIGuideDialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..21972ee3c270f3f0ec1a3bdb8ae88552103a501b --- /dev/null +++ b/mabe-lanterna/docs/GUIGuideDialogs.md @@ -0,0 +1,9 @@ +# Built-in dialogs + +**WARNING: This page has not yet been updated to Lanterna 3 and is out-of-date** + +* ActionListDialog +* ListSelectDialog +* MessageBox +* TextInputDialog +* WaitingDialog \ No newline at end of file diff --git a/mabe-lanterna/docs/GUIGuideMisc.md b/mabe-lanterna/docs/GUIGuideMisc.md new file mode 100644 index 0000000000000000000000000000000000000000..238596fea8733d5f9411c4c4cec437114d49a829 --- /dev/null +++ b/mabe-lanterna/docs/GUIGuideMisc.md @@ -0,0 +1,6 @@ +# Misc + +**WARNING: This page has not yet been updated to Lanterna 3 and is out-of-date** + +* Multi-threading +* Themes \ No newline at end of file diff --git a/mabe-lanterna/docs/GUIGuideStartTheGUI.md b/mabe-lanterna/docs/GUIGuideStartTheGUI.md new file mode 100644 index 0000000000000000000000000000000000000000..595f24c7709e8128a0ac994b4ac2cda54bf164eb --- /dev/null +++ b/mabe-lanterna/docs/GUIGuideStartTheGUI.md @@ -0,0 +1,42 @@ +# Start the GUI + +## Getting the `WindowBasedTextGUI` object ### + +Lanterna has a basic `TextGUI` interface and a slightly more extended `WindowBasedTextGUI` interface you will be using +when working with the GUI system built-in. In reality though, there is only one concrete implementation at this point, +`MultiWindowTextGUI` so there isn't really case where you would use `TextGUI` over `WindowBasedTextGUI`. + +To instantiate, you need a `Screen` object that the GUI will render too: + + WindowBasedTextGUI gui = new MultiWindowTextGUI(screen); + +## Threading concerns + +Usually GUI APIs have threading issues of some sort, many simply give up and declare that all modifications had to be +done on a designated "GUI thread". This generally a reasonably approach and while Lanterna doesn't enforce it +(internal API is synchronized on a best-effort basis), it's recommended. When you create the text GUI on your code, +you can make a decision about if you want Lanterna to create its own GUI thread will be responsible for all the drawing +operations or if you want your own thread to do this. The managed GUI thread is slightly more complicated so in this +guide we are going to use the latter. While most of the API remains the same (the separate GUI thread approach will +require manually starting and stopping), whenever there are differences these will be pointed out. + +By default, when you create a `MultiWindowTextGUI` like in the previous section, Lanterna will use the same-thread +strategy. + +## Starting the GUI + +Unlike `Screen`, the `TextGUI` interface doesn't have start or stop methods. It will simply use the `Screen` object you +pass in as-is. Because of this, you'll want to start the screen before attempting to draw anything with the GUI. + + Terminal term = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(term); + WindowBasedTextGUI gui = new MultiWindowTextGUI(screen); + screen.startScreen(); + + // use GUI here until the GUI wants to exit + + screen.stopScreen(); + +## Next + +Continue to [Basic windows](GUIGuideWindows.md) \ No newline at end of file diff --git a/mabe-lanterna/docs/GUIGuideWindows.md b/mabe-lanterna/docs/GUIGuideWindows.md new file mode 100644 index 0000000000000000000000000000000000000000..d3d23c254584cc58bdcb4c27829b1e5e8a532df2 --- /dev/null +++ b/mabe-lanterna/docs/GUIGuideWindows.md @@ -0,0 +1,182 @@ +# Basic Windows + +## Introduction + +Normally, all windows you create in Lanterna are modal. That means, there is no window management involved. When you +create and show one window, it will overlap all other windows and exclusively take input from the user. This window will +remain in focus until either it's closed or another window is shown. There is no way to switch between windows without +closing the currently focused. + +However, Lanterna *does* support multi-windows mode where each window is not necessarily modal. Due to the nature of the +terminal and how input works, there isn't any general standard for how to switch windows in a text console environment, +and as such Lanterna only provides programmatic support for switching active window. You'll have to register key +listeners and do the switching yourself if you want to support this. + +## The `Window` class + +Like in Swing/AWT, you will probably want to subclass the `BasicWindow` class (implementing the `Window` interface)when +you create your own windows. This is not a strict requirement but can make it easier when coding. + + public class MyWindow extends BasicWindow { + public MyWindow() { + super("WindowTitle"); + } + } + +To show your window, you use the `WindowBasedTextGUI` object you have and call the `addWindow`-method. + + MyWindow myWindow = new MyWindow(); + textGUI.addWindow(myWindow); + +This call will not block, the window is added to the GUI system and is ready to be drawn. + +If your GUI system is configured with the default same-thread mode, your thread is responsible for telling lanterna when +to draw the GUI to the `Screen`. There is a lower-level way of doing this (`TextGUIThread#processEventsAndUpdate()`) but +here we will use something a little bit more intuitive: + + myWindow.waitUntilClosed(); + +In this case though, we haven't added any way to close the window so effectively the call above would never come back. +We'll add an exit button further down. + +### Window hints + +The window system uses a special `WindowManager` to figure out how to place the windows inside the screen. This has a +reasonable default which will place windows in a traditional cascading pattern. To tell the window manager that you +would like something else for your window, rather than writing a custom window manager you can attach hints. There are +also other hints which are interpreted by the GUI system itself rather than the window manager. As of lanterna 3, these +are the available hints, which you can find in `Window.Hint`: + +#### `NO_DECORATIONS` + +With this hint, the `TextGUI` system should not draw any decorations around the window. Decorated size will be the same +as the window size. + +#### `NO_POST_RENDERING` + +With this hint, the `TextGUI` system should skip running any post-renderers for the window. By default this means the +window won't have any shadow. + +#### `NO_FOCUS` + +With this hint, the window should never receive focus by the window manager + +#### `CENTERED` + +With this hint, the window wants to be at the center of the terminal instead of using the cascading layout which is the +standard. + +#### `FIXED_POSITION` + +Windows with this hint should not be positioned by the window manager, rather they should use whatever position is +pre-set programmatically. + +#### `FIXED_SIZE` + +Windows with this hint should not be automatically sized by the window manager (using `getPreferredSize()`), rather +should rely on the code manually setting the size of the window using `setSize(..)` + +#### `FIT_TERMINAL_WINDOW` + +With this hint, don't let the window grow larger than the terminal screen, rather set components to a smaller size than +they prefer. + +#### `MODAL` + +This hint tells the window manager that this window should have exclusive access to the keyboard input until it is +closed. For window managers that allows the user to switch between open windows, putting a window on the screen with +this hint should make the window manager temporarily disable that function until the window is closed. + +#### `FULL_SCREEN` + +A window with this hint would like to be placed covering the entire screen. Use this in combination with +`NO_DECORATIONS` if you want the content area to take up the entire terminal. + +#### `EXPANDED` + +This window hint tells the window manager that the window should be taking up almost the entire screen, leaving only a +small space around it. This is different from `FULL_SCREEN` which takes all available space and completely hide +the background and any other window behind it. + +## Adding components + +In order to make windows a bit useful, you'll need to add some components. Lanterna is using layout system greatly +inspired by SWT and Swing/AWT based on `LayoutManager` implementations that are attached to container components. A +window itself contains only one component so you probably want to set that component to a container of some sort so you +can show more than one component inside the window. + +The most simple layout manager to attach to a container is `LinearLayout`, which places all components that are added to +it in either a horizontal or a vertical line, one after another. You can customize this a little bit by using alignments +but this is very simple to use. + +The basic components we will look at here (the rest can be found in the Components guide) are `Label`, `TextBox`, +`Button` and `Panel`. + +### Panel + +Panels enable you to visually group together one or more components, but more importantly also gives you some command +over the component layout. By default a panel will not have any border, but you can easily decorate it with one that +even has a title. Here's how you can create a couple of panels, laying them out horizontally. + + public class MyWindow extends BasicWindow { + public MyWindow() { + super("My Window!"); + Panel horizontalPanel = new Panel(); + horizontalPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)); + Panel leftPanel = new Panel(); + Panel middlePanel = new Panel(); + Panel rightPanel = new Panel(); + + horizontalPanel.addComponent(leftPanel); + horizontalPanel.addComponent(Borders.singleLineBevel(middlePanel, "Panel Title")); + horizontalPanel.addComponent(Borders.doubleLineBevel(rightPanel)); + + // This ultimately links in the panels as the window content + setComponent(horizontalPanel); + } + } + +### Label + +Simple text labels are created with the `Label` class. The label can be a multi line String, separated by `\n`. The +color of the text is determined by the current theme, but you can override this by calling `setForegroundColor` and +`setBackgroundColor` directly on the object. + +Here is a simple example: + + public class MyWindow extends BasicWindow { + public MyWindow() { + super("My Window!"); + Panel contentPane = new Panel(); + contentPane.setLayoutManager(new LinearLayout(Direction.VERTICAL)); + contentPane.addComponent(new Label("This is the first label")); + contentPane.addComponent(new Label("This is the second label, red").setForegroundColor(TextColor.ANSI.RED)); + contentPane.addComponent(new Label("This is the last label\nSpanning\nMultiple\nRows")); + setComponent(contentPane); + } + } + +### Button + +Button is a component that the user can interact with, by pressing the return key when they are currently highlighted. +Upon creation, you'll assign a label and an Action to a button; the Action will be executed by the GUI system's event +processor when the user activates it. + +To add a button that closes its window and thereby allow up to break out from the `waitUntilClosed()` invocation above, +here is a simple example: + + public class MyWindow extends BasicWindow { + public MyWindow() { + super("My Window!"); + setComponent(new Button("Exit", new Runnable() { + @Override + public void run() { + MyWindow.this.close(); + } + })); + } + } + +## Next + +Continue to [Components](GUIGuideComponents.md) diff --git a/mabe-lanterna/docs/Maven.md b/mabe-lanterna/docs/Maven.md new file mode 100644 index 0000000000000000000000000000000000000000..9bffc66b437f4ce0e7ae9787b345b7192a7ffd14 --- /dev/null +++ b/mabe-lanterna/docs/Maven.md @@ -0,0 +1,38 @@ +# Using Maven # + +If you want to use the Lanterna library through [Apache Maven](http://maven.apache.org), it's very easy to do. Just put +this dependency in your `pom.xml`: + +``` + + ... + + com.googlecode.lanterna + lanterna + 2.1.7 + + ... + +``` + +Adjust the version number as required (I'm probably not going to remember to update this page every time I make a +release). Since the Sonatype OSS repository is synchronized with Maven Central, you don't need to add any extra +repository definitions to your project or your maven settings. + +## Using snapshot releases ## + +If you want to try a snapshot release of Lanterna through Maven, you'll need to add the Sonatype OSS snapshot repository +to you project `pom.xml` or your global maven settings. I have not tested this, but according +to [this](https://github.com/thucydides-webtests/thucydides/wiki/Getting-Started) site, you can add it to your +user's `settings.xml` (or, probably easier, to your project `pom.xml`): + +``` + + ... + + sonatype-oss-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + ... + +``` \ No newline at end of file diff --git a/mabe-lanterna/docs/Screenshots.md b/mabe-lanterna/docs/Screenshots.md new file mode 100644 index 0000000000000000000000000000000000000000..c10dbeaa4484c8cad974f6dd14deea94defbfff6 --- /dev/null +++ b/mabe-lanterna/docs/Screenshots.md @@ -0,0 +1,14 @@ +Windows 10 running Lanterna in the Swing terminal emulator: +![http://mabe02.github.io/lanterna/resources/screenshots/win10-screenshot.png](http://mabe02.github.io/lanterna/resources/screenshots/win10-screenshot.png) + +Xubuntu running Lanterna in the Swing terminal emulator: +TBD + +Mac OS X running Lanterna in the Swing terminal emulator: +TBD + +Xubuntu running Lanterna in the Swing terminal emulator: +TBD + +Xubuntu running Lanterna in the Swing terminal emulator: +TBD \ No newline at end of file diff --git a/mabe-lanterna/docs/contents.md b/mabe-lanterna/docs/contents.md new file mode 100644 index 0000000000000000000000000000000000000000..787dee7f8bdd524b8b176e88e3f5eea6825ca916 --- /dev/null +++ b/mabe-lanterna/docs/contents.md @@ -0,0 +1,76 @@ +Lanterna 3 Documentation +--- + +## Overview + +Lanterna 3 is a large, and probably final, update to the Lanterna library. +Many parts have been completely rewritten and the parts not rewritten have been touched in at least some way. +The reason for this major overhaul is to finally get it 'right' and fix up all of those API mistakes that have been +highlighted +over the years, since lanterna was first published. + +## Development Guide + +1. [Introduction](introduction.md) +2. [Direct terminal access](using-terminal.md) +3. [Buffered screen API](using-screen.md) +4. [Text GUI](using-gui.md) + +## Tutorials + +1. [Tutorial 1](tutorial/Tutorial01.md) - Basic usage +2. [Tutorial 2](tutorial/Tutorial02.md) - More Terminal functionality +3. [Tutorial 3](tutorial/Tutorial03.md) - Using the Screen layer +4. [Tutorial 4](tutorial/Tutorial04.md) - Using the TextGUI layer + +## Examples + +### Terminal + +1. [Terminal Overview](examples/terminal/overview.md) + +### GUI + +1. [Hello World](examples/gui/hello_world.md) +2. [A basic form with submission](examples/gui/basic_form_submission.md) +3. [Windows](examples/gui/windows.md) +4. [Panels](examples/gui/panels.md) +5. [Component sizing](examples/gui/component_sizing.md) +6. [Layout Managers](examples/gui/layout_managers.md) +7. [Labels](examples/gui/labels.md) +8. [Text boxes](examples/gui/text_boxes.md) +9. [Buttons](examples/gui/buttons.md) +10. [Combo boxes](examples/gui/combo_boxes.md) +11. [Check boxes](examples/gui/check_boxes.md) +12. [Radio boxes](examples/gui/radio_boxes.md) +13. [Action list box](examples/gui/action_list_box.md) +14. [Message dialogs](examples/gui/message_dialogs.md) +15. [Text Input dialogs](examples/gui/text_input_dialogs.md) +16. [File dialogs](examples/gui/file_dialogs.md) +17. [Directory dialogs](examples/gui/dir_dialogs.md) +18. [Action list dialogs](examples/gui/action_list_dialogs.md) +19. [Tables](examples/gui/tables.md) +20. [Menus](examples/gui/menus.md) + +## Changes + +1. [2.0.0](ChangesFrom1to2.md) + 1. [2.0.1](ChangesFrom200to201.md) + 1. [2.0.3](ChangesFrom201to203.md) + 1. [2.0.4](ChangesFrom203to204.md) +1. [2.1.0](ChangesFrom20Xto210.md) + 1. [2.1.1](ChangesFrom210to211.md) + 1. [2.1.2](ChangesFrom211to212.md) + 1. [2.1.3](ChangesFrom212to213.md) + 1. [2.1.5](ChangesFrom213to215.md) + 1. [2.1.6](ChangesFrom215to216.md) + 1. [2.1.7](ChangesFrom216to217.md) + 1. [2.1.8](ChangesFrom217to218.md) + 1. [2.1.9](ChangesFrom218to219.md) +1. [3.0.0](ChangesFrom2to3.md) + +## About the name ## + +Originally named "lantern", Google code didn't allow the project to be registered under this name +since [there is already a project on SourceForge with this name](http://sourceforge.net/projects/lantern). That project +hasn't been updated since 2003 though... diff --git a/mabe-lanterna/docs/examples/gui/action_list_box.md b/mabe-lanterna/docs/examples/gui/action_list_box.md new file mode 100644 index 0000000000000000000000000000000000000000..11a7575588a23b8ef95ffadcc48476dcc122f93c --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/action_list_box.md @@ -0,0 +1,26 @@ +Action List Box +--- + +An action list box stores a list of actions the user can made. Each action runs within its own thread. + +To create an action list: + +``` + TerminalSize size = new TerminalSize(14, 10); + ActionListBox actionListBox = new ActionListBox(size); +``` + +To add an action to the `ActionListBox`: + +``` + actionListBox.addItem(itemText, new Runnable() { + @Override + public void run() { + // Code to run when action activated + } + }); +``` + +### Screenshot + +![](screenshots/action_list_box.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/action_list_dialogs.md b/mabe-lanterna/docs/examples/gui/action_list_dialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..11b97c28b984b4f2ed2bd6927f7a558ec417f1d8 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/action_list_dialogs.md @@ -0,0 +1,49 @@ +Action List Dialogs +--- + +Action list dialogs are pop-up windows that allow users to choose from a list of predetermined actions. + +To create an action list dialog, as with all dialogs, you'll need to create and pass in a `WindowBasedTextGUI`: + +``` + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Setup WindowBasedTextGUI for dialogs + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); +``` + +In the following example, an action list dialog box is shown to the user when the button is clicked. When a user +activates an action in the list, the corresponding thread is ran. + +``` + new ActionListDialogBuilder() + .setTitle("Action List Dialog") + .setDescription("Choose an item") + .addAction("First Item", new Runnable() { + @Override + public void run() { + // Do 1st thing... + } + }) + .addAction("Second Item", new Runnable() { + @Override + public void run() { + // Do 2nd thing... + } + }) + .addAction("Third Item", new Runnable() { + @Override + public void run() { + // Do 3rd thing... + } + }) + .build() + .showDialog(textGUI); +``` + +### Screenshot + +![](screenshots/action_list_dialogs.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/basic_form_submission.md b/mabe-lanterna/docs/examples/gui/basic_form_submission.md new file mode 100644 index 0000000000000000000000000000000000000000..591b7a8be9801f14a0da84c9bfd3c121b5fadc9b --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/basic_form_submission.md @@ -0,0 +1,144 @@ +Basic Form Submission Example +--- + +The last example was pretty boring, to say the least. A much more interesting example would be one that involves +interaction! + +In this example, we'll create a simple calculator. + +``` +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + +import java.io.IOException; +import java.util.regex.Pattern; + +public class Calculator { + public static void main(String[] args) throws IOException { + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create panel to hold components + Panel panel = new Panel(); + panel.setLayoutManager(new GridLayout(2)); + + final Label lblOutput = new Label(""); + + panel.addComponent(new Label("Num 1")); + final TextBox txtNum1 = new TextBox().setValidationPattern(Pattern.compile("[0-9]*")).addTo(panel); + + panel.addComponent(new Label("Num 2")); + final TextBox txtNum2 = new TextBox().setValidationPattern(Pattern.compile("[0-9]*")).addTo(panel); + + panel.addComponent(new EmptySpace(new TerminalSize(0, 0))); + new Button("Add!", new Runnable() { + @Override + public void run() { + int num1 = Integer.parseInt(txtNum1.getText()); + int num2 = Integer.parseInt(txtNum2.getText()); + lblOutput.setText(Integer.toString(num1 + num2)); + } + }).addTo(panel); + + panel.addComponent(new EmptySpace(new TerminalSize(0, 0))); + panel.addComponent(lblOutput); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + window.setComponent(panel); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(window); + } +} +``` + +Running the above code will show the user a simple form, showing two text boxes that only accept numbers and a button +which, when activated (pressing the Enter key), will add the two numbers in the text boxes and set the text of the +output label to the result. + +Hmm... this example is ok, but what if we wanted to do more than just add numbers? Let's add a combobox which allows the +user to select between addition and subtraction: + +``` +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + +import java.io.IOException; +import java.util.regex.Pattern; + +public class Calculator { + public static void main(String[] args) throws IOException { + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create panel to hold components + Panel panel = new Panel(); + panel.setLayoutManager(new GridLayout(2)); + + final Label lblOutput = new Label(""); + + panel.addComponent(new Label("Num 1")); + final TextBox txtNum1 = new TextBox().setValidationPattern(Pattern.compile("[0-9]*")).addTo(panel); + + panel.addComponent(new Label("Num 2")); + final TextBox txtNum2 = new TextBox().setValidationPattern(Pattern.compile("[0-9]*")).addTo(panel); + + panel.addComponent(new Label("Operation")); + final ComboBox operations = new ComboBox(); + operations.addItem("Add"); + operations.addItem("Subtract"); + panel.addComponent(operations); + + panel.addComponent(new EmptySpace(new TerminalSize(0, 0))); + new Button("Calculate!", new Runnable() { + @Override + public void run() { + int num1 = Integer.parseInt(txtNum1.getText()); + int num2 = Integer.parseInt(txtNum2.getText()); + if(operations.getSelectedIndex() == 0) { + lblOutput.setText(Integer.toString(num1 + num2)); + } else if(operations.getSelectedIndex() == 1) { + lblOutput.setText(Integer.toString(num1 - num2)); + } + } + }).addTo(panel); + + panel.addComponent(new EmptySpace(new TerminalSize(0, 0))); + panel.addComponent(lblOutput); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + window.setComponent(panel); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(window); + } +} + +``` + +The best way to get to grips with Lanterna is to experiment. Using the examples above, try adding more operations to the +combo box, or producing a new way to display the result. + +### Screenshot + +Here's a screenshot of the finished calculator: + +![](screenshots/calculator.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/buttons.md b/mabe-lanterna/docs/examples/gui/buttons.md new file mode 100644 index 0000000000000000000000000000000000000000..49eb6f70fd10283cc37379534729ef5b12a7f8de --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/buttons.md @@ -0,0 +1,29 @@ +Buttons +--- + +Buttons have a label and a callback. + +To create a simple button, with no callback: + +``` + Button button = new Button("Enter"); +``` + +You can also create a button with a callback: + +``` + Button button = new Button("Enter", new Runnable() { + @Override + public void run() { + // Actions go here + } + }); +``` + +As you can see, the callback runs in its own thread. + +Pressing the `Enter` key on the keyboard when the button is highlighted will trigger the callback. + +### Screenshot + +![](screenshots/buttons.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/check_boxes.md b/mabe-lanterna/docs/examples/gui/check_boxes.md new file mode 100644 index 0000000000000000000000000000000000000000..b8e27455436f9b7c547970ae6c34f2cdd1c63bc8 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/check_boxes.md @@ -0,0 +1,33 @@ +Check Boxes +--- + +Check boxes allow users to select multiple items at once. Check boxes appear in a list. To create a `CheckBox`: + +``` + TerminalSize size = new TerminalSize(14, 10); + CheckBoxList checkBoxList = new CheckBoxList(size); +``` + +To add checkboxes to a list: + +``` + checkBoxList.addItem("item 1"); + checkBoxList.addItem("item 2"); + checkBoxList.addItem("item 3"); +``` + +To get a list of selected items: + +``` + List checkedItems = checkBoxList.getCheckedItems(); +``` + +You can also check if a particular index is checked: + +``` + boolean result = checkBoxList.isChecked(2); +``` + +### Screenshot + +![](screenshots/check_boxes.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/combo_boxes.md b/mabe-lanterna/docs/examples/gui/combo_boxes.md new file mode 100644 index 0000000000000000000000000000000000000000..a2fae371872f16dd34994055d22ef282ac276f53 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/combo_boxes.md @@ -0,0 +1,53 @@ +Combo boxes +--- + +To create a new combo box: + +``` + ComboBox comboBox = new ComboBox(); +``` + +To add items to a `ComboBox`, call `addItem()`: + +``` + comboBox.addItem("item 1"); + comboBox.addItem("item 2"); + comboBox.addItem("item 3"); +``` + +To get the currently selected index: + +``` + // Returns an integer value + comboBox.getSelectedIndex(); +``` + +To mark the combo box as read-only: + +``` + ComboBox comboBox = new ComboBox().setReadOnly(false); +``` + +`setReadOnly` returns the instance of the combo box just created. This is so that methods can be chained together. + +As with all components, you can set the preferred size via `setPreferredSize`: + +``` + comboBox.setPreferredSize(new TerminalSize(15, 1)); +``` + +This will set the width of the combo box to be 15 characters wide. You can also change the height of the combo box if +you so wish. + +When the user highlights the combo box and presses the `Enter` key on the keyboard, the list of selectable items will +show. + +### Screenshots + +#### Non-activated: + +![](screenshots/combo_box.png) + +#### Activated: + +![](screenshots/combo_box_activated.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/component_sizing.md b/mabe-lanterna/docs/examples/gui/component_sizing.md new file mode 100644 index 0000000000000000000000000000000000000000..f6f4af374e90429b989440393edb46485e2c2b4f --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/component_sizing.md @@ -0,0 +1,22 @@ +Component Sizing +--- + +Throughout the GUI layer, the `TerminalSize` class is used to represent how many rows and columns a given component +takes up on screen. + +For example, to set the on screen size of a `Panel`, call the `setPreferredSize` method and pass in a +new `TerminalSize`: + +``` + Panel panel = new Panel(); + panel.setPreferredSize(new TerminalSize(40, 2)); +``` + +You can also pass a `TerminalSize` object into the constructor of many GUI components, for example, the `TextBox` +component: + +``` + // Creates a textbox 40 columns long, 1 row high + TextBox textBox = new TextBox(new TerminalSize(40, 1)) + +``` \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/dir_dialogs.md b/mabe-lanterna/docs/examples/gui/dir_dialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..6c014451263ea4a26e3f710d92c7885b60cf98f5 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/dir_dialogs.md @@ -0,0 +1,39 @@ +Directory Dialogs +--- + +Directory dialogs are pop-up windows that allow users to choose directories from the user's system. + +To create a directory dialog, as with all dialogs, you'll need to create and pass in a `WindowBasedTextGUI`: + +```java + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Setup WindowBasedTextGUI for dialogs + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); +``` + +In the following example, a direcotry dialog is shown to the user when the button is clicked. +When the user selects and submits a directory, the full file path of the selected directory +is returned and stored in the variable `input` and printed to stdout: + +```java + panel.addComponent(new Button("Test", new Runnable() { + @Override + public void run() { + File input = new DirectoryDialogBuilder() + .setTitle("Select directory") + .setDescription("Choose a directory") + .setActionLabel("Select") + .build() + .showDialog(textGUI); + System.out.println(input); + } + })); +``` + +### Screenshot + +![](screenshots/dir_dialogs.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/file_dialogs.md b/mabe-lanterna/docs/examples/gui/file_dialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..ac78efe0a8ca93f090e66e4686af61967438763d --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/file_dialogs.md @@ -0,0 +1,37 @@ +File Dialogs +--- + +File dialogs are pop-up windows that allow users to choose files from the user's system. + +To create an file dialog, as with all dialogs, you'll need to create and pass in a `WindowBasedTextGUI`: + +``` + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Setup WindowBasedTextGUI for dialogs + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); +``` + +In the following example, a file dialog is shown to the user when the button is clicked. When the user selects and +submits a file, the full file path of the selected file is returned and stored in the variable `input`: + +``` + panel.addComponent(new Button("Test", new Runnable() { + @Override + public void run() { + String input = new FileDialogBuilder() + .setTitle("Open File") + .setDescription("Choose a file") + .setActionLabel("Open") + .build() + .showDialog(textGUI); + } + })); +``` + +### Screenshot + +![](screenshots/file_dialogs.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/hello_world.md b/mabe-lanterna/docs/examples/gui/hello_world.md new file mode 100644 index 0000000000000000000000000000000000000000..7ecab8185d5d678e2d43cb2bc8ce73247489d36e --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/hello_world.md @@ -0,0 +1,56 @@ +Hello World - GUI Example +--- + +In this initial example, we take a look at a basic GUI window which contains a couple of components. +There is little to no interactivity in this example, however, it should be a good starting point as it demonstrates +how easy it is to build up interfaces. If you're familiar with Java Swing, you'll feel right at home with Lanterna. + +``` +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + +import java.io.IOException; + +public class HelloWorld { + public static void main(String[] args) throws IOException { + + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create panel to hold components + Panel panel = new Panel(); + panel.setLayoutManager(new GridLayout(2)); + + panel.addComponent(new Label("Forename")); + panel.addComponent(new TextBox()); + + panel.addComponent(new Label("Surname")); + panel.addComponent(new TextBox()); + + panel.addComponent(new EmptySpace(new TerminalSize(0,0))); // Empty space underneath labels + panel.addComponent(new Button("Submit")); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + window.setComponent(panel); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(window); + + } +} +``` + +### Screenshot + +Here's a screenshot of the above code, running: + +![](screenshots/hello_world.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/labels.md b/mabe-lanterna/docs/examples/gui/labels.md new file mode 100644 index 0000000000000000000000000000000000000000..bef27f226d578627f0affe6ba697f9618975cb6e --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/labels.md @@ -0,0 +1,21 @@ +Labels +--- + +Labels, as you may expect, are simple labels. + +To create a simple label: + +``` + Label label = new Label("Here is a label"); +``` + +As with many other components, you can add a `Label` to a `Panel` after instantiation: + +``` + Panel panel = new Panel(); + new Label("Surname").addTo(panel); +``` + +### Screenshot + +![](screenshots/labels.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/layout_managers.md b/mabe-lanterna/docs/examples/gui/layout_managers.md new file mode 100644 index 0000000000000000000000000000000000000000..cfce6fd022fb9deae72368d03c71c4a715a046dd --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/layout_managers.md @@ -0,0 +1,67 @@ +Layout Managers +--- + +Absolute Layout +--- +A Layout manager that places components where they are manually specified to be and sizes them to the size they are +manually assigned to. When using the AbsoluteLayout, please use `setPosition()` and `setSize()` manually on each +component to choose where to place them. + +To set a given panel's layout to be absolute: + +``` + panel.setLayoutManager(new AbsoluteLayout()); +``` + +Linear Layout +--- +A simple layout manager the puts all components on a single line. Linear Layout has two `Direction`'s, `HORIZONTAL` +and `VERTICAL`. + +``` + // To have the components sit next to each other + panel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)) + + // To have the components sit on top of each other + panel.setLayoutManager(new LinearLayout(Direction.VERTICAL)) +``` + +Border Layout +--- +Imitates the BorderLayout class from AWT, allowing you to add a center component with optional components around it in +top, bottom, left and right locations. The edge components will be sized at their preferred size and the center +component will take up whatever remains. + +In the following example, the text box will be full screen, centered and multiline: + +``` + panel.setLayoutManager(new BorderLayout()); + + // Sets the textbox to be in the center of the screen + TextBox textBox = new TextBox("", TextBox.Style.MULTI_LINE); + textBox.setLayoutData(BorderLayout.Location.CENTER); + + //... + + // Tip: Sets the window to be full screen. + window.setHints(Arrays.asList(Window.Hint.FULL_SCREEN)); +``` + +Grid Layout +--- +This emulates the behaviour of the GridLayout in SWT (as opposed to the one in AWT/Swing). + +To set a given panel's layout to be a grid layout: + +``` + // Grid will have two columns + panel.setLayoutManager(new GridLayout(2)); + + // As this grid is a 2xN grid, each row must have 2 elements. + // An empty space is added before the button to fill in the cell before the buttons cell. + buttonPanel.addComponent(new EmptySpace(new TerminalSize(0, 0))); + buttonPanel.addComponent(new Button("Enter")); +``` + +Each time a component is added to the panel, it will be added to the grid in a left to right fashion, starting at 0x0. +When the components get to the end of the column, it will start a new row. \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/menus.md b/mabe-lanterna/docs/examples/gui/menus.md new file mode 100644 index 0000000000000000000000000000000000000000..319631ff5f21e0c256f7f5957b7cc6bc86debedb --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/menus.md @@ -0,0 +1,71 @@ +Menus +--- + +Menus consists of basically three components: + +* *menu bar* - contains one or more drop-down menus +* *menu* - contains one or more menu items +* *menu item* - a label associated with an action + +The menu bar itself is just a panel with buttons that pop up an action-list-like +dialog window beneath the button, aka the menu, that just got clicked. This +dialog window contains all the menu items that were added to this menu. +The menu can be closed by pressing the ESC key on the keyboard. + +You can either add menu items using objects that implement the `Runnable` +interface via the `addMenuItem(Runnable)` method or call the +`addMenuItem(String,Runnable)` method which will take a separate label and not +derive it from the `toString()` of the `Runnable`. + +To add a sub-menu, you can create a add another `Menu` as a menu item to another +`Menu` using the `addSubMenu(Menu)` method. + +In the following example, a menu bar with two menus, *File* and *Help*, is +created, each containing two menu items: + +```java + MenuBar menubar = new MenuBar(); + + // "File" menu + Menu menuFile = new Menu("File"); + menubar.addMenu(menuFile); + menuFile.addMenuItem("Open...", new Runnable() { + public void run() { + File file = new FileDialogBuilder().build().showDialog(textGUI); + if (file != null) + MessageDialog.showMessageDialog( + textGUI, "Open", "Selected file:\n" + file, MessageDialogButton.OK); + } + }); + menuFile.addMenuItem("Exit", new Runnable() { + public void run() { + System.exit(0); + } + }); + + // "Help" menu + Menu menuHelp = new Menu("Help"); + menubar.addMenu(menuHelp); + menuHelp.addMenuItem("Homepage", new Runnable() { + public void run() { + MessageDialog.showMessageDialog( + textGUI, "Homepage", "https://github.com/mabe02/lanterna", MessageDialogButton.OK); + } + }); + menuHelp.addMenuItem("About", new Runnable() { + public void run() { + MessageDialog.showMessageDialog( + textGUI, "About", "Lanterna drop-down menu", MessageDialogButton.OK); + } + }); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + window.setComponent(menubar); + + textGUI.addWindow(window); +``` + +### Screenshot + +![](screenshots/menus.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/message_dialogs.md b/mabe-lanterna/docs/examples/gui/message_dialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..faf020d5a6b8ab1e741c383ef268e635627e6d7e --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/message_dialogs.md @@ -0,0 +1,70 @@ +Message Dialogs +--- + +Message dialogs are simply pop-up messages that are shown to the user and dismissed with the "Enter" key. + +To create a message dialog, as with all dialogs, you'll need to create and pass in a `WindowBasedTextGUI`: + +``` + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Setup WindowBasedTextGUI for dialogs + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); +``` + +In the following example, a message box is shown to the user when the button is clicked: + +``` + panel.addComponent(new Button("Test", new Runnable() { + @Override + public void run() { + MessageDialog.showMessageDialog(textGUI, "test", "test"); + } + })); +``` + +As you can see, it's incredibly easy to create and show a message dialog: + +``` + MessageDialog.showMessageDialog(textGUI, "Message", "Here is a message dialog!"); +``` + +You can also use a `MessageDialogBuilder` to build up and show a message dialog: + +``` + new MessageDialogBuilder() + .setTitle("Here is the title") + .setText("Here is a message") + .build() + .showDialog(textGUI); +``` + +You can also change the button on the `MessageDialog`: + +``` + new MessageDialogBuilder() + .setTitle("Here is the title") + .setText("Here is a message") + .addButton(MessageDialogButton.Close) + .build() + .showDialog(textGUI); +``` + +The following buttons are available: + +- OK +- Cancel +- Yes +- No +- Close +- Abort +- Ignore +- Retry +- Continue + +### Screenshot + +![](screenshots/message_dialogs.png) diff --git a/mabe-lanterna/docs/examples/gui/panels.md b/mabe-lanterna/docs/examples/gui/panels.md new file mode 100644 index 0000000000000000000000000000000000000000..2c3fea623ab18bba7cb8df198da35523a7f3d2c5 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/panels.md @@ -0,0 +1,51 @@ +Panels +--- + +Panels are a way for you to split up your UI and components and group them into seperate sections, similar to JPanels in +Swing. + +To create a `Panel`: + +``` + Panel panel = new Panel(); +``` + +To add a component to a `Panel`: + +``` + panel.addComponent(new Button("Enter")); +``` + +You can also nest `Panel`s: + +``` + BasicWindow window = new BasicWindow(); + + Panel mainPanel = new Panel(); + mainPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)); + + Panel leftPanel = new Panel(); + mainPanel.addComponent(leftPanel.withBorder(Borders.singleLine("Left Panel"))); + + Panel rightPanel = new Panel(); + mainPanel.addComponent(rightPanel.withBorder(Borders.singleLine("Right Panel"))); + + window.setComponent(mainPanel.withBorder(Borders.singleLine("Main Panel"))); + textGUI.addWindow(window); +``` + +In the example above, the "Main Panel" holds two seperate panels: the "Left Panel" and the "Right Panel". + +The left and right panels sit next to each other because a layout manager was provided: + +``` + mainPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)); +``` + +By default, the layout for `Panel`s is a default `LinearLayout` set to `VERTICAL`, meaning that when components are +added, they will sit on top of each other rather than next to each other. More information on layout managers can be +found in the layout manager section. + +### Screenshot + +![](screenshots/panels.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/radio_boxes.md b/mabe-lanterna/docs/examples/gui/radio_boxes.md new file mode 100644 index 0000000000000000000000000000000000000000..ae3e07a19c289b4226b319ec2e8328aee57c5146 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/radio_boxes.md @@ -0,0 +1,30 @@ +Radio Boxes +--- + +Radio boxes allow users to select a single item from a list of predetermined values. + +To create a radio box list: + +``` + TerminalSize size = new TerminalSize(14, 10); + RadioBoxList radioBoxList = new RadioBoxList(size); +``` + +To add radioboxes to a list: + +``` + radioBoxList.addItem("item 1"); + radioBoxList.addItem("item 2"); + radioBoxList.addItem("item 3"); +``` + +To get the currently checked item: + +``` + String checkedItems = radioBoxList.getCheckedItem(); +``` + +### Screenshot + +![](screenshots/radio_boxes.png) + diff --git a/mabe-lanterna/docs/examples/gui/screenshots/action_list_box.png b/mabe-lanterna/docs/examples/gui/screenshots/action_list_box.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7b8f54f3e874f773d42e10b4a23af09cafd1f5 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/action_list_box.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/action_list_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/action_list_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..0432cdf28c722451cd43ea70c03dccd867a28749 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/action_list_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/buttons.png b/mabe-lanterna/docs/examples/gui/screenshots/buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..6c12150824b840fb071f48bdb73195594a902f49 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/buttons.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/calculator.png b/mabe-lanterna/docs/examples/gui/screenshots/calculator.png new file mode 100644 index 0000000000000000000000000000000000000000..d713739b140bff3c694c36243ad0ca0046f0cb24 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/calculator.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/check_boxes.png b/mabe-lanterna/docs/examples/gui/screenshots/check_boxes.png new file mode 100644 index 0000000000000000000000000000000000000000..db81d0c664b2dcad5f280f31079c5f786ee339f7 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/check_boxes.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/combo_box.png b/mabe-lanterna/docs/examples/gui/screenshots/combo_box.png new file mode 100644 index 0000000000000000000000000000000000000000..117c91da04dad2f568a01f594396ae3fadce03be Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/combo_box.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/combo_box_activated.png b/mabe-lanterna/docs/examples/gui/screenshots/combo_box_activated.png new file mode 100644 index 0000000000000000000000000000000000000000..283225c61115c563dd54e607f7c6e082e2cd908a Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/combo_box_activated.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/dir_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/dir_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..808b81efe8d34183d4915417513aba702be1c6aa Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/dir_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/file_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/file_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..1ea4d6b9b7d5c539f155764efce2f92e9a735dad Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/file_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/hello_world.png b/mabe-lanterna/docs/examples/gui/screenshots/hello_world.png new file mode 100644 index 0000000000000000000000000000000000000000..02df9d57f82493482a593064890b10540b6e39da Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/hello_world.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/labels.png b/mabe-lanterna/docs/examples/gui/screenshots/labels.png new file mode 100644 index 0000000000000000000000000000000000000000..b861884e80280cb01f7b9f8b22048bc73d088072 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/labels.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/menus.png b/mabe-lanterna/docs/examples/gui/screenshots/menus.png new file mode 100644 index 0000000000000000000000000000000000000000..ad5227d86898254834b3a79809f3b1e2ce607f8f Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/menus.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/message_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/message_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2a2cb5a68cbc9b58822823ae2b26c029c55794 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/message_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/multiline_input_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/multiline_input_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..6e980376de87791b663ad6c1efd3ddc8f7e028b1 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/multiline_input_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/panels.png b/mabe-lanterna/docs/examples/gui/screenshots/panels.png new file mode 100644 index 0000000000000000000000000000000000000000..38cb3f55aeba5b10eb6c233e7fa9d1f4342011f3 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/panels.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/radio_boxes.png b/mabe-lanterna/docs/examples/gui/screenshots/radio_boxes.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e5a9cf7bb6fa417053346913fa2205f8b069d8 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/radio_boxes.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/tables.png b/mabe-lanterna/docs/examples/gui/screenshots/tables.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a884f551ac4b3b4cd320317241816213bbe590 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/tables.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/text_boxes.png b/mabe-lanterna/docs/examples/gui/screenshots/text_boxes.png new file mode 100644 index 0000000000000000000000000000000000000000..1c67d8d124fbde733ccb32b689ecdce7a44b469c Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/text_boxes.png differ diff --git a/mabe-lanterna/docs/examples/gui/screenshots/text_input_dialogs.png b/mabe-lanterna/docs/examples/gui/screenshots/text_input_dialogs.png new file mode 100644 index 0000000000000000000000000000000000000000..52edda2467636164556c7b4250c02c5e37c62945 Binary files /dev/null and b/mabe-lanterna/docs/examples/gui/screenshots/text_input_dialogs.png differ diff --git a/mabe-lanterna/docs/examples/gui/tables.md b/mabe-lanterna/docs/examples/gui/tables.md new file mode 100644 index 0000000000000000000000000000000000000000..b523dbe1f269a850859ea9c4684387fead09d4b9 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/tables.md @@ -0,0 +1,42 @@ +Tables +--- + +To create a `Table`: + +``` + Table table = new Table("Column 1", "Column 2", "Column 3"); +``` + +The above line will create a table with three headers. + +All `Table`s have a connected `TableModel`. The `TableModel` is used to access the data within the table, add rows, add +columns, etc. + +To add a row: + +``` + table.getTableModel().addRow("1", "2", "3"); +``` + +When adding a row, the number of arguments much match the number of columns. + +You can add a select action, which will run when a row is selected and the `Enter` key is pressed: + +``` + table.setSelectAction(new Runnable() { + @Override + public void run() { + List data = table.getTableModel().getRow(table.getSelectedRow()); + for(int i = 0; i < data.size(); i++) { + System.out.println(data.get(i)); + } + } + }); +``` + +The code above will print out each value in the row to the console when a row is selected and the `Enter` key is +pressed. + +### Screenshot + +![](screenshots/tables.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/text_boxes.md b/mabe-lanterna/docs/examples/gui/text_boxes.md new file mode 100644 index 0000000000000000000000000000000000000000..808ad5439365af2588f839ad09ff6a04cd369d36 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/text_boxes.md @@ -0,0 +1,55 @@ +Text Boxes +--- + +Text boxes allow users to enter information. To create a text box: + +``` + TextBox textBox = new TextBox(); +``` + +You can pass in the [size](component_sizing.md) of the textbox via the constructor: + +``` + // Creates a textbox 30 columns long, 1 column high + new TextBox(new TerminalSize(30,1)); +``` + +If you want to create a multi-line textbox, simply increase the row size: + +``` + // Creates a textbox 30 columns long, 5 column high + new TextBox(new TerminalSize(30,5)); +``` + +You can also add a border to the text box after instantiation: + +``` + new TextBox(new TerminalSize(10, 1)).withBorder(Borders.singleLine("Heading")); +``` + +You can also supply some default text for the text box: + +``` + new TextBox(new TerminalSize(10, 1), "Here is some default content!"); +``` + +You can limit what the user can type into the text box by adding a validation pattern via `setValidationPattern`. The +following example only allows the user to enter a single number into the text box: + +``` + new TextBox().setValidationPattern(Pattern.compile("[0-9]")); +``` + +This will validate the users input on each key press and only allow a single number to be present in the textbox at any +given time. + +Partial matchings are not allowed; the whole pattern must match, however, empty lines will always be allowed. When the +user tries to modify the content of the `TextBox` in a way that does not match the pattern, the operation will be +silently ignored. + +When setting the validation pattern on a given `TextBox`, the existing content will be validated. If the existing +content does not match the provided pattern, a new `IllegalStateException` will be thrown. + +### Screenshot + +![](screenshots/text_boxes.png) diff --git a/mabe-lanterna/docs/examples/gui/text_input_dialogs.md b/mabe-lanterna/docs/examples/gui/text_input_dialogs.md new file mode 100644 index 0000000000000000000000000000000000000000..6fe67909e8c523af74819143a140ec1275529db1 --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/text_input_dialogs.md @@ -0,0 +1,68 @@ +Text Input Dialogs +--- + +Text input dialogs are pop-up windows that allow user input. + +To create an input dialog, as with all dialogs, you'll need to create and pass in a `WindowBasedTextGUI`: + +``` + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Setup WindowBasedTextGUI for dialogs + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); +``` + +In the following example, an input box is shown to the user when the button is clicked and whatever is typed into the +input dialog is stored in the variable `input`: + +``` + panel.addComponent(new Button("Test", new Runnable() { + @Override + public void run() { + String input = TextInputDialog.showDialog(textGUI, "Title", "This is the description", "Initial content"); + } + })); +``` + +As you can see, it's incredibly easy to create and show an input dialog: + +``` + TextInputDialog.showDialog(textGUI, "Title", "This is the description", "Initial content"); +``` + +You can also use a `TextInputDialogBuilder` to build up and show a text dialog: + +``` + new TextInputDialogBuilder() + .setTitle("Title") + .setDescription("Enter a single number") + .setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!") + .build() + .showDialog(textGUI); +``` + +The `TextInputDialog` also supports multi-line text input: + +``` + String result = new TextInputDialogBuilder() + .setTitle("Multi-line editor") + .setTextBoxSize(new TerminalSize(35, 5)) + .build() + .showDialog(textGUI); +``` + +By using a builder, you can set a validation pattern, as shown above, which spits out an error if the user tries to pass +in anything that doesn't match the provided regex. + +### Screenshots + +#### Single-line: + +![](screenshots/text_input_dialogs.png) + +#### Multi-line: + +![](screenshots/multiline_input_dialogs.png) \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/gui/windows.md b/mabe-lanterna/docs/examples/gui/windows.md new file mode 100644 index 0000000000000000000000000000000000000000..56cd094224cc70012f97364a6a92162fe0d79a6e --- /dev/null +++ b/mabe-lanterna/docs/examples/gui/windows.md @@ -0,0 +1,46 @@ +Windows +--- + +Windows are there to hold your components. + +To create a basic window: + +``` + // Setup terminal and screen layers + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + screen.startScreen(); + + // Create window to hold the panel + BasicWindow window = new BasicWindow(); + + // Create gui and start gui + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE)); + gui.addWindowAndWait(window); +``` + +This will create a basic window, ready for you to fill with components: + +What if you wanted to create a full screen window? No problem: + +``` + BasicWindow window = new BasicWindow(); + window.setHints(Arrays.asList(Window.Hint.FULL_SCREEN)); +``` + +You can combine many hints together, to customise how your window should look: + +``` + // Full screen without the border + BasicWindow window = new BasicWindow(); + window.setHints(Arrays.asList(Window.Hint.FULL_SCREEN, Window.Hint.NO_DECORATIONS)); +``` + +How about a center screen window? + +``` + BasicWindow window = new BasicWindow(); + window.setHints(Arrays.asList(Window.Hint.CENTERED)); +``` + +Experiment with a number of `Hint`s to get the `Window` you want. \ No newline at end of file diff --git a/mabe-lanterna/docs/examples/terminal/overview.md b/mabe-lanterna/docs/examples/terminal/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..b48805421398a1387300d9dd8a24302ddc4ce775 --- /dev/null +++ b/mabe-lanterna/docs/examples/terminal/overview.md @@ -0,0 +1,36 @@ +Terminal Overview +--- +The Terminal layer is the lowest level available in Lanterna, giving you very precise control of what data is sent to +the client. You will find these classes in the `com.googlecode.lanterna.terminal` package. + +### Getting the `Terminal` object + +The core of the Terminal layer is the Terminal interface which you must retrieve. This can be done either by directly +instantiating one of the implementing concrete classes (UnixTerminal, CygwinTerminal or SwingTerminal) or by using +the `DefaultTerminalFactory` to create a terminal: + +``` + Terminal terminal = new DefaultTerminalFactory().createTerminal(); +``` + +### What's the big deal anyway? + +So, what's the difference between these implementations of `Terminal`? If, by terminal, you think of applications like +xterm, konsole, putty or Terminal.app, then using Lanterna with a terminal would involve writing control sequences to +standard output (stdout) and reading from standard input (stdin). You would run your Java application in this terminal +and calling the various methods on the `Terminal` interface will be translated by the implementation to the equivalent +control sequences. But a terminal could be anything and we don't want to lock ourselves to a particular idea of what a +terminal is (something using stdout/stdin for example). Instead, in Lanterna, a terminal is something that +implements `Terminal` and with a correct implementation of the interface, the entire stack (Screen and GUIScreen) will +work on top of it, without any modifications. + +### The `SwingTerminal` + +One alternative implementation of `Terminal` is the `SwingTerminal` class. This is a basic terminal emulator implemented +in Swing. It will create a JFrame that represents your terminal. Calling the Terminal methods on a SwingTerminal won't +do anything with stdout or stdin, instead it will translate it to paint operations on the JFrame. This is useful, for +example, when you are developing your application that uses Lanterna. You are probably using an IDE like Eclipse, +NetBeans or IntelliJ. The console windows in these IDE's don't support any of the control sequences the UnixTerminal +will, such as writing to stdout when you try to move around the cursor or change the color of the text. By +using `SwingTerminal`, running the application will instead create a new window that emulates a terminal so that you can +develop and debug like you would do when developing generic Java Swing applications. \ No newline at end of file diff --git a/mabe-lanterna/docs/introduction.md b/mabe-lanterna/docs/introduction.md new file mode 100644 index 0000000000000000000000000000000000000000..77d4e3f99942a9c8fb21212ace10feaf96ad650e --- /dev/null +++ b/mabe-lanterna/docs/introduction.md @@ -0,0 +1,81 @@ +## Introduction + +This document will give you a general introduction to how text terminals work and how Lanterna interacts with them. +In the sub-documents, you can find specific guides on how to program each one of the layers. +You are encouraged to read them all and in order to get the full picture. + +## About terminals + +### TERM and terminfo + +The way terminals work make things difficult when you want to create a portable text GUI. Initially, way back, each +computer system might have its own set of control characters, a kind of escape sequence, that signalled to the screen +that a special action (such as setting the text color to red) was about to happen. With different systems and different +environments, it was tough to write a program that would work everywhere and have the same appearance (well, if you +stick +to just printing text, that would probably be fine, but if you wanted to make use of more advanced commands such as +moving the +cursor, setting the color of the text, making the text blink or change to bold font, etc, then you had a bit of a +challenge. + +There was then an idea that when a terminal logs on to a system, it will set the TERM environmental variable to a +particular value that represented which standard it supported. On the system is a database with different standards +and so any program using text magic can look up the TERM value in this database and get a list of commands supported and +how to execute them. There is however no guarantee that the terminal will set this variable and there is no guarantee +that the system will have the value in its database. Also, there may be discrepancies when, for example, several +implementations of `xterm` exists, which one do you store in your database (the implementations mostly agree on the +control characters, but not completely)? + +Today, most terminals will identify themselves as `xterm` through the TERM environmental variable and for the most part +support this. There is also an [ANSI standard](http://en.wikipedia.org/wiki/ANSI_escape_code) for escape codes and this +is very much in line with what most terminal emulators support. Incompatibilities arise mostly when it comes to +special keys on the keyboard, such as insert, home, end, page up, and so on (what the terminal emulator will send to +standard input when those keys are pressed). + +### Lanterna + +Lanterna will use the `xterm` standard as most terminals understand it, which is basically the standard +[ANSI escape codes](http://en.wikipedia.org/wiki/ANSI_escape_code). As notes in the previous passage explained, where +terminals +diverge most from the `xterm` specification is in the escape codes for all the special keyboard keys. At the moment, +Lantern +tries to accommodate for all of them by adding 'input profiles' for each known type. As new terminals (emulators) are +added, the profiles might need to be updated. So far, as a developer you shouldn't need to mess around with these +profiles as there are no key collisions and we'll try to add any popular terminal emulator out there. The idea is that +Lanterna should work right out of the box, without any tweaking to make it work with your favourite terminal emulator. + +Some more exotic extensions are also supported, such as mouse support and colors above the standard ANSI 8+8. When using +this functionality though, it's likely that your application will have compatibility issues with some common terminal +emulators used in the wild. A good rule of thumb is that if it works with vanilla `xterm`, it will probably be +relatively well-supported. + +### Encoding + +Another problem is the encoding, where terminals may or may not support UTF-8 no matter how you tweak it. By default, +Lanterna will use the system property `file.encoding`, which is setup automatically by the JVM. This seems to be +sufficient with most terminals, but you may want to present the user with an option to force either UTF-8 or iso-8859-1 +(or whatever you see suitable), maybe through a command-line argument. + +## How big is the terminal? + +Yet another problem is how to know the size of the terminal. There are ways of figuring this out through C APIs for most +platforms but since Lanterna is intended to be 100% Java code, we can't do that. Instead, Lanterna is doing a bit of a +hack by memorizing the cursor location, then moving it to 5000x5000 and asking the terminal to print the position. Since +most terminal won't be that big, the cursor will end up in the bottom right corner and report this position when we ask +to print it. This will be the size of the terminal. + +### What if it doesn't work? + +Let us know! + +### What happens if the user resizes the window? + +There is a special Unix signal to notify an application that the terminal has been resized and that one is WINCH. This +is currently implemented using `sun.misc.SignalHandler` which is SUN JRE specific code and probably not portable. Still, +catching signals without invoking native code seems difficult and relying on JNI calls is something we don't want to do. +This will probably be changed to be done through Java reflections through auto-detection by the JVM. + +## Learn more + +To learn more about how to code against the Lanterna API, continue reading +at [Direct terminal access](using-terminal.md) diff --git a/mabe-lanterna/docs/tutorial/Tutorial01.md b/mabe-lanterna/docs/tutorial/Tutorial01.md new file mode 100644 index 0000000000000000000000000000000000000000..f504daf0bc13794453cdbbf9e5453ad739bf42fe --- /dev/null +++ b/mabe-lanterna/docs/tutorial/Tutorial01.md @@ -0,0 +1,173 @@ +Tutorial 1 +--- + +This is the first tutorial and entrypoint for learning more about how to use lanterna. We will use the lower +layer in this tutorial to demonstrate how to move around the cursor and how to output text in different +styles and color. + +First of all, we need to get hold of a Terminal object. This will be our main way of interacting with the +terminal itself. There are a couple of implementation available and it's important you pick the correct one: + +* `UnixTerminal` - Uses ANSI escape codes through standard input/output to carry out the operations +* `SwingTerminal` - Creates a Swing JComponent extending surface that is implementing a terminal emulator +* `SwingTerminalFrame` - Creates a Swing JFrame containing a surface that is implementing a terminal emulator +* `AWTTerminal` - Creates an AWT Component extending surface that is implementing a terminal emulator +* `AWTTerminalFrame` - Creates an AWT Frame containing a surface that is implementing a terminal emulator +* `TelnetTerminal` - Through TelnetTerminalServer, this allows you control the output to the client through the Terminal + interface +* `VirtualTerminal` - Complete in-memory implementation + +If you intend to write a program that runs in a standard console, like for example on a remote server you're +connecting to through some terminal emulator and ssh, what you want is UnixTerminal. However, when developing +the program in your IDE, you might have issues as the IDE's console probably doesn't implement the ANSI escape +codes correctly and the output is complete garbage. Because of this, you might want to use one of the graphical +terminal emulators (Swing or AWT), which will open a new window when you run the program instead of writing to +standard output, and then switch to UnixTerminal when the application is ready. In order to simplify this, +lanterna provides a TerminalFactory with a DefaultTerminalFactory implementation that tries to figure out which +implementation to use. It's mainly checking for if the runtime system has a graphical frontend or not (i.e. if +Java considers the system headless) and if Java is detecting a semi-standard terminal or not (checking if +System.console() returns something), giving you either a terminal emulator or a UnixTerminal. + + DefaultTerminalFactory defaultTerminalFactory = new DefaultTerminalFactory(); + +The DefaultTerminalFactory can be further tweaked, but we'll leave it with default settings in this tutorial. + + Terminal terminal = null; + try { + +Let the factory do its magic and figure out which implementation to use by calling createTerminal() + + terminal = defaultTerminalFactory.createTerminal(); + +If we got a terminal emulator (probably Swing) then we are currently looking at an empty terminal emulator +window at this point. If the code ran in another terminal emulator (putty, gnome-terminal, konsole, etc) by +invoking java manually, there is yet no changes to the content. + +Let's print some text, this has the same as calling System.out.println("Hello"); + + terminal.putCharacter('H'); + terminal.putCharacter('e'); + terminal.putCharacter('l'); + terminal.putCharacter('l'); + terminal.putCharacter('o'); + terminal.putCharacter('\n'); + terminal.flush(); + +Notice the flush() call above; it is necessary to finish off terminal output operations with a call to +flush() both in the case of native terminal and the bundled terminal emulators. Lanterna's Unix terminal +doesn't buffer the output by itself but one can assume the underlying I/O layer does. In the case of the +terminal emulators bundled in lanterna, the flush call will signal a repaint to the underlying UI component. + + Thread.sleep(2000); + +At this point the cursor should be on start of the next line, immediately after the Hello that was just +printed. Let's move the cursor to a new position, relative to the current position. Notice we still need to +call flush() to ensure the change is immediately visible (i.e. the user can see the text cursor moved to the +new position). +One thing to notice here is that if you are running this in a 'proper' terminal and the cursor position is +at the bottom line, it won't actually move the text up. Attempts at setting the cursor position outside the +terminal bounds are usually rounded to the first/last column/row. If you run into this, please clear the +terminal content so the cursor is at the top again before running this code. + + TerminalPosition startPosition = terminal.getCursorPosition(); + terminal.setCursorPosition(startPosition.withRelativeColumn(3).withRelativeRow(2)); + terminal.flush(); + Thread.sleep(2000); + +Let's continue by changing the color of the text printed. This doesn't change any currently existing text, +it will only take effect on whatever we print after this. + + terminal.setBackgroundColor(TextColor.ANSI.BLUE); + terminal.setForegroundColor(TextColor.ANSI.YELLOW); + +Now print text with these new colors + + terminal.putCharacter('Y'); + terminal.putCharacter('e'); + terminal.putCharacter('l'); + terminal.putCharacter('l'); + terminal.putCharacter('o'); + terminal.putCharacter('w'); + terminal.putCharacter(' '); + terminal.putCharacter('o'); + terminal.putCharacter('n'); + terminal.putCharacter(' '); + terminal.putCharacter('b'); + terminal.putCharacter('l'); + terminal.putCharacter('u'); + terminal.putCharacter('e'); + terminal.flush(); + Thread.sleep(2000); + +In addition to colors, most terminals supports some sort of style that can be selectively enabled. The most +common one is bold mode, which on many terminal implementations (emulators and otherwise) is not actually +using bold text at all but rather shifts the tint of the foreground color so it stands out a bit. Let's +print the same text as above in bold mode to compare. + +Notice that startPosition has the same value as when we retrieved it with getTerminalSize(), the +TerminalPosition class is immutable and calling the with* methods will return a copy. So the following +setCursorPosition(..) call will put us exactly one row below the previous row. + + terminal.setCursorPosition(startPosition.withRelativeColumn(3).withRelativeRow(3)); + terminal.flush(); + Thread.sleep(2000); + terminal.enableSGR(SGR.BOLD); + terminal.putCharacter('Y'); + terminal.putCharacter('e'); + terminal.putCharacter('l'); + terminal.putCharacter('l'); + terminal.putCharacter('o'); + terminal.putCharacter('w'); + terminal.putCharacter(' '); + terminal.putCharacter('o'); + terminal.putCharacter('n'); + terminal.putCharacter(' '); + terminal.putCharacter('b'); + terminal.putCharacter('l'); + terminal.putCharacter('u'); + terminal.putCharacter('e'); + terminal.flush(); + Thread.sleep(2000); + +Ok, that's enough for now. Let's reset colors and SGR modifiers and move down one more line + + terminal.resetColorAndSGR(); + terminal.setCursorPosition(terminal.getCursorPosition().withColumn(0).withRelativeRow(1)); + terminal.putCharacter('D'); + terminal.putCharacter('o'); + terminal.putCharacter('n'); + terminal.putCharacter('e'); + terminal.putCharacter('\n'); + terminal.flush(); + + Thread.sleep(2000); + +Beep and exit + + terminal.bell(); + terminal.flush(); + Thread.sleep(200); + } + catch(IOException e) { + e.printStackTrace(); + } + finally { + if(terminal != null) { + try { + +Closing the terminal doesn't always do something, but if you run the Swing or AWT bundled terminal +emulators for example, it will close the window and allow this application to terminate. Calling it +on a UnixTerminal will not have any affect. + + terminal.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + +The full code to this tutorial is available in +the [test section of the source code](https://github.com/mabe02/lanterna/blob/master/src/test/java/com/googlecode/lanterna/tutorial/Tutorial01.java) + +[Move on to tutorial 2](Tutorial02.md) \ No newline at end of file diff --git a/mabe-lanterna/docs/tutorial/Tutorial02.md b/mabe-lanterna/docs/tutorial/Tutorial02.md new file mode 100644 index 0000000000000000000000000000000000000000..8235f70d4badc21ba6591f0f444bae2ede80df5f --- /dev/null +++ b/mabe-lanterna/docs/tutorial/Tutorial02.md @@ -0,0 +1,131 @@ +Tutorial 2 +--- + +In this second tutorial, we'll expand on how to use the Terminal interface to provide more advanced +functionality. + + DefaultTerminalFactory defaultTerminalFactory = new DefaultTerminalFactory(); + Terminal terminal = null; + try { + terminal = defaultTerminalFactory.createTerminal(); + +Most terminals and terminal emulators supports what's known as "private mode" which is a separate buffer for +the text content that does not support any scrolling. This is frequently used by text editors such as `nano` +and `vi` in order to give a "fullscreen" view. When exiting from private mode, the previous content is usually +restored, including the scrollback history. Emulators that don't support this properly might at least clear +the screen after exiting. + + terminal.enterPrivateMode(); + +You can use the `enterPrivateMode()` to activate private mode, but you'll need to remember to also exit +private mode afterwards so that you don't leave the terminal in a weird state when the application exists. +The usual `close()` at the end will do this automatically, but you can also manually call `exitPrivateMode()` +and finally Lanterna will register a shutdown hook that tries to restore the terminal (including exiting +private mode, if necessary) as well. + +The terminal content should already be cleared after switching to private mode, but in case it's not, the +clear method should make all content set to default background color with no characters and the input cursor +in the top-left corner. + + terminal.clearScreen(); + +It's possible to tell the terminal to hide the text input cursor + + terminal.setCursorVisible(false); + +Instead of manually writing one character at a time like we did in the previous tutorial, an easier way is +to use a TextGraphic object. You'll see more of this object later on, it's reused on the Screen and TextGUI +layers too. + + final TextGraphics textGraphics = terminal.newTextGraphics(); + +The TextGraphics object keeps its own state of the current colors, separate from the terminal. You can use +the foreground and background set methods to specify it and it will take effect on all operations until +further modified. + + textGraphics.setForegroundColor(TextColor.ANSI.WHITE); + textGraphics.setBackgroundColor(TextColor.ANSI.BLACK); + +`putString(..)` exists in a couple of different flavors but it generally works by taking a string and +outputting it somewhere in terminal window. Notice that it doesn't take the current position of the text +cursor when doing this. + + textGraphics.putString(2, 1, "Lanterna Tutorial 2 - Press ESC to exit", SGR.BOLD); + textGraphics.setForegroundColor(TextColor.ANSI.DEFAULT); + textGraphics.setBackgroundColor(TextColor.ANSI.DEFAULT); + textGraphics.putString(5, 3, "Terminal Size: ", SGR.BOLD); + textGraphics.putString(5 + "Terminal Size: ".length(), 3, terminal.getTerminalSize().toString()); + +You still need to flush for changes to become visible + + terminal.flush(); + +You can attach a resize listener to your `Terminal` object, which will invoke a callback method (usually on a +separate thread) when it is informed of the terminal emulator window changing size. Notice that maybe not +all implementations supports this. The `UnixTerminal`, for example, relies on the `WINCH` signal being sent to +the java process, which might not make it though if you remote shell isn't forwarding the signal properly. + + terminal.addResizeListener(new TerminalResizeListener() { + @Override + public void onResized(Terminal terminal, TerminalSize newSize) { + // Be careful here though, this is likely running on a separate thread. Lanterna is threadsafe in + // a best-effort way so while it shouldn't blow up if you call terminal methods on multiple threads, + // it might have unexpected behavior if you don't do any external synchronization + textGraphics.drawLine(5, 3, newSize.getColumns() - 1, 3, ' '); + textGraphics.putString(5, 3, "Terminal Size: ", SGR.BOLD); + textGraphics.putString(5 + "Terminal Size: ".length(), 3, newSize.toString()); + try { + terminal.flush(); + } + catch(IOException e) { + // Not much we can do here + throw new RuntimeException(e); + } + } + }); + + textGraphics.putString(5, 4, "Last Keystroke: ", SGR.BOLD); + textGraphics.putString(5 + "Last Keystroke: ".length(), 4, ""); + terminal.flush(); + +Now let's try reading some input. There are two methods for this, `pollInput()` and `readInput()`. One is +blocking (readInput) and one isn't (pollInput), returning null if there was nothing to read. + + KeyStroke keyStroke = terminal.readInput(); + +The KeyStroke class has a couple of different methods for getting details on the particular input that was +read. Notice that some keys, like CTRL and ALT, cannot be individually distinguished as the standard input +stream doesn't report these as individual keys. Generally special keys are categorized with a special +`KeyType`, while regular alphanumeric and symbol keys are all under `KeyType.Character`. Notice that tab and +enter are not considered `KeyType.Character` but special types (`KeyType.Tab` and `KeyType.Enter` respectively) + + while(keyStroke.getKeyType() != KeyType.Escape) { + textGraphics.drawLine(5, 4, terminal.getTerminalSize().getColumns() - 1, 4, ' '); + textGraphics.putString(5, 4, "Last Keystroke: ", SGR.BOLD); + textGraphics.putString(5 + "Last Keystroke: ".length(), 4, keyStroke.toString()); + terminal.flush(); + keyStroke = terminal.readInput(); + } + + } + catch(IOException e) { + e.printStackTrace(); + } + finally { + if(terminal != null) { + try { + +The close() call here will exit private mode + + terminal.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + +The full code to this tutorial is available in +the [test section of the source code](https://github.com/mabe02/lanterna/blob/master/src/test/java/com/googlecode/lanterna/tutorial/Tutorial02.java) + +[Move on to tutorial 3](Tutorial03.md) \ No newline at end of file diff --git a/mabe-lanterna/docs/tutorial/Tutorial03.md b/mabe-lanterna/docs/tutorial/Tutorial03.md new file mode 100644 index 0000000000000000000000000000000000000000..6b40a19cfd1613a673210fb1043971f8d2d5a888 --- /dev/null +++ b/mabe-lanterna/docs/tutorial/Tutorial03.md @@ -0,0 +1,191 @@ +Tutorial 3 +--- + +In the third tutorial, we will look at using the next layer available in Lanterna, which is built on top of the +Terminal interface you saw in tutorial 1 and 2. + +A `Screen` works similar to double-buffered video memory, it has two surfaces than can be directly addressed and +modified and by calling a special method that content of the back-buffer is move to the front. Instead of pixels +though, a `Screen` holds two text character surfaces (front and back) which corresponds to each "cell" in the +terminal. You can freely modify the back "buffer" and you can read from the front "buffer", calling the +`refreshScreen()` method to copy content from the back buffer to the front buffer, which will make Lanterna also +apply the changes so that the user can see them in the terminal. + + DefaultTerminalFactory defaultTerminalFactory = new DefaultTerminalFactory(); + Screen screen = null; + try { + +You can use the `DefaultTerminalFactory` to create a Screen, this will generally give you the `TerminalScreen` +implementation that is probably what you want to use. Please see `VirtualScreen` for more details on a separate +implementation that allows you to create a terminal surface that is bigger than the physical size of the +terminal emulator the software is running in. Just to demonstrate that a `Screen` sits on top of a `Terminal`, +we are going to create one manually instead of using `DefaultTerminalFactory`. + + Terminal terminal = defaultTerminalFactory.createTerminal(); + screen = new TerminalScreen(terminal); + +Screens will only work in private mode and while you can call methods to mutate its state, before you can +make any of these changes visible, you'll need to call startScreen() which will prepare and setup the +terminal. + + screen.startScreen(); + +Let's turn off the cursor for this tutorial + + screen.setCursorPosition(null); + +Now let's draw some random content in the screen buffer + + Random random = new Random(); + TerminalSize terminalSize = screen.getTerminalSize(); + for(int column = 0; column < terminalSize.getColumns(); column++) { + for(int row = 0; row < terminalSize.getRows(); row++) { + screen.setCharacter(column, row, new TextCharacter( + ' ', + TextColor.ANSI.DEFAULT, + // This will pick a random background color + TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)])); + } + } + +So at this point, we've only modified the back buffer in the screen, nothing is visible yet. In order to +move the content from the back buffer to the front buffer and refresh the screen, we need to call `refresh()` + + screen.refresh(); + +Now there should be completely random colored cells in the terminal (assuming your terminal (emulator) +supports colors). Let's look at it for two seconds or until the user press a key. + + long startTime = System.currentTimeMillis(); + while(System.currentTimeMillis() - startTime < 2000) { + // The call to pollInput() is not blocking, unlike readInput() + if(screen.pollInput() != null) { + break; + } + try { + Thread.sleep(1); + } + catch(InterruptedException ignore) { + break; + } + } + +Ok, now we loop and keep modifying the screen until the user exits by pressing escape on the keyboard or the +input stream is closed. When using the Swing/AWT bundled emulator, if the user closes the window this will +result in an EOF `KeyStroke`. + + while(true) { + KeyStroke keyStroke = screen.pollInput(); + if(keyStroke != null && (keyStroke.getKeyType() == KeyType.Escape || keyStroke.getKeyType() == KeyType.EOF)) { + break; + } + +Screens will automatically listen and record size changes, but you have to let the `Screen` know when is +a good time to update its internal buffers. Usually you should do this at the start of your "drawing" +loop, if you have one. This ensures that the dimensions of the buffers stays constant and doesn't change +while you are drawing content. The method `doReizeIfNecessary()` will check if the terminal has been +resized since last time it was called (or since the screen was created if this is the first time +calling) and update the buffer dimensions accordingly. It returns null if the terminal has not changed +size since last time. + + TerminalSize newSize = screen.doResizeIfNecessary(); + if(newSize != null) { + terminalSize = newSize; + } + + // Increase this to increase speed + final int charactersToModifyPerLoop = 1; + for(int i = 0; i < charactersToModifyPerLoop; i++) { + +We pick a random location + + TerminalPosition cellToModify = new TerminalPosition( + random.nextInt(terminalSize.getColumns()), + random.nextInt(terminalSize.getRows())); + +Pick a random background color again + + TextColor.ANSI color = TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)]; + +Update it in the back buffer, notice that just like `TerminalPosition` and `TerminalSize`, `TextCharacter` +objects are immutable so the `withBackgroundColor(..)` call below returns a copy with the background color +modified. + + TextCharacter characterInBackBuffer = screen.getBackCharacter(cellToModify); + characterInBackBuffer = characterInBackBuffer.withBackgroundColor(color); + characterInBackBuffer = characterInBackBuffer.withCharacter(' '); // Because of the label box further down, if it shrinks + screen.setCharacter(cellToModify, characterInBackBuffer); + } + +Just like with `Terminal`, it's probably easier to draw using `TextGraphics`. Let's do that to put a little +box with information on the size of the terminal window + + String sizeLabel = "Terminal Size: " + terminalSize; + TerminalPosition labelBoxTopLeft = new TerminalPosition(1, 1); + TerminalSize labelBoxSize = new TerminalSize(sizeLabel.length() + 2, 3); + TerminalPosition labelBoxTopRightCorner = labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 1); + TextGraphics textGraphics = screen.newTextGraphics(); + //This isn't really needed as we are overwriting everything below anyway, but just for demonstrative purpose + textGraphics.fillRectangle(labelBoxTopLeft, labelBoxSize, ' '); + +Draw horizontal lines, first upper then lower + + textGraphics.drawLine( + labelBoxTopLeft.withRelativeColumn(1), + labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 2), + Symbols.DOUBLE_LINE_HORIZONTAL); + textGraphics.drawLine( + labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(1), + labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(labelBoxSize.getColumns() - 2), + Symbols.DOUBLE_LINE_HORIZONTAL); + +Manually do the edges and (since it's only one) the vertical lines, first on the left then on the right + + textGraphics.setCharacter(labelBoxTopLeft, Symbols.DOUBLE_LINE_TOP_LEFT_CORNER); + textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL); + textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER); + textGraphics.setCharacter(labelBoxTopRightCorner, Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER); + textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL); + textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER); + +Finally put the text inside the box + + textGraphics.putString(labelBoxTopLeft.withRelative(1, 1), sizeLabel); + +Ok, we are done and can display the change. Let's also be nice and allow the OS to schedule other +threads so we don't clog up the core completely. + + screen.refresh(); + Thread.yield(); + +Every time we call refresh, the whole terminal is NOT re-drawn. Instead, the `Screen` will compare the +back and front buffers and figure out only the parts that have changed and only update those. This is +why in the code drawing the size information box above, we write it out every time we loop but it's +actually not sent to the terminal except for the first time because the `Screen` knows the content is +already there and has not changed. Because of this, you should never use the underlying `Terminal` object +when working with a `Screen` because that will cause modifications that the Screen won't know about. + + } + } + catch(IOException e) { + e.printStackTrace(); + } + finally { + if(screen != null) { + try { + +The `close()` call here will restore the terminal by exiting from private mode which was done in +the call to `startScreen()` + + screen.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + +The full code to this tutorial is available in +the [test section of the source code](https://github.com/mabe02/lanterna/blob/master/src/test/java/com/googlecode/lanterna/tutorial/Tutorial03.java) + +[Move on to tutorial 4](Tutorial04.md) diff --git a/mabe-lanterna/docs/tutorial/Tutorial04.md b/mabe-lanterna/docs/tutorial/Tutorial04.md new file mode 100644 index 0000000000000000000000000000000000000000..59b9e2ffd74f58ef22328f4b55c6f712d2a7fef0 --- /dev/null +++ b/mabe-lanterna/docs/tutorial/Tutorial04.md @@ -0,0 +1,176 @@ +Tutorial 4 +--- + +In this forth tutorial we will finally look at creating a multi-window text GUI, all based on text. Just like +the `Screen`-layer in the previous tutorial was based on the lower-level `Terminal` layer, the GUI classes we will +use here are all build upon the Screen interface. Because of this, if you use these classes, you should never +interact with the underlying Screen that backs the GUI directly, as it might modify the screen in a way the +GUI isn't aware of. + +The GUI system is designed around a background surface that is usually static, but can have components, and +multiple windows. The recommended approach it to make all windows modal and not let the user switch between +windows, but the latter can also be done. Components are added to windows by using a layout manager that +determines the position of each component. + + DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory(); + Screen screen = null; + + try { + +The `DefaultTerminalFactory` class does not provide any helper method for creating a Text GUI, you'll need to +get a `Screen` like we did in the previous tutorial and start it so it puts the terminal in private mode. + + screen = terminalFactory.createScreen(); + screen.startScreen(); + +There are a couple of different constructors to `MultiWindowTextGUI`, we are going to go with the defaults for +most of these values. The one thing to consider is threading; with the default options, lanterna will use +the calling thread for all UI operations which mean that you are basically letting the calling thread block +until the GUI is shut down. There is a separate `TextGUIThread` implementation you can use if you'd like +Lanterna to create a dedicated UI thread and not lock the caller. Just like with AWT and Swing, you should +be scheduling any kind of UI operation to always run on the UI thread but lanterna tries to be best-effort +if you attempt to mutate the GUI from another thread. Another default setting that will be applied is that +the background of the GUI will be solid blue. + + final WindowBasedTextGUI textGUI = new MultiWindowTextGUI(screen); + +Creating a new window is relatively uncomplicated, you can optionally supply a title for the window + + final Window window = new BasicWindow("My Root Window"); + +The `Window` has no content initially, you need to call `setComponent(..)` to populate it with something. In this +case, and quite often in fact, you'll want to use more than one component so we'll create a composite +`Panel` component that can hold multiple sub-components. This is where we decide what the layout manager +should be. + + Panel contentPanel = new Panel(new GridLayout(2)); + +Lanterna contains a number of built-in layout managers, the simplest one being `LinearLayout` that simply +arranges components in either a horizontal or a vertical line. In this tutorial, we'll use the `GridLayout` +which is based on the layout manager with the same name in SWT. In the constructor above we have +specified that we want to have a grid with two columns, below we customize the layout further by adding +some spacing between the columns. + + GridLayout gridLayout = (GridLayout)contentPanel.getLayoutManager(); + gridLayout.setHorizontalSpacing(3); + +One of the most basic components is the `Label`, which simply displays a static text. In the example below, +we use the layout data field attached to each component to give the layout manager extra hints about how it +should be placed. Obviously the layout data has to be created from the same layout manager as the container +is using, otherwise it will be ignored. + + Label title = new Label("This is a label that spans two columns"); + title.setLayoutData(GridLayout.createLayoutData( + GridLayout.Alignment.BEGINNING, // Horizontal alignment in the grid cell if the cell is larger than the component's preferred size + GridLayout.Alignment.BEGINNING, // Vertical alignment in the grid cell if the cell is larger than the component's preferred size + true, // Give the component extra horizontal space if available + false, // Give the component extra vertical space if available + 2, // Horizontal span + 1)); // Vertical span + contentPanel.addComponent(title); + +Since the grid has two columns, we can do something like this to add components when we don't need to +customize them any further. + + contentPanel.addComponent(new Label("Text Box (aligned)")); + contentPanel.addComponent( + new TextBox() + .setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER))); + +Here is an example of customizing the regular `TextBox` component so it masks the content and can work for +password input. + + contentPanel.addComponent(new Label("Password Box (right aligned)")); + contentPanel.addComponent( + new TextBox() + .setMask('*') + .setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER))); + +While we are not going to demonstrate all components here, here is an example of `ComboBox`es, one that is +read-only and one that is editable. + + contentPanel.addComponent(new Label("Read-only Combo Box (forced size)")); + List timezonesAsStrings = new ArrayList(); + for(String id: TimeZone.getAvailableIDs()) { + timezonesAsStrings.add(id); + } + ComboBox readOnlyComboBox = new ComboBox(timezonesAsStrings); + readOnlyComboBox.setReadOnly(true); + readOnlyComboBox.setPreferredSize(new TerminalSize(20, 1)); + contentPanel.addComponent(readOnlyComboBox); + + contentPanel.addComponent(new Label("Editable Combo Box (filled)")); + contentPanel.addComponent( + new ComboBox("Item #1", "Item #2", "Item #3", "Item #4") + .setReadOnly(false) + .setLayoutData(GridLayout.createHorizontallyFilledLayoutData(1))); + +Some user interactables, like `Button`s, work by registering callback methods. In this example here, we're +using one of the pre-defined dialogs when the button is triggered. + + contentPanel.addComponent(new Label("Button (centered)")); + contentPanel.addComponent(new Button("Button", new Runnable() { + @Override + public void run() { + MessageDialog.showMessageDialog(textGUI, "MessageBox", "This is a message box", MessageDialogButton.OK); + } + }).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER))); + +Close off with an empty row and a separator, then a button to close the window + + contentPanel.addComponent( + new EmptySpace() + .setLayoutData( + GridLayout.createHorizontallyFilledLayoutData(2))); + contentPanel.addComponent( + new Separator(Direction.HORIZONTAL) + .setLayoutData( + GridLayout.createHorizontallyFilledLayoutData(2))); + contentPanel.addComponent( + new Button("Close", new Runnable() { + @Override + public void run() { + window.close(); + } + }).setLayoutData( + GridLayout.createHorizontallyEndAlignedLayoutData(2))); + +We now have the content panel fully populated with components. A common mistake is to forget to attach it to +the window, so let's make sure to do that. + + window.setComponent(contentPanel); + +Now the `Window` is created and fully populated. As discussed above regarding the threading model, we have the +option to fire off the GUI here and then later on decide when we want to stop it. In order for this to work, +you need a dedicated UI thread to run all the GUI operations, usually done by passing in a +`SeparateTextGUIThread` object when you create the `TextGUI`. In this tutorial, we are using the conceptually +simpler `SameTextGUIThread`, which essentially hijacks the caller thread and uses it as the GUI thread until +some stop condition is met. The absolutely simplest way to do this is to simply ask lanterna to display the +window and wait for it to be closed. This will initiate the event loop and make the GUI functional. In the +"Close" button above, we tied a call to the `close()` method on the Window object when the button is +triggered, this will then break the even loop and our call finally returns. + + textGUI.addWindowAndWait(window); + +When our call has returned, the window is closed and no longer visible. The screen still contains the last +state the `TextGUI` left it in, so we can easily add and display another window without any flickering. In +this case, we want to shut down the whole thing and return to the ordinary prompt. We just need to stop the +underlying `Screen` for this, the `TextGUI` system does not require any additional disassembly. + + } + catch (IOException e) { + e.printStackTrace(); + } + finally { + if(screen != null) { + try { + screen.stopScreen(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + +The full code to this tutorial is available in +the [test section of the source code](https://github.com/mabe02/lanterna/blob/master/src/test/java/com/googlecode/lanterna/tutorial/Tutorial04.java) diff --git a/mabe-lanterna/docs/using-gui.md b/mabe-lanterna/docs/using-gui.md new file mode 100644 index 0000000000000000000000000000000000000000..0bd8a0923a24e724a76d9e4d4efb83d94d4e7c46 --- /dev/null +++ b/mabe-lanterna/docs/using-gui.md @@ -0,0 +1,11 @@ +# Text GUI + +_Please note: You should read the [Buffered screen API](using-screen.md) guide before you start reading this guide_ + +The guide on how to use the GUI is a bit too long for one page so it's been split up: + +* [Start the GUI](GUIGuideStartTheGUI.md) +* [Basic windows](GUIGuideWindows.md) +* [Components](GUIGuideComponents.md) +* [Built-in dialogs](GUIGuideDialogs.md) +* [Misc](GUIGuideMisc.md) \ No newline at end of file diff --git a/mabe-lanterna/docs/using-screen.md b/mabe-lanterna/docs/using-screen.md new file mode 100644 index 0000000000000000000000000000000000000000..d9585515ac529549b0d3e3703219f0f0bd462136 --- /dev/null +++ b/mabe-lanterna/docs/using-screen.md @@ -0,0 +1,106 @@ +# Buffered screen API + +_Please note: You should read the [Direct terminal access](using-terminal.md) guide before you start reading this guide_ + +## About the Screen layer + +When creating text-based GUIs using low-level operations, it's very likely that the advantages of buffering screen data +will be apparent. Instead of redrawing the whole screen every time something changes, wouldn't it be better to calculate +the difference and only apply the changes there? The `Screen` layer is doing precisely this; keeping a back buffer that +you write to and then allow you to perform a refresh operation that will calculate and update the differences between +your back buffer and the visible screen. + +## Getting the `Screen` object + +In order to use the Screen layer, you will need a `com.googlecode.lanterna.screen.Screen` object. Just like the +`Terminal` object was the basis for all operations in the lowest layer, you will use the `Screen` object here. To create +a `Screen` object, you'll need an already existing `Terminal`. Create the `Screen` like this: + + Screen screen = new TerminalScreen(terminal); + +### Using the `DefaultTerminalFactory` + +Just like with the Terminal layer, you can also use the `DefaultTerminalFactory` class to quickly create a `Screen`. +Please see JavaDoc documentation for more information about the `DefaultTerminalFactory` class. + + Screen screen = new DefaultTerminalFactory().createScreen(); + +## Start the screen + +Instead of the notion of _private mode_, using the Screen layer requires you to _start_ the screen before you use it. +In the same way you can also stop the screen when the application is done (or at least doesn't need the screen anymore). + + screen.startScreen(); + + // do text GUI application logic here until done + + screen.stopScreen(); + +## Drawing text ## + +The `Screen` object exposes a `setCharacter(..)` method that will put a certain character at a certain position with +certain attributes (color and style). It's very straight-forward: + + screen.setCharacter(10, 5, new TextCharacter('!', TextColor.ANSI.RED, TextColor.ANSI.GREEN)); + +When drawing full strings, it's easier to use the `TextGraphics` helper interface instead. + + TextGraphics textGraphics = screen.newTextGraphics(); + textGraphics.setForegroundColor(TextColor.ANSI.RED); + textGraphics.setBackgroundColor(TextColor.ANSI.GREEN); + textGraphics.putString(10, 5, "Hello Lanterna!"); + +After writing text to the screen, in order to make it show up you have to call the `refresh()` method. + + screen.refresh(); + +## Reading input from the keyboard + +The Screen also exposes the `pollInput()` and `readInput()` methods from `InputProvider`, which in this case will be +delegated to the underlying terminal. Please read the section on keyboard input +in [Direct terminal access](using-terminal.md). + +## Clearing the screen + +The correct way to reset a `Screen` is to call `clear()` on it. + + //Let's say the terminal contains some random characters... + screen.clear(); + screen.refresh(); + //The terminal is now blank to the user + +## Refreshing the screen + +As have been noted above, when you have been modifying your screen you need to call the `refresh()` method to make the +changes show up. This is because the `Screen` will keep an in-memory buffer of the terminal window. When you draw +strings to the screen, you actually modify the buffer and not the the real terminal. Calling `refresh()` will make the +screen compare what's on the screen and what's in the buffer, and output commands to the underlying `Terminal` so that +the screen looks like the buffer. This way, you can print text on the same location over and over and in the end won't +waste anything when the changes are flushed to standard out (or whatever your terminal is using). + +## Handling terminal resize + +Screens will automatically listen and record size changes, but you have to let the Screen know when is +a good time to update its internal buffers. Usually you should do this at the start of your "drawing" +loop, if you have one. This ensures that the dimensions of the buffers stays constant and doesn't change +while you are drawing content. The method doReizeIfNecessary() will check if the terminal has been +resized since last time it was called (or since the screen was created if this is the first time +calling) and update the buffer dimensions accordingly. It returns null if the terminal has not changed +size since last time. + + TerminalSize newSize = screen.doResizeIfNecessary(); + if(newSize != null) { + terminalSize = newSize; + } + +## Manipulating the underlying terminal + +The only way a `Screen` can know what visible in the real terminal window is by remembering what the buffer looked like +before the last refresh. When you modify the underlying directly terminal, this buffer-memory will get out of sync and +the `refresh()` is likely to not properly update the content. So if you use `Screen`, a general rule of thumb is to +avoid operating on the `Terminal` you passed in when creating the `Screen`. + +## Learn more + +To learn about how to use the bundled text GUI API in Lanterna built on top of the screen API described in this page, +continue reading at [Text GUI](using-gui.md). \ No newline at end of file diff --git a/mabe-lanterna/docs/using-terminal.md b/mabe-lanterna/docs/using-terminal.md new file mode 100644 index 0000000000000000000000000000000000000000..f381851e79cd922600c1af7178ffb26826e41f08 --- /dev/null +++ b/mabe-lanterna/docs/using-terminal.md @@ -0,0 +1,243 @@ +# Direct terminal access + +## Introduction + +The `Terminal` layer is the lowest level available in Lanterna, giving you very precise control of what data is sent to +the client. You will find these classes in the `com.googlecode.lanterna.terminal` package. + +## Getting the `Terminal` object + +The core of the Terminal layer is the `Terminal` interface which you must retrieve. This can be done either by directly +instantiating one of the implementing classes (for example `UnixTerminal` or `SwingTerminalFrame`) or by using the +`com.googlecode.lanterna.terminal.DefaultTerminalFactory` class which auto-detects and guesses what kind of +implementation is +most suitable given the system environment. + +### Different implementations + +in Lanterna, a terminal is something that implements `Terminal` and with a correct implementation of the interface, the +entire stack (`Screen` and `TextGUI`) will work on top of it without any modifications. The regular implementation of +`Terminal` can be thought of as the `UnixTerminal` class, which translates the interface calls on `Terminal` to the +ANSI control sequences required to perform those operations, by default to `stdout` and reading from `stdin`. But +lanterna also contains a simple built-in terminal emulator that implements the `Terminal` interface, accessible through +`SwingTerminal` and `AWTTerminal` (and related helper classes) that will open a new window containing the terminal and +each API call is directly mutating the internal state of this emulator. There is also a build-in telnet server that +presents incoming clients as a `Terminal` object that you can control. + +### Using `DefaultTerminalFactory` to choose implementation + +By using the `DefaultTerminalFactory` class, you can use some build-in code that tries to auto-detect the system +environment during runtime. The encoding will be picked up through the `file.encoding` system property (which is +automatically set by the JVM) and the type of `Terminal` implementation to use will be decided based on if the system +has a graphical environment (then use `SwingTerminal`) or not (then use `UnixTerminal`). The idea for this class is to +help you create an application that works on both a graphical system and a headless system without requiring any change +of configuration or recompilation. Also, if you are on a system with a windowing environment but want to force a +text-based (`stdout`/`stdin`) terminal, you can pass the following option to the JRE: + + java -Djava.awt.headless=true ... + +While not exposed on the `TerminalFactory` interface (that `DefaultTerminalFactory` implements), +`DefaultTerminalFactory` has a number of extra methods you can use to further customize the terminals it creates. + +#### Example + + Terminal terminal = new DefaultTerminalFactory(System.out, System.in, Charset.forName("UTF8")).createTerminal(); + +or you can do just this which will use `stdout`, `stdin` and the platform encoding. + + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + +###### Note: + +On Windows, you need to use [javaw](http://pages.citebite.com/p6q0p5r4h7sny) to start your application or +`IOException` will be thrown while invoking `DefaultTerminalFactory.createTerminal()`, see mabe02/lanterna#335. + +## Entering and exiting private mode + +Before you can print any text or start to move the cursor around, you should enter what's called the _private mode_. In +this mode the screen is cleared, scrolling is disabled and the previous content is stored away. When you exit private +mode, the previous content is restored and displayed on screen again as it was before private mode was entered. Some +systems/terminals doesn't support this mode at all, but will still perform some screen cleaning operations for you. +It's always recommended to enter private mode when you start your GUI and exit when you finish. + +### Example + + terminal.enterPrivateMode(); + ... + terminal.exitPrivateMode(); + +## Moving the cursor + +When you start your GUI, you can never make any assumptions on where the text cursor is. Since text printing will always +appear at the text cursor position, it is very important to be able to move this around. Here is how you do it: + + terminal.setCursorPosition(10, 5); + +The first parameter is to which column (the first is 0) and the record to which row (again, first row is 0). + +## Get the size of the terminal + +In order to be able to make good decisions on moving the cursor, you might want to know how big the terminal is. The +`Terminal` object will expose a `getTerminalSize()` method that will do precisely this. + + TerminalSize screenSize = terminal.getTerminalSize(); + + //Place the cursor in the bottom right corner + terminal.setCursorPosition(screenSize.getColumns() - 1, screenSize.getRows() - 1); + +## Printing text + +Printing text is another very useful operation, it's simple enough but a bit limited as you have to print character by +character. + + terminal.setCursorPosition(10, 5); + terminal.putCharacter('H'); + terminal.putCharacter('e'); + terminal.putCharacter('l'); + terminal.putCharacter('l'); + terminal.putCharacter('o'); + terminal.putCharacter('!'); + terminal.setCursorPosition(0, 0); + +Notice that just like when you type in text manually, the cursor position will move one column to the right for every +character you put. What happens after you put a character on the last column is undefined and may differ between +different terminal emulators. You should always use the `moveCursor(..)` method to place the cursor somewhere else after +writing something to the end of the row. + +## Using colors + +If you want to make your text a bit more beautiful, try adding some color! The underlying terminal emulator will keep a +state of various modifiers, including which foreground color and which background color is currently active, so after +modifying this state it will be applied to all text written until you change it again. + +The following colors are the ANSI standard ones, most of which has a brighter version also available (see _bold_ mode): + +* BLACK +* RED +* GREEN +* YELLOW +* BLUE +* MAGENTA +* CYAN +* WHITE +* DEFAULT + +Notice the default color, which is up to the terminal emulator to decide, you can't know what it will be. There are two +xterm color extensions supported by some terminal emulators which you can use through Lanterna (indexed mode and RGB) +but avoid doing this as most terminal emulators won't understand them. + +### Example + + terminal.setForegroundColor(TextColor.ANSI.RED); + terminal.setBackgroundColor(TextColor.ANSI.BLUE); + + //Print something + + terminal.setForegroundColor(TextColor.ANSI.DEFAULT); + terminal.setBackgroundColor(TextColor.ANSI.DEFAULT); + +## Using text styles + +More than color, you can also add certain styles to the text. How well this is supported depends completely on the +client terminal so results may vary. Typically at least bold mode is supported, but usually not rendering the text in +bold font but rather as a brighter color. These style modes are referred to as SGR and just like with colors the +terminal emulator will keep their state until you explicitly change it. The following SGR are available in lanterna as +of version 3: + +* `BOLD` - text usually drawn in a brighter color rather than in bold font +* `REVERSE` - Reverse text mode, will flip the foreground and background colors +* `UNDERLINE` - Draws a horizontal line under the text. Surprisingly not widely supported. +* `BLINK` - Text will blink on the screen by alternating the foreground color between the real foreground color and the + background color. Not widely supported but your users will love it! +* `BORDERED` - Draws a border around the text. Rarely supported. +* `FRAKTUR` - I have no idea, exotic extension, please send me a reference screen shots! +* `CROSSED_OUT` - Draws a horizontal line through the text. Rarely supported. +* `CIRCLED` - Draws a circle around the text. Rarely supported. +* `ITALIC` - Italic (cursive) text mode. Some terminals seem to support it, but rarely encountered in use + +Here is how you apply the SGR states: + + terminal.enableSGR(SGR.BOLD); + + //Draw text highlighted + + terminal.disableSGR(SGR.BOLD); //or terminal.resetColorAndSGR() + +The `Terminal` interface method `resetColorAndSGR()` is the preferred way to reset all colors and SGR states. + +## Clearing the screen + +If you want to completely reset the screen, you can use the `clearScreen()` method. This will (probably) remove all +characters on the screen and replace them with the empty character (' ') having `TextColor.ANSI.DEFAULT` set as +background. + + terminal.clearScreen(); + +## Flushing + +To be sure that the text has been sent to the client terminal, you should call the `flush()` method on the `Terminal` +interface when you have done all your operations. + +## Read keyboard input + +Finally, retrieving user input is necessary unless the GUI is totally hands-off. Normally, when the user presses a key, +it will be added to an input buffer awaiting retrieval. You can poll this buffer by calling `pollInput()` on the +`Terminal` object (blocking version also exists: `readInput()`), which will return you a `KeyStroke` object representing +the key pressed, or null is no key has been pressed. The `pollInput()` method is actually not directly on the `Terminal` +interface, but on another interface, `InputProvider`, which the `Terminal` interface extends. To interpret the +`KeyStroke` returned, you first need to call `getKeyType()` which returns a `KeyType` enum value that tells you what +kind of key was pressed. The `KeyType` enum has the following values as of Lanterna 3: + +* Character +* Escape +* Backspace +* ArrowLeft +* ArrowRight +* ArrowUp +* ArrowDown +* Insert +* Delete +* Home +* End +* PageUp +* PageDown +* Tab +* ReverseTab +* Enter +* F1 +* F2 +* F3 +* F4 +* F5 +* F6 +* F7 +* F8 +* F9 +* F10 +* F11 +* F12 +* F13 +* F14 +* F15 +* F16 +* F17 +* F18 +* F19 +* Unknown +* CursorLocation +* MouseEvent +* EOF + +If you get the type `KeyType.Character`, the key is then a alpha-numerical-symbol key and it can be retrieved using the +`KeyStroke` object's `getCharacter()` method. At the moment, there is no way to detect when the shift-key is pressed, +you will see the result on `KeyStroke` objects read while the key is pressed though (characters will be in upper-case, +numbers will turn into symbols, etc). You can detect if CTRL and/or ALT keys were pressed down while the keystroke +happened, by using the `isAltDown()` and `isCtrlDown()` methods on `KeyStroke` (unfortunately, not all combinations +can be detected because of ambiguities that sometimes occur; if you want to use special key combinations in your +program, please test with multiple terminals to make sure it's working as you intend). Unfortunately, you cannot detect +CTRL or ALT as individual keystrokes due to how the terminal is designed to work + +## Learn more + +To learn about how to use an abstraction API in Lanterna built on top of the terminal API described in this page, +continue reading at [Buffered screen API](using-screen.md). diff --git a/mabe-lanterna/licenseheader.txt b/mabe-lanterna/licenseheader.txt new file mode 100644 index 0000000000000000000000000000000000000000..af92df813b3fc40bcd0abf64fcdf5af249262436 --- /dev/null +++ b/mabe-lanterna/licenseheader.txt @@ -0,0 +1,18 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ \ No newline at end of file diff --git a/mabe-lanterna/native-integration/pom.xml b/mabe-lanterna/native-integration/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..b7dc266e64b913f2184da0f48a1516ef0582d661 --- /dev/null +++ b/mabe-lanterna/native-integration/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + com.googlecode.lanterna + lanterna-native-integration + 3.0.0-SNAPSHOT + + + + com.googlecode.lanterna + lanterna + 3.0.0 + + + net.java.dev.jna + jna + 4.4.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + \ No newline at end of file diff --git a/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/NativeGNULinuxTerminal.java b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/NativeGNULinuxTerminal.java new file mode 100644 index 0000000000000000000000000000000000000000..75376c00ddc86a9eb271cc1b9347d80425e24862 --- /dev/null +++ b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/NativeGNULinuxTerminal.java @@ -0,0 +1,126 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.terminal; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.terminal.ansi.UnixLikeTerminal; +import com.sun.jna.Native; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import static com.googlecode.lanterna.terminal.PosixLibC.*; + +/** + * Terminal implementation that uses native libraries + */ +public class NativeGNULinuxTerminal extends UnixLikeTerminal { + + private final PosixLibC libc; + private PosixLibC.termios savedTerminalState; + + public NativeGNULinuxTerminal() throws IOException { + this(System.in, + System.out, + Charset.defaultCharset(), + CtrlCBehaviour.CTRL_C_KILLS_APPLICATION); + } + + public NativeGNULinuxTerminal( + InputStream terminalInput, + OutputStream terminalOutput, + Charset terminalCharset, + CtrlCBehaviour terminalCtrlCBehaviour) throws IOException { + + super(terminalInput, + terminalOutput, + terminalCharset, + terminalCtrlCBehaviour); + + + this.libc = (PosixLibC) Native.loadLibrary("c", PosixLibC.class); + this.savedTerminalState = null; + } + + public void saveTerminalSettings() throws IOException { + savedTerminalState = getTerminalState(); + } + + public void restoreTerminalSettings() throws IOException { + if(savedTerminalState != null) { + libc.tcsetattr(STDIN_FILENO, TCSANOW, savedTerminalState); + } + } + + public void keyEchoEnabled(boolean b) throws IOException { + PosixLibC.termios state = getTerminalState(); + if(b) { + state.c_lflag |= ECHO; + } + else { + state.c_lflag &= ~ECHO; + } + libc.tcsetattr(STDIN_FILENO, TCSANOW, state); + } + + public void canonicalMode(boolean b) throws IOException { + PosixLibC.termios state = getTerminalState(); + if(b) { + state.c_lflag |= ICANON; + } + else { + state.c_lflag &= ~ICANON; + } + libc.tcsetattr(STDIN_FILENO, TCSANOW, state); + } + + public void keyStrokeSignalsEnabled(boolean b) throws IOException { + PosixLibC.termios state = getTerminalState(); + if(b) { + state.c_lflag |= ISIG; + } + else { + state.c_lflag &= ~ISIG; + } + libc.tcsetattr(STDIN_FILENO, TCSANOW, state); + } + + public void registerTerminalResizeListener(final Runnable runnable) throws IOException { + libc.signal(SIGWINCH, new sig_t() { + public synchronized void invoke(int signal) { + runnable.run(); + } + }); + } + + @Override + protected TerminalSize findTerminalSize() throws IOException { + PosixLibC.winsize winsize = new winsize(); + libc.ioctl(PosixLibC.STDOUT_FILENO, PosixLibC.TIOCGWINSZ, winsize); + return new TerminalSize(winsize.ws_col, winsize.ws_row); + } + + private PosixLibC.termios getTerminalState() { + PosixLibC.termios termios = new PosixLibC.termios(); + libc.tcgetattr(STDIN_FILENO, termios); + return termios; + } +} diff --git a/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/PosixLibC.java b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/PosixLibC.java new file mode 100644 index 0000000000000000000000000000000000000000..f4cf36c1fc9d1bfe187d6f8008bc44c67840e0a8 --- /dev/null +++ b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/PosixLibC.java @@ -0,0 +1,125 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.terminal; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Structure; + +import java.util.Arrays; +import java.util.List; + +/** + * Interface to Posix libc + */ +public interface PosixLibC extends Library { + int tcgetattr(int fd, termios termios_p); + int tcsetattr(int fd, int optional_actions, termios termios_p); + int ioctl(int fd, int request, winsize winsize); + sig_t signal(int sig, sig_t fn); + + // Constants + int STDIN_FILENO = 0; + int STDOUT_FILENO = 1; + int TCSANOW = 0; + int NCCS = 32; + + // Constants for c_lflag (beware of octal numbers below!!) + @SuppressWarnings("OctalInteger") + int ISIG = 01; + @SuppressWarnings("OctalInteger") + int ICANON = 02; + @SuppressWarnings("OctalInteger") + int ECHO = 010; + + // Signals + int SIGWINCH = 28; + + // Constants for ioctl + int TIOCGWINSZ = 0x5413; + + interface sig_t extends Callback { + void invoke(int signal); + } + + class termios extends Structure { + public int c_iflag; // input mode flags + public int c_oflag; // output mode flags + public int c_cflag; // control mode flags + public int c_lflag; // local mode flags + public byte c_line; // line discipline + public byte c_cc[]; // control characters + public int c_ispeed; // input speed + public int c_ospeed; // output speed + + public termios() { + c_cc = new byte[NCCS]; + } + + protected List getFieldOrder() { + return Arrays.asList( + "c_iflag", + "c_oflag", + "c_cflag", + "c_lflag", + "c_line", + "c_cc", + "c_ispeed", + "c_ospeed" + ); + } + + @Override + public String toString() { + return "termios{" + + "c_iflag=" + c_iflag + + ", c_oflag=" + c_oflag + + ", c_cflag=" + c_cflag + + ", c_lflag=" + c_lflag + + ", c_line=" + c_line + + ", c_cc=" + Arrays.toString(c_cc) + + ", c_ispeed=" + c_ispeed + + ", c_ospeed=" + c_ospeed + + '}'; + } + } + + class winsize extends Structure + { + public short ws_row; + public short ws_col; + public short ws_xpixel; + public short ws_ypixel; + + @Override + protected List getFieldOrder() { + return Arrays.asList("ws_row", "ws_col", "ws_xpixel", "ws_ypixel"); + } + + @Override + public String toString() { + return "winsize{" + + "ws_row=" + ws_row + + ", ws_col=" + ws_col + + ", ws_xpixel=" + ws_xpixel + + ", ws_ypixel=" + ws_ypixel + + '}'; + } + } +} diff --git a/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WinDef.java b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WinDef.java new file mode 100644 index 0000000000000000000000000000000000000000..6e5c86e237feb7dcd4735151bd0fbcd0fcdb33fb --- /dev/null +++ b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WinDef.java @@ -0,0 +1,142 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.terminal; + +import com.sun.jna.*; + +import java.util.Arrays; +import java.util.List; + +/** + * Class containing common Win32 structures involved when operating on the terminal + */ +public class WinDef { + + public static final HANDLE INVALID_HANDLE_VALUE = new HANDLE(Pointer.createConstant(Pointer.SIZE == 8?-1L:4294967295L)); + + public static class HANDLE extends PointerType { + private boolean immutable; + + public HANDLE() { + } + + public HANDLE(Pointer p) { + this.setPointer(p); + this.immutable = true; + } + + public Object fromNative(Object nativeValue, FromNativeContext context) { + Object o = super.fromNative(nativeValue, context); + return INVALID_HANDLE_VALUE.equals(o) ? INVALID_HANDLE_VALUE : o; + } + + public void setPointer(Pointer p) { + if(this.immutable) { + throw new UnsupportedOperationException("immutable reference"); + } else { + super.setPointer(p); + } + } + + public String toString() { + return String.valueOf(this.getPointer()); + } + } + + public static class WORD extends IntegerType implements Comparable { + public static final int SIZE = 2; + + public WORD() { + this(0L); + } + + public WORD(long value) { + super(2, value, true); + } + + public int compareTo(WORD other) { + return compare(this, other); + } + } + + public static class COORD extends Structure { + public short X; + public short Y; + + @Override + protected List getFieldOrder() { + return Arrays.asList("X", "Y"); + } + + @Override + public String toString() { + return "COORD{" + + "X=" + X + + ", Y=" + Y + + '}'; + } + } + + public static class SMALL_RECT extends Structure { + public short Left; + public short Top; + public short Right; + public short Bottom; + + @Override + protected List getFieldOrder() { + return Arrays.asList("Left", "Top", "Right", "Bottom"); + } + + @Override + public String toString() { + return "SMALL_RECT{" + + "Left=" + Left + + ", Top=" + Top + + ", Right=" + Right + + ", Bottom=" + Bottom + + '}'; + } + } + + public static class CONSOLE_SCREEN_BUFFER_INFO extends Structure { + public COORD dwSize; + public COORD dwCursorPosition; + public WORD wAttributes; + public SMALL_RECT srWindow; + public COORD dwMaximumWindowSize; + + protected List getFieldOrder() { + return Arrays.asList("dwSize", "dwCursorPosition", "wAttributes", "srWindow", "dwMaximumWindowSize"); + } + + @Override + public String toString() { + return "CONSOLE_SCREEN_BUFFER_INFO{" + + "dwSize=" + dwSize + + ", dwCursorPosition=" + dwCursorPosition + + ", wAttributes=" + wAttributes + + ", srWindow=" + srWindow + + ", dwMaximumWindowSize=" + dwMaximumWindowSize + + '}'; + } + } + + private WinDef() {} +} diff --git a/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/Wincon.java b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/Wincon.java new file mode 100644 index 0000000000000000000000000000000000000000..24e9b88b325f1562811f1b0bc02f16d1749c6f9f --- /dev/null +++ b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/Wincon.java @@ -0,0 +1,44 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.terminal; + +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.win32.StdCallLibrary; + +/** + * Interface to Wincon, module in Win32 that can operate on the terminal + */ +interface Wincon extends StdCallLibrary { + int STD_INPUT_HANDLE = -10; + int STD_OUTPUT_HANDLE = -11; + + // SetConsoleMode input values + int ENABLE_PROCESSED_INPUT = 1; + int ENABLE_LINE_INPUT = 2; + int ENABLE_ECHO_INPUT = 4; + + // SetConsoleMode screen buffer values + int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4; + int DISABLE_NEWLINE_AUTO_RETURN = 8; + + WinDef.HANDLE GetStdHandle(int var1); + boolean GetConsoleMode(WinDef.HANDLE var1, IntByReference var2); + boolean SetConsoleMode(WinDef.HANDLE var1, int var2); + boolean GetConsoleScreenBufferInfo(WinDef.HANDLE hConsoleOutput, WinDef.CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo); +} diff --git a/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WindowsTerminal.java b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WindowsTerminal.java new file mode 100644 index 0000000000000000000000000000000000000000..3018df3d19109b9f80714680ae5c92ade76b2cd5 --- /dev/null +++ b/mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WindowsTerminal.java @@ -0,0 +1,137 @@ +package com.googlecode.lanterna.terminal; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.terminal.ansi.UnixLikeTerminal; +import com.sun.jna.Native; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.win32.W32APIOptions; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Terminal implementation for the regular Windows cmd.exe terminal emulator, using native invocations through jna to + * interact with it. + */ +public class WindowsTerminal extends UnixLikeTerminal { + + private static final Wincon WINDOWS_CONSOLE = (Wincon) Native.loadLibrary("kernel32", Wincon.class, W32APIOptions.UNICODE_OPTIONS); + private static final WinDef.HANDLE CONSOLE_INPUT_HANDLE = WINDOWS_CONSOLE.GetStdHandle(Wincon.STD_INPUT_HANDLE); + private static final WinDef.HANDLE CONSOLE_OUTPUT_HANDLE = WINDOWS_CONSOLE.GetStdHandle(Wincon.STD_OUTPUT_HANDLE); + + private Integer savedTerminalInputMode; + private Integer savedTerminalOutputMode; + + public WindowsTerminal() throws IOException { + this(System.in, System.out, Charset.defaultCharset(), CtrlCBehaviour.CTRL_C_KILLS_APPLICATION); + } + + public WindowsTerminal( + InputStream terminalInput, + OutputStream terminalOutput, + Charset terminalCharset, + CtrlCBehaviour terminalCtrlCBehaviour) throws IOException { + + super(terminalInput, + terminalOutput, + terminalCharset, + terminalCtrlCBehaviour); + } + + @Override + protected void acquire() throws IOException { + super.acquire(); + int terminalOutputMode = getConsoleOutputMode(); + terminalOutputMode |= Wincon.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + terminalOutputMode |= Wincon.DISABLE_NEWLINE_AUTO_RETURN; + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_OUTPUT_HANDLE, terminalOutputMode); + } + + @Override + public synchronized void saveTerminalSettings() throws IOException { + this.savedTerminalInputMode = getConsoleInputMode(); + this.savedTerminalOutputMode = getConsoleOutputMode(); + } + + @Override + public synchronized void restoreTerminalSettings() throws IOException { + if(savedTerminalInputMode != null) { + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_INPUT_HANDLE, savedTerminalInputMode); + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_OUTPUT_HANDLE, savedTerminalOutputMode); + } + } + + @Override + public synchronized void keyEchoEnabled(boolean enabled) throws IOException { + int mode = getConsoleInputMode(); + if(enabled) { + mode |= Wincon.ENABLE_ECHO_INPUT; + } + else { + mode &= ~Wincon.ENABLE_ECHO_INPUT; + } + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_INPUT_HANDLE, mode); + } + + @Override + public synchronized void canonicalMode(boolean enabled) throws IOException { + int mode = getConsoleInputMode(); + if(enabled) { + mode |= Wincon.ENABLE_LINE_INPUT; + } + else { + mode &= ~Wincon.ENABLE_LINE_INPUT; + } + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_INPUT_HANDLE, mode); + } + + @Override + public synchronized void keyStrokeSignalsEnabled(boolean enabled) throws IOException { + int mode = getConsoleInputMode(); + if(enabled) { + mode |= Wincon.ENABLE_PROCESSED_INPUT; + } + else { + mode &= ~Wincon.ENABLE_PROCESSED_INPUT; + } + WINDOWS_CONSOLE.SetConsoleMode(CONSOLE_INPUT_HANDLE, mode); + } + + + @Override + protected TerminalSize findTerminalSize() throws IOException { + WinDef.CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo = new WinDef.CONSOLE_SCREEN_BUFFER_INFO(); + WINDOWS_CONSOLE.GetConsoleScreenBufferInfo(CONSOLE_OUTPUT_HANDLE, screenBufferInfo); + int columns = screenBufferInfo.srWindow.Right - screenBufferInfo.srWindow.Left + 1; + int rows = screenBufferInfo.srWindow.Bottom - screenBufferInfo.srWindow.Top + 1; + return new TerminalSize(columns, rows); + } + + @Override + public void registerTerminalResizeListener(Runnable runnable) throws IOException { + // Not implemented yet + } + + public synchronized TerminalPosition getCursorPosition() { + WinDef.CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo = new WinDef.CONSOLE_SCREEN_BUFFER_INFO(); + WINDOWS_CONSOLE.GetConsoleScreenBufferInfo(CONSOLE_OUTPUT_HANDLE, screenBufferInfo); + int column = screenBufferInfo.dwCursorPosition.X - screenBufferInfo.srWindow.Left; + int row = screenBufferInfo.dwCursorPosition.Y - screenBufferInfo.srWindow.Top; + return new TerminalPosition(column, row); + } + + private int getConsoleInputMode() { + IntByReference lpMode = new IntByReference(); + WINDOWS_CONSOLE.GetConsoleMode(CONSOLE_INPUT_HANDLE, lpMode); + return lpMode.getValue(); + } + + private int getConsoleOutputMode() { + IntByReference lpMode = new IntByReference(); + WINDOWS_CONSOLE.GetConsoleMode(CONSOLE_OUTPUT_HANDLE, lpMode); + return lpMode.getValue(); + } +} diff --git a/mabe-lanterna/pom.xml b/mabe-lanterna/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..7431dc8735a17440997eccee32cd9ac89b2401f0 --- /dev/null +++ b/mabe-lanterna/pom.xml @@ -0,0 +1,238 @@ + + 4.0.0 + com.googlecode.lanterna + lanterna + jar + Lanterna + Java library for creating text-based terminal GUIs + 3.2.0-SNAPSHOT + https://github.com/mabe02/lanterna + + + + GNU Lesser General Public License + http://www.gnu.org/licenses/lgpl-3.0.txt + repo + + + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + scm:git:https://github.com/mabe02/lanterna.git + scm:git:https://github.com/mabe02/lanterna.git + https://github.com/mabe02/lanterna + + + + + + net.java.dev.jna + jna + 5.5.0 + true + + + net.java.dev.jna + jna-platform + 5.5.0 + true + + + + + junit + junit + 4.13.1 + test + + + com.sun.xml.ws + jaxws-ri + 2.3.2 + pom + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + default-compile + + compile + + + + java9 + + compile + + compile + + 1.9 + 1.9 + 9 + true + + ${basedir}/src/main/java9 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + test-jar + + + + + + + true + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-maven + + enforce + + + + + 3.3.9 + + + + + + + + org.codehaus.mojo + clirr-maven-plugin + 2.8 + + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + + + + + Martin + Martin Berglund + mabe02@gmail.com + + + diff --git a/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/DrawRectangle.java b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/DrawRectangle.java new file mode 100644 index 0000000000000000000000000000000000000000..b370a76d8bd28509cde87567dbfa734eada7cecd --- /dev/null +++ b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/DrawRectangle.java @@ -0,0 +1,40 @@ +package com.googlecode.lanterna.examples; + +import java.io.IOException; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.graphics.TextGraphics; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + + +/** + * Creates a TerminalScreen and a TextGraphics from it and writes a rectangle + * from the character '*' + * + * @author Peter Borkuti + * + */ +public class DrawRectangle { + + public static void main(String[] args) throws IOException { + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + + TextGraphics tGraphics = screen.newTextGraphics(); + + screen.startScreen(); + screen.clear(); + + tGraphics.drawRectangle( + new TerminalPosition(3,3), new TerminalSize(10,10), '*'); + screen.refresh(); + + screen.readInput(); + screen.stopScreen(); + } + +} diff --git a/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputChar.java b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputChar.java new file mode 100644 index 0000000000000000000000000000000000000000..6c4e6b87e33ee6c72a9805ac0abe4543b9618500 --- /dev/null +++ b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputChar.java @@ -0,0 +1,35 @@ +package com.googlecode.lanterna.examples; + +import java.io.IOException; + +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + + +/** + * Creates a terminal and prints a '*' to the (10,10) position. + * Waits for a keypress then exit. + * + * @author Peter Borkuti + * + */ +public class OutputChar { + + public static void main(String[] args) throws IOException { + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + + screen.startScreen(); + screen.clear(); + + screen.setCharacter(10, 10, new TextCharacter('*')); + screen.refresh(); + + screen.readInput(); + screen.stopScreen(); + } + +} diff --git a/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputString.java b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputString.java new file mode 100644 index 0000000000000000000000000000000000000000..a8fb28dce7c5383ca3819f495d9eea53f9c709e7 --- /dev/null +++ b/mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputString.java @@ -0,0 +1,31 @@ +package com.googlecode.lanterna.examples; + +import java.io.IOException; + +import com.googlecode.lanterna.graphics.TextGraphics; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.Terminal; + + +public class OutputString { + + public static void main(String[] args) throws IOException { + Terminal terminal = new DefaultTerminalFactory().createTerminal(); + Screen screen = new TerminalScreen(terminal); + + String s = "Hello World!"; + TextGraphics tGraphics = screen.newTextGraphics(); + + screen.startScreen(); + screen.clear(); + + tGraphics.putString(10, 10, s); + screen.refresh(); + + screen.readInput(); + screen.stopScreen(); + } + +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/SGR.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/SGR.java new file mode 100644 index 0000000000000000000000000000000000000000..db8406978d6502ad2d9e9cc51194dee15ec965e6 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/SGR.java @@ -0,0 +1,75 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +/** + * SGR - Select Graphic Rendition, changes the state of the terminal as to what kind of text to print after this + * command. When working with the Terminal interface, its keeping a state of which SGR codes are active, so activating + * one of these codes will make it apply to all text until you explicitly deactivate it. When you work with Screen and + * GUI systems, usually the SGR is a property of an independent character and won't affect others. + */ +public enum SGR { + /** + * Bold text mode. Please note that on some terminal implementations, instead of (or in addition to) making the text + * bold, it will draw the text in a slightly different color + */ + BOLD, + + /** + * Reverse text mode, will flip the foreground and background colors while active + */ + REVERSE, + + /** + * Draws a horizontal line under the text. Not widely supported. + */ + UNDERLINE, + + /** + * Text will blink on the screen by alternating the foreground color between the real foreground color and the + * background color. Not widely supported. + */ + BLINK, + + /** + * Draws a border around the text. Rarely supported. + */ + BORDERED, + + /** + * I have no idea, exotic extension, please send me a reference screen shots! + */ + FRAKTUR, + + /** + * Draws a horizontal line through the text. Rarely supported. + */ + CROSSED_OUT, + + /** + * Draws a circle around the text. Rarely supported. + */ + CIRCLED, + + /** + * Italic (cursive) text mode. Some Terminal seem to support it. + */ + ITALIC, + ; +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/Symbols.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/Symbols.java new file mode 100644 index 0000000000000000000000000000000000000000..943afecb6b216218930686d248312dfc658d3800 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/Symbols.java @@ -0,0 +1,328 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ + +package com.googlecode.lanterna; + +/** + * Some text graphics, taken from http://en.wikipedia.org/wiki/Codepage_437 but converted to its UTF-8 counterpart. + * This class it mostly here to help out with building text GUIs when you don't have a handy Unicode chart available. + * Previously this class was known as ACS, which was taken from ncurses (meaning "Alternative Character Set"). + * + * @author martin + */ +public class Symbols { + private Symbols() { + } + + /** + * ☺ + */ + public static final char FACE_WHITE = 0x263A; + /** + * ☻ + */ + public static final char FACE_BLACK = 0x263B; + /** + * ♥ + */ + public static final char HEART = 0x2665; + /** + * ♣ + */ + public static final char CLUB = 0x2663; + /** + * ♦ + */ + public static final char DIAMOND = 0x2666; + /** + * ♠ + */ + public static final char SPADES = 0x2660; + /** + * • + */ + public static final char BULLET = 0x2022; + /** + * ◘ + */ + public static final char INVERSE_BULLET = 0x25d8; + /** + * ○ + */ + public static final char WHITE_CIRCLE = 0x25cb; + /** + * ◙ + */ + public static final char INVERSE_WHITE_CIRCLE = 0x25d9; + + /** + * ■ + */ + public static final char SOLID_SQUARE = 0x25A0; + /** + * ▪ + */ + public static final char SOLID_SQUARE_SMALL = 0x25AA; + /** + * □ + */ + public static final char OUTLINED_SQUARE = 0x25A1; + /** + * ▫ + */ + public static final char OUTLINED_SQUARE_SMALL = 0x25AB; + + /** + * ♀ + */ + public static final char FEMALE = 0x2640; + /** + * ♂ + */ + public static final char MALE = 0x2642; + + /** + * ↑ + */ + public static final char ARROW_UP = 0x2191; + /** + * ↓ + */ + public static final char ARROW_DOWN = 0x2193; + /** + * → + */ + public static final char ARROW_RIGHT = 0x2192; + /** + * + */ + public static final char ARROW_LEFT = 0x2190; + + /** + * █ + */ + public static final char BLOCK_SOLID = 0x2588; + /** + * ▓ + */ + public static final char BLOCK_DENSE = 0x2593; + /** + * ▒ + */ + public static final char BLOCK_MIDDLE = 0x2592; + /** + * ░ + */ + public static final char BLOCK_SPARSE = 0x2591; + + /** + * ► + */ + public static final char TRIANGLE_RIGHT_POINTING_BLACK = 0x25BA; + /** + * ◄ + */ + public static final char TRIANGLE_LEFT_POINTING_BLACK = 0x25C4; + /** + * ▲ + */ + public static final char TRIANGLE_UP_POINTING_BLACK = 0x25B2; + /** + * ▼ + */ + public static final char TRIANGLE_DOWN_POINTING_BLACK = 0x25BC; + + /** + * + */ + public static final char TRIANGLE_RIGHT_POINTING_MEDIUM_BLACK = 0x23F4; + /** + * + */ + public static final char TRIANGLE_LEFT_POINTING_MEDIUM_BLACK = 0x23F5; + /** + * + */ + public static final char TRIANGLE_UP_POINTING_MEDIUM_BLACK = 0x23F6; + /** + * + */ + public static final char TRIANGLE_DOWN_POINTING_MEDIUM_BLACK = 0x23F7; + + + /** + * ─ + */ + public static final char SINGLE_LINE_HORIZONTAL = 0x2500; + /** + * + */ + public static final char BOLD_SINGLE_LINE_HORIZONTAL = 0x2501; + /** + * ╾ + */ + public static final char BOLD_TO_NORMAL_SINGLE_LINE_HORIZONTAL = 0x257E; + /** + * ╼ + */ + public static final char BOLD_FROM_NORMAL_SINGLE_LINE_HORIZONTAL = 0x257C; + /** + * + */ + public static final char DOUBLE_LINE_HORIZONTAL = 0x2550; + /** + * │ + */ + public static final char SINGLE_LINE_VERTICAL = 0x2502; + /** + * ┃ + */ + public static final char BOLD_SINGLE_LINE_VERTICAL = 0x2503; + /** + * ╿ + */ + public static final char BOLD_TO_NORMAL_SINGLE_LINE_VERTICAL = 0x257F; + /** + * ╽ + */ + public static final char BOLD_FROM_NORMAL_SINGLE_LINE_VERTICAL = 0x257D; + /** + * ║ + */ + public static final char DOUBLE_LINE_VERTICAL = 0x2551; + + /** + * ┌ + */ + public static final char SINGLE_LINE_TOP_LEFT_CORNER = 0x250C; + /** + * ╔ + */ + public static final char DOUBLE_LINE_TOP_LEFT_CORNER = 0x2554; + /** + * + */ + public static final char SINGLE_LINE_TOP_RIGHT_CORNER = 0x2510; + /** + * ╗ + */ + public static final char DOUBLE_LINE_TOP_RIGHT_CORNER = 0x2557; + + /** + * └ + */ + public static final char SINGLE_LINE_BOTTOM_LEFT_CORNER = 0x2514; + /** + * ╚ + */ + public static final char DOUBLE_LINE_BOTTOM_LEFT_CORNER = 0x255A; + /** + * ┘ + */ + public static final char SINGLE_LINE_BOTTOM_RIGHT_CORNER = 0x2518; + /** + * + */ + public static final char DOUBLE_LINE_BOTTOM_RIGHT_CORNER = 0x255D; + + /** + * ┼ + */ + public static final char SINGLE_LINE_CROSS = 0x253C; + /** + * ╬ + */ + public static final char DOUBLE_LINE_CROSS = 0x256C; + /** + * ╪ + */ + public static final char DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS = 0x256A; + /** + * ╫ + */ + public static final char DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS = 0x256B; + + /** + * ┴ + */ + public static final char SINGLE_LINE_T_UP = 0x2534; + /** + * ┬ + */ + public static final char SINGLE_LINE_T_DOWN = 0x252C; + /** + * ├ + */ + public static final char SINGLE_LINE_T_RIGHT = 0x251c; + /** + * ┤ + */ + public static final char SINGLE_LINE_T_LEFT = 0x2524; + + /** + * ╨ + */ + public static final char SINGLE_LINE_T_DOUBLE_UP = 0x2568; + /** + * ╥ + */ + public static final char SINGLE_LINE_T_DOUBLE_DOWN = 0x2565; + /** + * ╞ + */ + public static final char SINGLE_LINE_T_DOUBLE_RIGHT = 0x255E; + /** + * ╡ + */ + public static final char SINGLE_LINE_T_DOUBLE_LEFT = 0x2561; + + /** + * ╩ + */ + public static final char DOUBLE_LINE_T_UP = 0x2569; + /** + * ╦ + */ + public static final char DOUBLE_LINE_T_DOWN = 0x2566; + /** + * ╠ + */ + public static final char DOUBLE_LINE_T_RIGHT = 0x2560; + /** + * ╣ + */ + public static final char DOUBLE_LINE_T_LEFT = 0x2563; + + /** + * ╧ + */ + public static final char DOUBLE_LINE_T_SINGLE_UP = 0x2567; + /** + * ╤ + */ + public static final char DOUBLE_LINE_T_SINGLE_DOWN = 0x2564; + /** + * ╟ + */ + public static final char DOUBLE_LINE_T_SINGLE_RIGHT = 0x255F; + /** + * ╢ + */ + public static final char DOUBLE_LINE_T_SINGLE_LEFT = 0x2562; +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalPosition.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalPosition.java new file mode 100644 index 0000000000000000000000000000000000000000..f40185d8fdf72e2839fb4c4bf740df93a02308db --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalPosition.java @@ -0,0 +1,248 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +/** + * A 2-d position in 'terminal space'. Please note that the coordinates are 0-indexed, meaning 0x0 is the top left + * corner of the terminal. This object is immutable so you cannot change it after it has been created. Instead, you + * can easily create modified 'clones' by using the 'with' methods. + * + * @author Martin + */ +public class TerminalPosition implements Comparable { + + /** + * Constant for the top-left corner (0x0) + */ + public static final TerminalPosition TOP_LEFT_CORNER = new TerminalPosition(0, 0); + /** + * Constant for the 1x1 position (one offset in both directions from top-left) + */ + public static final TerminalPosition OFFSET_1x1 = new TerminalPosition(1, 1); + + private final int row; + private final int column; + + /** + * Creates a new TerminalPosition object, which represents a location on the screen. There is no check to verify + * that the position you specified is within the size of the current terminal and you can specify negative positions + * as well. + * + * @param column Column of the location, or the "x" coordinate, zero indexed (the first column is 0) + * @param row Row of the location, or the "y" coordinate, zero indexed (the first row is 0) + */ + public TerminalPosition(int column, int row) { + this.row = row; + this.column = column; + } + + /** + * Returns the index of the column this position is representing, zero indexed (the first column has index 0). + * + * @return Index of the column this position has + */ + public int getColumn() { + return column; + } + + /** + * Returns the index of the row this position is representing, zero indexed (the first row has index 0) + * + * @return Index of the row this position has + */ + public int getRow() { + return row; + } + + /** + * Creates a new TerminalPosition object representing a position with the same column index as this but with a + * supplied row index. + * + * @param row Index of the row for the new position + * @return A TerminalPosition object with the same column as this but with a specified row index + */ + public TerminalPosition withRow(int row) { + if (row == 0 && this.column == 0) { + return TOP_LEFT_CORNER; + } + return new TerminalPosition(this.column, row); + } + + /** + * Creates a new TerminalPosition object representing a position with the same row index as this but with a + * supplied column index. + * + * @param column Index of the column for the new position + * @return A TerminalPosition object with the same row as this but with a specified column index + */ + public TerminalPosition withColumn(int column) { + if (column == 0 && this.row == 0) { + return TOP_LEFT_CORNER; + } + return new TerminalPosition(column, this.row); + } + + /** + * Creates a new TerminalPosition object representing a position on the same row, but with a column offset by a + * supplied value. Calling this method with delta 0 will return this, calling it with a positive delta will return + * a terminal position delta number of columns to the right and for negative numbers the same to the left. + * + * @param delta Column offset + * @return New terminal position based off this one but with an applied offset + */ + public TerminalPosition withRelativeColumn(int delta) { + if (delta == 0) { + return this; + } + return withColumn(column + delta); + } + + /** + * Creates a new TerminalPosition object representing a position on the same column, but with a row offset by a + * supplied value. Calling this method with delta 0 will return this, calling it with a positive delta will return + * a terminal position delta number of rows to the down and for negative numbers the same up. + * + * @param delta Row offset + * @return New terminal position based off this one but with an applied offset + */ + public TerminalPosition withRelativeRow(int delta) { + if (delta == 0) { + return this; + } + return withRow(row + delta); + } + + /** + * Creates a new TerminalPosition object that is 'translated' by an amount of rows and columns specified by another + * TerminalPosition. Same as calling + * withRelativeRow(translate.getRow()).withRelativeColumn(translate.getColumn()) + * + * @param translate How many columns and rows to translate + * @return New TerminalPosition that is the result of the original with added translation + */ + public TerminalPosition withRelative(TerminalPosition translate) { + return withRelative(translate.getColumn(), translate.getRow()); + } + + /** + * Creates a new TerminalPosition object that is 'translated' by an amount of rows and columns specified by the two + * parameters. Same as calling + * withRelativeRow(deltaRow).withRelativeColumn(deltaColumn) + * + * @param deltaColumn How many columns to move from the current position in the new TerminalPosition + * @param deltaRow How many rows to move from the current position in the new TerminalPosition + * @return New TerminalPosition that is the result of the original position with added translation + */ + public TerminalPosition withRelative(int deltaColumn, int deltaRow) { + return withRelativeRow(deltaRow).withRelativeColumn(deltaColumn); + } + + /** + * Returns itself if it is equal to the supplied position, otherwise the supplied position. You can use this if you + * have a position field which is frequently recalculated but often resolves to the same; it will keep the same + * object in memory instead of swapping it out every cycle. + * + * @param position Position you want to return + * @return Itself if this position equals the position passed in, otherwise the position passed in + */ + public TerminalPosition with(TerminalPosition position) { + if (equals(position)) { + return this; + } + return position; + } + + public TerminalPosition plus(TerminalPosition position) { + return withRelative(position); + } + + public TerminalPosition minus(TerminalPosition position) { + return withRelative(-position.getColumn(), -position.getRow()); + } + + public TerminalPosition multiply(TerminalPosition position) { + return new TerminalPosition(column * position.column, row * position.row); + } + + public TerminalPosition divide(TerminalPosition denominator) { + return new TerminalPosition(column / denominator.column, row / denominator.row); + } + + public TerminalPosition abs() { + int x = Math.abs(column); + int y = Math.abs(row); + return new TerminalPosition(x, y); + } + + public TerminalPosition min(TerminalPosition position) { + int x = Math.min(column, position.column); + int y = Math.min(row, position.row); + return new TerminalPosition(x, y); + } + + public TerminalPosition max(TerminalPosition position) { + int x = Math.max(column, position.column); + int y = Math.max(row, position.row); + return new TerminalPosition(x, y); + } + + @Override + public int compareTo(TerminalPosition o) { + if (row < o.row) { + return -1; + } else if (row == o.row) { + if (column < o.column) { + return -1; + } else if (column == o.column) { + return 0; + } + } + return 1; + } + + @Override + public String toString() { + return "[" + column + ":" + row + "]"; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 23 * hash + this.row; + hash = 23 * hash + this.column; + return hash; + } + + public boolean equals(int columnIndex, int rowIndex) { + return this.column == columnIndex && + this.row == rowIndex; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TerminalPosition other = (TerminalPosition) obj; + return this.row == other.row && this.column == other.column; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalRectangle.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalRectangle.java new file mode 100644 index 0000000000000000000000000000000000000000..b97afb9c8dfdc0012d1323cd7d4dce035b100b15 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalRectangle.java @@ -0,0 +1,125 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +import java.util.Objects; + +/** + * This class is immutable and cannot change its internal state after creation. + * + * @author ginkoblongata + */ +public class TerminalRectangle { + + // one of the benefits of immutable: ease of usage + public final TerminalPosition position; + public final TerminalSize size; + public final int x; + public final int y; + public final int width; + public final int height; + + public final int xAndWidth; + public final int yAndHeight; + + /** + * Creates a new terminal rect representation at the supplied x y position with the supplied width and height. + *

+ * Both width and height must be at least zero (non negative) as checked in TerminalSize. + * + * @param width number of columns + * @param height number of rows + */ + public TerminalRectangle(int x, int y, int width, int height) { + position = new TerminalPosition(x, y); + size = new TerminalSize(width, height); + + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.xAndWidth = x + width; + this.yAndHeight = y + height; + } + + /** + * @return Returns the width of this rect, in number of columns + */ + public int getColumns() { + return width; + } + + /** + * @return Returns the height of this rect representation, in number of rows + */ + public int getRows() { + return height; + } + + /** + * Creates a new rect based on this rect, but with a different width + * + * @param columns Width of the new rect, in columns + * @return New rect based on this one, but with a new width + */ + public TerminalRectangle withColumns(int columns) { + return new TerminalRectangle(x, y, columns, height); + } + + /** + * Creates a new rect based on this rect, but with a different height + * + * @param rows Height of the new rect, in rows + * @return New rect based on this one, but with a new height + */ + public TerminalRectangle withRows(int rows) { + return new TerminalRectangle(x, y, width, rows); + } + + public boolean whenContains(TerminalPosition p, Runnable op) { + return whenContains(p.getColumn(), p.getRow(), op); + } + + public boolean whenContains(int x, int y, Runnable op) { + if (this.x <= x && x < this.xAndWidth && this.y <= y && y < this.yAndHeight) { + op.run(); + return true; + } + return false; + } + + + @Override + public String toString() { + return "{x: " + x + ", y: " + y + ", width: " + width + ", height: " + height + "}"; + } + + @Override + public boolean equals(Object obj) { + return obj != null + && obj.getClass() == getClass() + && Objects.equals(position, ((TerminalRectangle) obj).position) + && Objects.equals(size, ((TerminalRectangle) obj).size); + } + + @Override + public int hashCode() { + return Objects.hash(position, size); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalSize.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalSize.java new file mode 100644 index 0000000000000000000000000000000000000000..329b260ccbb78c6b046d23ae67411b559859346b --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalSize.java @@ -0,0 +1,218 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +/** + * Terminal dimensions in 2-d space, measured in number of rows and columns. This class is immutable and cannot change + * its internal state after creation. + * + * @author Martin + */ +public class TerminalSize { + public static final TerminalSize ZERO = new TerminalSize(0, 0); + public static final TerminalSize ONE = new TerminalSize(1, 1); + + private final int columns; + private final int rows; + + /** + * Creates a new terminal size representation with a given width (columns) and height (rows) + * + * @param columns Width, in number of columns + * @param rows Height, in number of columns + */ + public TerminalSize(int columns, int rows) { + if (columns < 0 || rows < 0) { + throw new IllegalArgumentException("TerminalSize dimensions cannot be less than 0: [columns: " + columns + ", rows: " + rows + "]"); + } + + this.columns = columns; + this.rows = rows; + } + + /** + * @return Returns the width of this size representation, in number of columns + */ + public int getColumns() { + return columns; + } + + /** + * Creates a new size based on this size, but with a different width + * + * @param columns Width of the new size, in columns + * @return New size based on this one, but with a new width + */ + public TerminalSize withColumns(int columns) { + if (this.columns == columns) { + return this; + } + if (columns == 0 && this.rows == 0) { + return ZERO; + } + return new TerminalSize(columns, this.rows); + } + + + /** + * @return Returns the height of this size representation, in number of rows + */ + public int getRows() { + return rows; + } + + /** + * Creates a new size based on this size, but with a different height + * + * @param rows Height of the new size, in rows + * @return New size based on this one, but with a new height + */ + public TerminalSize withRows(int rows) { + if (this.rows == rows) { + return this; + } + if (rows == 0 && this.columns == 0) { + return ZERO; + } + return new TerminalSize(this.columns, rows); + } + + /** + * Creates a new TerminalSize object representing a size with the same number of rows, but with a column size offset by a + * supplied value. Calling this method with delta 0 will return this, calling it with a positive delta will return + * a terminal size delta number of columns wider and for negative numbers shorter. + * + * @param delta Column offset + * @return New terminal size based off this one but with an applied transformation + */ + public TerminalSize withRelativeColumns(int delta) { + if (delta == 0) { + return this; + } + // Prevent going below 0 (which would throw an exception) + return withColumns(Math.max(0, columns + delta)); + } + + /** + * Creates a new TerminalSize object representing a size with the same number of columns, but with a row size offset by a + * supplied value. Calling this method with delta 0 will return this, calling it with a positive delta will return + * a terminal size delta number of rows longer and for negative numbers shorter. + * + * @param delta Row offset + * @return New terminal size based off this one but with an applied transformation + */ + public TerminalSize withRelativeRows(int delta) { + if (delta == 0) { + return this; + } + // Prevent going below 0 (which would throw an exception) + return withRows(Math.max(0, rows + delta)); + } + + /** + * Creates a new TerminalSize object representing a size based on this object's size but with a delta applied. + * This is the same as calling + * withRelativeColumns(delta.getColumns()).withRelativeRows(delta.getRows()) + * + * @param delta Column and row offset + * @return New terminal size based off this one but with an applied resize + */ + public TerminalSize withRelative(TerminalSize delta) { + return withRelative(delta.getColumns(), delta.getRows()); + } + + /** + * Creates a new TerminalSize object representing a size based on this object's size but with a delta applied. + * This is the same as calling + * withRelativeColumns(deltaColumns).withRelativeRows(deltaRows) + * + * @param deltaColumns How many extra columns the new TerminalSize will have (negative values are allowed) + * @param deltaRows How many extra rows the new TerminalSize will have (negative values are allowed) + * @return New terminal size based off this one but with an applied resize + */ + public TerminalSize withRelative(int deltaColumns, int deltaRows) { + return withRelativeRows(deltaRows).withRelativeColumns(deltaColumns); + } + + /** + * Takes a different TerminalSize and returns a new TerminalSize that has the largest dimensions of the two, + * measured separately. So calling 3x5 on a 5x3 will return 5x5. + * + * @param other Other TerminalSize to compare with + * @return TerminalSize that combines the maximum width between the two and the maximum height + */ + public TerminalSize max(TerminalSize other) { + return withColumns(Math.max(columns, other.columns)) + .withRows(Math.max(rows, other.rows)); + } + + /** + * Takes a different TerminalSize and returns a new TerminalSize that has the smallest dimensions of the two, + * measured separately. So calling 3x5 on a 5x3 will return 3x3. + * + * @param other Other TerminalSize to compare with + * @return TerminalSize that combines the minimum width between the two and the minimum height + */ + public TerminalSize min(TerminalSize other) { + return withColumns(Math.min(columns, other.columns)) + .withRows(Math.min(rows, other.rows)); + } + + /** + * Returns itself if it is equal to the supplied size, otherwise the supplied size. You can use this if you have a + * size field which is frequently recalculated but often resolves to the same size; it will keep the same object + * in memory instead of swapping it out every cycle. + * + * @param size Size you want to return + * @return Itself if this size equals the size passed in, otherwise the size passed in + */ + public TerminalSize with(TerminalSize size) { + if (equals(size)) { + return this; + } + return size; + } + + @Override + public String toString() { + return "{" + columns + "x" + rows + "}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TerminalSize)) { + return false; + } + + TerminalSize other = (TerminalSize) obj; + return columns == other.columns + && rows == other.rows; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 53 * hash + this.columns; + hash = 53 * hash + this.rows; + return hash; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalTextUtils.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalTextUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..746069e23001f17313503353b9142c85726b254e --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalTextUtils.java @@ -0,0 +1,483 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +import com.googlecode.lanterna.graphics.StyleSet; +import com.googlecode.lanterna.screen.TabBehaviour; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * This class contains a number of utility methods for analyzing characters and strings in a terminal context. The main + * purpose is to make it easier to work with text that may or may not contain double-width text characters, such as CJK + * (Chinese, Japanese, Korean) and other special symbols. This class assumes those are all double-width and in case the + * terminal (-emulator) chooses to draw them (somehow) as single-column then all the calculations in this class will be + * wrong. It seems safe to assume what this class considers double-width really is taking up two columns though. + * + * @author Martin + */ +public class TerminalTextUtils { + private TerminalTextUtils() { + } + + /** + * Given a string and an index in that string, returns the ANSI control sequence beginning on this index. If there + * is no control sequence starting there, the method will return null. The returned value is the complete escape + * sequence including the ESC prefix. + * + * @param string String to scan for control sequences + * @param index Index in the string where the control sequence begins + * @return {@code null} if there was no control sequence starting at the specified index, otherwise the entire + * control sequence + */ + public static String getANSIControlSequenceAt(String string, int index) { + int len = getANSIControlSequenceLength(string, index); + return len == 0 ? null : string.substring(index, index + len); + } + + /** + * Given a string and an index in that string, returns the number of characters starting at index that make up + * a complete ANSI control sequence. If there is no control sequence starting there, the method will return 0. + * + * @param string String to scan for control sequences + * @param index Index in the string where the control sequence begins + * @return {@code 0} if there was no control sequence starting at the specified index, otherwise the length + * of the entire control sequence + */ + public static int getANSIControlSequenceLength(String string, int index) { + int len = 0, restlen = string.length() - index; + if (restlen >= 3) { // Control sequences require a minimum of three characters + char esc = string.charAt(index), + bracket = string.charAt(index + 1); + if (esc == 0x1B && bracket == '[') { // escape & open bracket + len = 3; // esc,bracket and (later)terminator. + // digits or semicolons can still precede the terminator: + for (int i = 2; i < restlen; i++) { + char ch = string.charAt(i + index); + // only ascii-digits or semicolons allowed here: + if ((ch >= '0' && ch <= '9') || ch == ';') { + len++; + } else { + break; + } + } + // if string ends in digits/semicolons, then it's not a sequence. + if (len > restlen) { + len = 0; + } + } + } + return len; + } + + /** + * Given a character, is this character considered to be a CJK character? + * Shamelessly stolen from + * StackOverflow + * where it was contributed by user Rakesh N + * + * @param c Character to test + * @return {@code true} if the character is a CJK character + */ + public static boolean isCharCJK(final char c) { + Character.UnicodeBlock unicodeBlock = Character.UnicodeBlock.of(c); + return (unicodeBlock == Character.UnicodeBlock.HIRAGANA) + || (unicodeBlock == Character.UnicodeBlock.KATAKANA) + || (unicodeBlock == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_JAMO) + || (unicodeBlock == Character.UnicodeBlock.HANGUL_SYLLABLES) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) + || (unicodeBlock == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B) + || (unicodeBlock == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS) + || (unicodeBlock == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS) + || (unicodeBlock == Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT) + || (unicodeBlock == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION) + || (unicodeBlock == Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS) + || (unicodeBlock == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS && c < 0xFF61); //The magic number here is the separating index between full-width and half-width + } + + /** + * Given a character, is this character considered to be a Thai character? + * + * @param c Character to test + * @return {@code true} if the character is a Thai character + */ + public static boolean isCharThai(char c) { + Character.UnicodeBlock unicodeBlock = Character.UnicodeBlock.of(c); + return unicodeBlock == Character.UnicodeBlock.THAI; + } + + /** + * Checks if a character is expected to be taking up two columns if printed to a terminal. This will generally be + * {@code true} for CJK (Chinese, Japanese and Korean) characters. + * + * @param c Character to test if it's double-width when printed to a terminal + * @return {@code true} if this character is expected to be taking up two columns when printed to the terminal, + * otherwise {@code false} + */ + public static boolean isCharDoubleWidth(final char c) { + return isCharCJK(c); + } + + /** + * Checks if a particular character is a control character, in Lanterna this currently means it's 0-31 or 127 in the + * ascii table. + * + * @param c character to test + * @return {@code true} if the character is a control character, {@code false} otherwise + */ + public static boolean isControlCharacter(char c) { + return c < 32 || c == 127; + } + + /** + * Checks if a particular character is printable. This generally means that the code is not a control character that + * isn't able to be printed to the terminal properly. For example, NULL, ENQ, BELL and ESC and all control codes + * that has no proper character associated with it so the behaviour is undefined and depends completely on the + * terminal what happens if you try to print them. However, certain control characters have a particular meaning to + * the terminal and are as such considered printable. In Lanterna, we consider these control characters printable: + *

    + *
  • Backspace
  • + *
  • Horizontal Tab
  • + *
  • Line feed
  • + *
+ * + * @param c character to test + * @return {@code true} if the character is considered printable, {@code false} otherwise + */ + public static boolean isPrintableCharacter(char c) { + return !isControlCharacter(c) || c == '\t' || c == '\n' || c == '\b'; + } + + /** + * Given a string, returns how many columns this string would need to occupy in a terminal, taking into account that + * CJK characters takes up two columns. + * + * @param s String to check length + * @return Number of actual terminal columns the string would occupy + */ + public static int getColumnWidth(String s) { + return getColumnIndex(s, s.length()); + } + + /** + * Given a string and a character index inside that string, find out what the column index of that character would + * be if printed in a terminal. If the string only contains non-CJK characters then the returned value will be same + * as {@code stringCharacterIndex}, but if there are CJK characters the value will be different due to CJK + * characters taking up two columns in width. If the character at the index in the string is a CJK character itself, + * the returned value will be the index of the left-side of character. The tab character is counted as four spaces. + * + * @param s String to translate the index from + * @param stringCharacterIndex Index within the string to get the terminal column index of + * @return Index of the character inside the String at {@code stringCharacterIndex} when it has been writted to a + * terminal + * @throws StringIndexOutOfBoundsException if the index given is outside the String length or negative + */ + public static int getColumnIndex(String s, int stringCharacterIndex) throws StringIndexOutOfBoundsException { + return getColumnIndex(s, stringCharacterIndex, TabBehaviour.CONVERT_TO_FOUR_SPACES, -1); + } + + /** + * Given a string and a character index inside that string, find out what the column index of that character would + * be if printed in a terminal. If the string only contains non-CJK characters then the returned value will be same + * as {@code stringCharacterIndex}, but if there are CJK characters the value will be different due to CJK + * characters taking up two columns in width. If the character at the index in the string is a CJK character itself, + * the returned value will be the index of the left-side of character. + * + * @param s String to translate the index from + * @param stringCharacterIndex Index within the string to get the terminal column index of + * @param tabBehaviour The behavior to use when encountering the tab character + * @param firstCharacterColumnPosition Where on the screen the first character in the string would be printed, this + * applies only when you have an alignment-based {@link TabBehaviour} + * @return Index of the character inside the String at {@code stringCharacterIndex} when it has been writted to a + * terminal + * @throws StringIndexOutOfBoundsException if the index given is outside the String length or negative + */ + public static int getColumnIndex(String s, int stringCharacterIndex, TabBehaviour tabBehaviour, int firstCharacterColumnPosition) throws StringIndexOutOfBoundsException { + int index = 0; + for (int i = 0; i < stringCharacterIndex; i++) { + if (s.charAt(i) == '\t') { + index += tabBehaviour.getTabReplacement(firstCharacterColumnPosition).length(); + } else { + if (isCharCJK(s.charAt(i))) { + index++; + } + index++; + } + } + return index; + } + + /** + * This method does the reverse of getColumnIndex, given a String and imagining it has been printed out to the + * top-left corner of a terminal, in the column specified by {@code columnIndex}, what is the index of that + * character in the string. If the string contains no CJK characters, this will always be the same as + * {@code columnIndex}. If the index specified is the right column of a CJK character, the index is the same as if + * the column was the left column. So calling {@code getStringCharacterIndex("英", 0)} and + * {@code getStringCharacterIndex("英", 1)} will both return 0. + * + * @param s String to translate the index to + * @param columnIndex Column index of the string written to a terminal + * @return The index in the string of the character in terminal column {@code columnIndex} + */ + public static int getStringCharacterIndex(String s, int columnIndex) { + int index = 0; + int counter = 0; + while (counter < columnIndex) { + if (isCharCJK(s.charAt(index++))) { + counter++; + if (counter == columnIndex) { + return index - 1; + } + } + counter++; + } + return index; + } + + /** + * Given a string that may or may not contain CJK characters, returns the substring which will fit inside + * availableColumnSpace columns. This method does not handle special cases like tab or new-line. + *

+ * Calling this method is the same as calling {@code fitString(string, 0, availableColumnSpace)}. + * + * @param string The string to fit inside the availableColumnSpace + * @param availableColumnSpace Number of columns to fit the string inside + * @return The whole or part of the input string which will fit inside the supplied availableColumnSpace + */ + public static String fitString(String string, int availableColumnSpace) { + return fitString(string, 0, availableColumnSpace); + } + + /** + * Given a string that may or may not contain CJK characters, returns the substring which will fit inside + * availableColumnSpace columns. This method does not handle special cases like tab or new-line. + *

+ * This overload has a {@code fromColumn} parameter that specified where inside the string to start fitting. Please + * notice that {@code fromColumn} is not a character index inside the string, but a column index as if the string + * has been printed from the left-most side of the terminal. So if the string is "日本語", fromColumn set to 1 will + * not starting counting from the second character ("本") in the string but from the CJK filler character belonging + * to "日". If you want to count from a particular character index inside the string, please pass in a substring + * and use fromColumn set to 0. + * + * @param string The string to fit inside the availableColumnSpace + * @param fromColumn From what column of the input string to start fitting (see description above!) + * @param availableColumnSpace Number of columns to fit the string inside + * @return The whole or part of the input string which will fit inside the supplied availableColumnSpace + */ + public static String fitString(String string, int fromColumn, int availableColumnSpace) { + if (availableColumnSpace <= 0) { + return ""; + } + + StringBuilder bob = new StringBuilder(); + int column = 0; + int index = 0; + while (index < string.length() && column < fromColumn) { + char c = string.charAt(index++); + column += TerminalTextUtils.isCharCJK(c) ? 2 : 1; + } + if (column > fromColumn) { + bob.append(" "); + availableColumnSpace--; + } + + while (availableColumnSpace > 0 && index < string.length()) { + char c = string.charAt(index++); + availableColumnSpace -= TerminalTextUtils.isCharCJK(c) ? 2 : 1; + if (availableColumnSpace < 0) { + bob.append(' '); + } else { + bob.append(c); + } + } + return bob.toString(); + } + + /** + * This method will calculate word wrappings given a number of lines of text and how wide the text can be printed. + * The result is a list of new rows where word-wrapping was applied. + * + * @param maxWidth Maximum number of columns that can be used before word-wrapping is applied, if <= 0 then the + * lines will be returned unchanged + * @param lines Input text + * @return The input text word-wrapped at {@code maxWidth}; this may contain more rows than the input text + */ + public static List getWordWrappedText(int maxWidth, String... lines) { + //Bounds checking + if (maxWidth <= 0) { + return Arrays.asList(lines); + } + + List result = new ArrayList<>(); + LinkedList linesToBeWrapped = new LinkedList<>(Arrays.asList(lines)); + while (!linesToBeWrapped.isEmpty()) { + String row = linesToBeWrapped.removeFirst(); + int rowWidth = getColumnWidth(row); + if (rowWidth <= maxWidth) { + result.add(row); + } else { + //Now search in reverse and find the first possible line-break + final int characterIndexMax = getStringCharacterIndex(row, maxWidth); + int characterIndex = characterIndexMax; + while (characterIndex >= 0 && + !Character.isSpaceChar(row.charAt(characterIndex)) && + !isCharCJK(row.charAt(characterIndex))) { + characterIndex--; + } + // right *after* a CJK is also a "nice" spot to break the line! + if (characterIndex >= 0 && characterIndex < characterIndexMax && + isCharCJK(row.charAt(characterIndex))) { + characterIndex++; // with these conditions it fits! + } + + if (characterIndex < 0) { + //Failed! There was no 'nice' place to cut so just cut it at maxWidth + characterIndex = Math.max(characterIndexMax, 1); // at least 1 char + result.add(row.substring(0, characterIndex)); + linesToBeWrapped.addFirst(row.substring(characterIndex)); + } else { + // characterIndex == 0 only happens, if either + // - first char is CJK and maxWidth==1 or + // - first char is whitespace + // either way: put it in row before break to prevent infinite loop. + characterIndex = Math.max(characterIndex, 1); // at least 1 char + + //Ok, split the row, add it to the result and continue processing the second half on a new line + result.add(row.substring(0, characterIndex)); + while (characterIndex < row.length() && + Character.isSpaceChar(row.charAt(characterIndex))) { + characterIndex++; + } + if (characterIndex < row.length()) { // only if rest contains non-whitespace + linesToBeWrapped.addFirst(row.substring(characterIndex)); + } + } + } + } + return result; + } + + private static Integer[] mapCodesToIntegerArray(String[] codes) { + Integer[] result = new Integer[codes.length]; + for (int i = 0; i < result.length; i++) { + if (codes[i].isEmpty()) { + result[i] = 0; + } else { + try { + // An empty string is equivalent to 0. + // Warning: too large values could throw an Exception! + result[i] = Integer.parseInt(codes[i]); + } catch (NumberFormatException ignored) { + throw new IllegalArgumentException("Unknown CSI code " + codes[i]); + } + } + } + return result; + } + + public static void updateModifiersFromCSICode( + String controlSequence, + StyleSet target, + StyleSet original) { + + char controlCodeType = controlSequence.charAt(controlSequence.length() - 1); + controlSequence = controlSequence.substring(2, controlSequence.length() - 1); + Integer[] codes = mapCodesToIntegerArray(controlSequence.split(";")); + + TextColor[] palette = TextColor.ANSI.values(); + + if (controlCodeType == 'm') { // SGRs + for (int i = 0; i < codes.length; i++) { + int code = codes[i]; + switch (code) { + case 0: + target.setStyleFrom(original); + break; + case 1: + target.enableModifiers(SGR.BOLD); + break; + case 3: + target.enableModifiers(SGR.ITALIC); + break; + case 4: + target.enableModifiers(SGR.UNDERLINE); + break; + case 5: + target.enableModifiers(SGR.BLINK); + break; + case 7: + target.enableModifiers(SGR.REVERSE); + break; + case 21: // both do. 21 seems more straightforward. + case 22: + target.disableModifiers(SGR.BOLD); + break; + case 23: + target.disableModifiers(SGR.ITALIC); + break; + case 24: + target.disableModifiers(SGR.UNDERLINE); + break; + case 25: + target.disableModifiers(SGR.BLINK); + break; + case 27: + target.disableModifiers(SGR.REVERSE); + break; + case 38: + if (i + 2 < codes.length && codes[i + 1] == 5) { + target.setForegroundColor(new TextColor.Indexed(codes[i + 2])); + i += 2; + } else if (i + 4 < codes.length && codes[i + 1] == 2) { + target.setForegroundColor(new TextColor.RGB(codes[i + 2], codes[i + 3], codes[i + 4])); + i += 4; + } + break; + case 39: + target.setForegroundColor(original.getForegroundColor()); + break; + case 48: + if (i + 2 < codes.length && codes[i + 1] == 5) { + target.setBackgroundColor(new TextColor.Indexed(codes[i + 2])); + i += 2; + } else if (i + 4 < codes.length && codes[i + 1] == 2) { + target.setBackgroundColor(new TextColor.RGB(codes[i + 2], codes[i + 3], codes[i + 4])); + i += 4; + } + break; + case 49: + target.setBackgroundColor(original.getBackgroundColor()); + break; + default: + if (code >= 30 && code <= 37) { + target.setForegroundColor(palette[code - 30]); + } else if (code >= 40 && code <= 47) { + target.setBackgroundColor(palette[code - 40]); + } + } + } + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextCharacter.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextCharacter.java new file mode 100644 index 0000000000000000000000000000000000000000..4603af282f23264abb30b2ea9a3ff75633663f77 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextCharacter.java @@ -0,0 +1,452 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +import java.io.Serializable; +import java.text.BreakIterator; +import java.util.*; + +/** + * Represents a single character with additional metadata such as colors and modifiers. This class is immutable and + * cannot be modified after creation. + * + * @author Martin + */ +public class TextCharacter implements Serializable { + private static EnumSet toEnumSet(SGR... modifiers) { + if (modifiers.length == 0) { + return EnumSet.noneOf(SGR.class); + } else { + return EnumSet.copyOf(Arrays.asList(modifiers)); + } + } + + public static final TextCharacter DEFAULT_CHARACTER = new TextCharacter(' ', TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT); + + public static TextCharacter[] fromCharacter(char c) { + return fromString(Character.toString(c)); + } + + public static TextCharacter[] fromString(String string) { + return fromString(string, TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT); + } + + public static TextCharacter[] fromCharacter(char c, TextColor foregroundColor, TextColor backgroundColor, SGR... modifiers) { + return fromString(Character.toString(c), foregroundColor, backgroundColor, modifiers); + } + + public static TextCharacter[] fromString( + String string, + TextColor foregroundColor, + TextColor backgroundColor, + SGR... modifiers) { + return fromString(string, foregroundColor, backgroundColor, toEnumSet(modifiers)); + } + + public static TextCharacter[] fromString( + String string, + TextColor foregroundColor, + TextColor backgroundColor, + EnumSet modifiers) { + + BreakIterator breakIterator = BreakIterator.getCharacterInstance(); + breakIterator.setText(string); + List result = new ArrayList<>(); + for (int begin = 0, end = 0; (end = breakIterator.next()) != BreakIterator.DONE; begin = breakIterator.current()) { + result.add(new TextCharacter(string.substring(begin, end), foregroundColor, backgroundColor, modifiers)); + } + return result.toArray(new TextCharacter[0]); + } + + /** + * The "character" might not fit in a Java 16-bit char (emoji and other types) so we store it in a String + * as of 3.1 instead. + */ + private final String character; + private final TextColor foregroundColor; + private final TextColor backgroundColor; + private final EnumSet modifiers; //This isn't immutable, but we should treat it as such and not expose it! + + /** + * Creates a {@code ScreenCharacter} based on a supplied character, with default colors and no extra modifiers. + * + * @param character Physical character to use + * @deprecated Use fromCharacter instead + */ + @Deprecated + public TextCharacter(char character) { + this(character, TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT); + } + + /** + * Copies another {@code ScreenCharacter} + * + * @param character screenCharacter to copy from + * @deprecated TextCharacters are immutable so you shouldn't need to call this + */ + @Deprecated + public TextCharacter(TextCharacter character) { + this(character.getCharacterString(), + character.getForegroundColor(), + character.getBackgroundColor(), + EnumSet.copyOf(character.getModifiers())); + } + + /** + * Creates a new {@code ScreenCharacter} based on a physical character, color information and optional modifiers. + * + * @param character Physical character to refer to + * @param foregroundColor Foreground color the character has + * @param backgroundColor Background color the character has + * @param styles Optional list of modifiers to apply when drawing the character + * @deprecated Use fromCharacter instead + */ + @SuppressWarnings("WeakerAccess") + @Deprecated + public TextCharacter( + char character, + TextColor foregroundColor, + TextColor backgroundColor, + SGR... styles) { + + this(character, + foregroundColor, + backgroundColor, + toEnumSet(styles)); + } + + /** + * Creates a new {@code ScreenCharacter} based on a physical character, color information and a set of modifiers. + * + * @param character Physical character to refer to + * @param foregroundColor Foreground color the character has + * @param backgroundColor Background color the character has + * @param modifiers Set of modifiers to apply when drawing the character + * @deprecated Use fromCharacter instead + */ + @Deprecated + public TextCharacter( + char character, + TextColor foregroundColor, + TextColor backgroundColor, + EnumSet modifiers) { + this(Character.toString(character), foregroundColor, backgroundColor, modifiers); + } + + /** + * Creates a new {@code ScreenCharacter} based on a physical character, color information and a set of modifiers. + * + * @param character Physical character to refer to + * @param foregroundColor Foreground color the character has + * @param backgroundColor Background color the character has + * @param modifiers Set of modifiers to apply when drawing the character + */ + private TextCharacter( + String character, + TextColor foregroundColor, + TextColor backgroundColor, + EnumSet modifiers) { + + if (character.isEmpty()) { + throw new IllegalArgumentException("Cannot create TextCharacter from an empty string"); + } + validateSingleCharacter(character); + + // intern the string so we don't waste more memory than necessary + this.character = character.intern(); + char firstCharacter = character.charAt(0); + + // Don't allow creating a TextCharacter containing a control character + // For backward-compatibility, do allow tab for now + if (TerminalTextUtils.isControlCharacter(firstCharacter) && firstCharacter != '\t') { + throw new IllegalArgumentException("Cannot create a TextCharacter from a control character (0x" + Integer.toHexString(firstCharacter) + ")"); + } + + if (foregroundColor == null) { + foregroundColor = TextColor.ANSI.DEFAULT; + } + if (backgroundColor == null) { + backgroundColor = TextColor.ANSI.DEFAULT; + } + + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.modifiers = EnumSet.copyOf(modifiers); + } + + private void validateSingleCharacter(String character) { + BreakIterator breakIterator = BreakIterator.getCharacterInstance(); + breakIterator.setText(character); + String firstCharacter = null; + for (int begin = 0, end = 0; (end = breakIterator.next()) != BreakIterator.DONE; begin = breakIterator.current()) { + if (firstCharacter == null) { + firstCharacter = character.substring(begin, end); + } else { + throw new IllegalArgumentException("Invalid String for TextCharacter, can only have one logical character"); + } + } + } + + public boolean is(char otherCharacter) { + return otherCharacter == character.charAt(0) && character.length() == 1; + } + + /** + * The actual character this TextCharacter represents + * + * @return character of the TextCharacter + * @deprecated This won't work with advanced characters like emoji + */ + @Deprecated + public char getCharacter() { + return character.charAt(0); + } + + /** + * Returns the character this TextCharacter represents as a String. This is not returning a char + * + * @return + */ + public String getCharacterString() { + return character; + } + + + /** + * Foreground color specified for this TextCharacter + * + * @return Foreground color of this TextCharacter + */ + public TextColor getForegroundColor() { + return foregroundColor; + } + + /** + * Background color specified for this TextCharacter + * + * @return Background color of this TextCharacter + */ + public TextColor getBackgroundColor() { + return backgroundColor; + } + + /** + * Returns a set of all active modifiers on this TextCharacter + * + * @return Set of active SGR codes + */ + public EnumSet getModifiers() { + return EnumSet.copyOf(modifiers); + } + + /** + * Returns true if this TextCharacter has the bold modifier active + * + * @return {@code true} if this TextCharacter has the bold modifier active + */ + public boolean isBold() { + return modifiers.contains(SGR.BOLD); + } + + /** + * Returns true if this TextCharacter has the reverse modifier active + * + * @return {@code true} if this TextCharacter has the reverse modifier active + */ + public boolean isReversed() { + return modifiers.contains(SGR.REVERSE); + } + + /** + * Returns true if this TextCharacter has the underline modifier active + * + * @return {@code true} if this TextCharacter has the underline modifier active + */ + public boolean isUnderlined() { + return modifiers.contains(SGR.UNDERLINE); + } + + /** + * Returns true if this TextCharacter has the blink modifier active + * + * @return {@code true} if this TextCharacter has the blink modifier active + */ + public boolean isBlinking() { + return modifiers.contains(SGR.BLINK); + } + + /** + * Returns true if this TextCharacter has the bordered modifier active + * + * @return {@code true} if this TextCharacter has the bordered modifier active + */ + public boolean isBordered() { + return modifiers.contains(SGR.BORDERED); + } + + /** + * Returns true if this TextCharacter has the crossed-out modifier active + * + * @return {@code true} if this TextCharacter has the crossed-out modifier active + */ + public boolean isCrossedOut() { + return modifiers.contains(SGR.CROSSED_OUT); + } + + /** + * Returns true if this TextCharacter has the italic modifier active + * + * @return {@code true} if this TextCharacter has the italic modifier active + */ + public boolean isItalic() { + return modifiers.contains(SGR.ITALIC); + } + + /** + * Returns a new TextCharacter with the same colors and modifiers but a different underlying character + * + * @param character Character the copy should have + * @return Copy of this TextCharacter with different underlying character + */ + @SuppressWarnings("SameParameterValue") + public TextCharacter withCharacter(char character) { + if (this.character.equals(Character.toString(character))) { + return this; + } + return new TextCharacter(character, foregroundColor, backgroundColor, modifiers); + } + + /** + * Returns a copy of this TextCharacter with a specified foreground color + * + * @param foregroundColor Foreground color the copy should have + * @return Copy of the TextCharacter with a different foreground color + */ + public TextCharacter withForegroundColor(TextColor foregroundColor) { + if (this.foregroundColor == foregroundColor || this.foregroundColor.equals(foregroundColor)) { + return this; + } + return new TextCharacter(character, foregroundColor, backgroundColor, modifiers); + } + + /** + * Returns a copy of this TextCharacter with a specified background color + * + * @param backgroundColor Background color the copy should have + * @return Copy of the TextCharacter with a different background color + */ + public TextCharacter withBackgroundColor(TextColor backgroundColor) { + if (this.backgroundColor == backgroundColor || this.backgroundColor.equals(backgroundColor)) { + return this; + } + return new TextCharacter(character, foregroundColor, backgroundColor, modifiers); + } + + /** + * Returns a copy of this TextCharacter with specified list of SGR modifiers. None of the currently active SGR codes + * will be carried over to the copy, only those in the passed in value. + * + * @param modifiers SGR modifiers the copy should have + * @return Copy of the TextCharacter with a different set of SGR modifiers + */ + public TextCharacter withModifiers(Collection modifiers) { + EnumSet newSet = EnumSet.copyOf(modifiers); + if (modifiers.equals(newSet)) { + return this; + } + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + /** + * Returns a copy of this TextCharacter with an additional SGR modifier. All of the currently active SGR codes + * will be carried over to the copy, in addition to the one specified. + * + * @param modifier SGR modifiers the copy should have in additional to all currently present + * @return Copy of the TextCharacter with a new SGR modifier + */ + public TextCharacter withModifier(SGR modifier) { + if (modifiers.contains(modifier)) { + return this; + } + EnumSet newSet = EnumSet.copyOf(this.modifiers); + newSet.add(modifier); + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + /** + * Returns a copy of this TextCharacter with an SGR modifier removed. All of the currently active SGR codes + * will be carried over to the copy, except for the one specified. If the current TextCharacter doesn't have the + * SGR specified, it will return itself. + * + * @param modifier SGR modifiers the copy should not have + * @return Copy of the TextCharacter without the SGR modifier + */ + public TextCharacter withoutModifier(SGR modifier) { + if (!modifiers.contains(modifier)) { + return this; + } + EnumSet newSet = EnumSet.copyOf(this.modifiers); + newSet.remove(modifier); + return new TextCharacter(character, foregroundColor, backgroundColor, newSet); + } + + public boolean isDoubleWidth() { + // TODO: make this better to work properly with emoji and other complicated "characters" + return TerminalTextUtils.isCharDoubleWidth(character.charAt(0)) || + // If the character takes up more than one char, assume it's double width (unless thai) + (character.length() > 1 && !TerminalTextUtils.isCharThai(character.charAt(0))); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TextCharacter other = (TextCharacter) obj; + if (!Objects.equals(this.character, other.character)) { + return false; + } + if (!Objects.equals(this.foregroundColor, other.foregroundColor)) { + return false; + } + if (!Objects.equals(this.backgroundColor, other.backgroundColor)) { + return false; + } + return Objects.equals(this.modifiers, other.modifiers); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + this.character.hashCode(); + hash = 37 * hash + (this.foregroundColor != null ? this.foregroundColor.hashCode() : 0); + hash = 37 * hash + (this.backgroundColor != null ? this.backgroundColor.hashCode() : 0); + hash = 37 * hash + (this.modifiers != null ? this.modifiers.hashCode() : 0); + return hash; + } + + @Override + public String toString() { + return "TextCharacter{" + "character=" + character + ", foregroundColor=" + foregroundColor + ", backgroundColor=" + backgroundColor + ", modifiers=" + modifiers + '}'; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextColor.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextColor.java new file mode 100644 index 0000000000000000000000000000000000000000..8f178a991170e2a775778ce2519f15fba474821c --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/TextColor.java @@ -0,0 +1,704 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna; + +import java.awt.*; +import java.io.Serializable; +import java.util.regex.Pattern; + +/** + * This is an abstract base class for terminal color definitions. Since there are different ways of specifying terminal + * colors, all with a different range of adoptions, this makes it possible to program an API against an implementation- + * agnostic color definition. Please remember when using colors that not all terminals and terminal emulators supports + * them. The 24-bit color mode is very unsupported, for example, and even the default Linux terminal doesn't support + * the 256-color indexed mode. + * + * @author Martin + */ +public interface TextColor extends Serializable { + /** + * Returns the byte sequence in between CSI and character 'm' that is used to enable this color as the foreground + * color on an ANSI-compatible terminal. + * + * @return Byte array out data to output in between of CSI and 'm' + */ + byte[] getForegroundSGRSequence(); + + /** + * Returns the byte sequence in between CSI and character 'm' that is used to enable this color as the background + * color on an ANSI-compatible terminal. + * + * @return Byte array out data to output in between of CSI and 'm' + */ + byte[] getBackgroundSGRSequence(); + + /** + * @return Red intensity of this color, from 0 to 255 + */ + int getRed(); + + /** + * @return Green intensity of this color, from 0 to 255 + */ + int getGreen(); + + /** + * @return Blue intensity of this color, from 0 to 255 + */ + int getBlue(); + + /** + * Converts this color to an AWT color object, assuming a standard VGA palette. + * + * @return TextColor as an AWT Color + * @deprecated This adds a runtime dependency to the java.desktop module which isn't declared in the module + * descriptor of lanterna. If you want to call this method, make sure to add it to your module. + */ + @Deprecated + Color toColor(); + + /** + * This class represent classic ANSI colors that are likely to be very compatible with most terminal + * implementations. It is limited to 8 colors (plus the 'default' color) but as a norm, using bold mode (SGR code) + * will slightly alter the color, giving it a bit brighter tone, so in total this will give you 16 (+1) colors. + *

+ * For more information, see http://en.wikipedia.org/wiki/File:Ansi.png + */ + enum ANSI implements TextColor { + BLACK(0, 0, 0, 0), + RED(1, 170, 0, 0), + GREEN(2, 0, 170, 0), + YELLOW(3, 170, 85, 0), + BLUE(4, 0, 0, 170), + MAGENTA(5, 170, 0, 170), + CYAN(6, 0, 170, 170), + WHITE(7, 170, 170, 170), + DEFAULT(9, 0, 0, 0), + BLACK_BRIGHT(0, true, 85, 85, 85), + RED_BRIGHT(1, true, 255, 85, 85), + GREEN_BRIGHT(2, true, 85, 255, 85), + YELLOW_BRIGHT(3, true, 255, 255, 85), + BLUE_BRIGHT(4, true, 85, 85, 255), + MAGENTA_BRIGHT(5, true, 255, 85, 255), + CYAN_BRIGHT(6, true, 85, 255, 255), + WHITE_BRIGHT(7, true, 255, 255, 255); + + private final boolean bright; + private final int red; + private final int green; + private final int blue; + private final byte[] foregroundSGR; + private final byte[] backgroundSGR; + + ANSI(int index, int red, int green, int blue) { + this(index, false, red, green, blue); + } + + ANSI(int index, boolean bright, int red, int green, int blue) { + this.bright = bright; + this.red = red; + this.green = green; + this.blue = blue; + foregroundSGR = String.format("%d%d", bright ? 9 : 3, index).getBytes(); + backgroundSGR = String.format("%d%d", bright ? 10 : 4, index).getBytes(); + } + + @Override + public byte[] getForegroundSGRSequence() { + return foregroundSGR.clone(); + } + + @Override + public byte[] getBackgroundSGRSequence() { + return backgroundSGR.clone(); + } + + public boolean isBright() { + return bright; + } + + @Override + public int getRed() { + return red; + } + + @Override + public int getGreen() { + return green; + } + + @Override + public int getBlue() { + return blue; + } + + @Override + public Color toColor() { + return new Color(getRed(), getGreen(), getBlue()); + } + } + + /** + * This class represents a color expressed in the indexed XTerm 256 color extension, where each color is defined in a + * lookup-table. All in all, there are 256 codes, but in order to know which one to know you either need to have the + * table at hand, or you can use the two static helper methods which can help you convert from three 8-bit + * RGB values to the closest approximate indexed color number. If you are interested, the 256 index values are + * actually divided like this:
+ * 0 .. 15 - System colors, same as ANSI, but the actual rendered color depends on the terminal emulators color scheme
+ * 16 .. 231 - Forms a 6x6x6 RGB color cube
+ * 232 .. 255 - A gray scale ramp (without black and white endpoints)
+ *

+ * Support for indexed colors is somewhat widely adopted, not as much as the ANSI colors (TextColor.ANSI) but more + * than the RGB (TextColor.RGB). + *

+ * For more details on this, please see + * this commit message to Konsole. + */ + class Indexed implements TextColor { + private static final byte[][] COLOR_TABLE = new byte[][]{ + //These are the standard 16-color VGA palette entries + {(byte) 0, (byte) 0, (byte) 0}, + {(byte) 170, (byte) 0, (byte) 0}, + {(byte) 0, (byte) 170, (byte) 0}, + {(byte) 170, (byte) 85, (byte) 0}, + {(byte) 0, (byte) 0, (byte) 170}, + {(byte) 170, (byte) 0, (byte) 170}, + {(byte) 0, (byte) 170, (byte) 170}, + {(byte) 170, (byte) 170, (byte) 170}, + {(byte) 85, (byte) 85, (byte) 85}, + {(byte) 255, (byte) 85, (byte) 85}, + {(byte) 85, (byte) 255, (byte) 85}, + {(byte) 255, (byte) 255, (byte) 85}, + {(byte) 85, (byte) 85, (byte) 255}, + {(byte) 255, (byte) 85, (byte) 255}, + {(byte) 85, (byte) 255, (byte) 255}, + {(byte) 255, (byte) 255, (byte) 255}, + + //Starting 6x6x6 RGB color cube from 16 + {(byte) 0x00, (byte) 0x00, (byte) 0x00}, + {(byte) 0x00, (byte) 0x00, (byte) 0x5f}, + {(byte) 0x00, (byte) 0x00, (byte) 0x87}, + {(byte) 0x00, (byte) 0x00, (byte) 0xaf}, + {(byte) 0x00, (byte) 0x00, (byte) 0xd7}, + {(byte) 0x00, (byte) 0x00, (byte) 0xff}, + {(byte) 0x00, (byte) 0x5f, (byte) 0x00}, + {(byte) 0x00, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0x00, (byte) 0x5f, (byte) 0x87}, + {(byte) 0x00, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0x00, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0x00, (byte) 0x5f, (byte) 0xff}, + {(byte) 0x00, (byte) 0x87, (byte) 0x00}, + {(byte) 0x00, (byte) 0x87, (byte) 0x5f}, + {(byte) 0x00, (byte) 0x87, (byte) 0x87}, + {(byte) 0x00, (byte) 0x87, (byte) 0xaf}, + {(byte) 0x00, (byte) 0x87, (byte) 0xd7}, + {(byte) 0x00, (byte) 0x87, (byte) 0xff}, + {(byte) 0x00, (byte) 0xaf, (byte) 0x00}, + {(byte) 0x00, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0x00, (byte) 0xaf, (byte) 0x87}, + {(byte) 0x00, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0x00, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0x00, (byte) 0xaf, (byte) 0xff}, + {(byte) 0x00, (byte) 0xd7, (byte) 0x00}, + {(byte) 0x00, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0x00, (byte) 0xd7, (byte) 0x87}, + {(byte) 0x00, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0x00, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0x00, (byte) 0xd7, (byte) 0xff}, + {(byte) 0x00, (byte) 0xff, (byte) 0x00}, + {(byte) 0x00, (byte) 0xff, (byte) 0x5f}, + {(byte) 0x00, (byte) 0xff, (byte) 0x87}, + {(byte) 0x00, (byte) 0xff, (byte) 0xaf}, + {(byte) 0x00, (byte) 0xff, (byte) 0xd7}, + {(byte) 0x00, (byte) 0xff, (byte) 0xff}, + {(byte) 0x5f, (byte) 0x00, (byte) 0x00}, + {(byte) 0x5f, (byte) 0x00, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0x00, (byte) 0x87}, + {(byte) 0x5f, (byte) 0x00, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0x00, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0x00, (byte) 0xff}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0x00}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0x87}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0x5f, (byte) 0xff}, + {(byte) 0x5f, (byte) 0x87, (byte) 0x00}, + {(byte) 0x5f, (byte) 0x87, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0x87, (byte) 0x87}, + {(byte) 0x5f, (byte) 0x87, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0x87, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0x87, (byte) 0xff}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0x00}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0x87}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0xaf, (byte) 0xff}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0x00}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0x87}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0xd7, (byte) 0xff}, + {(byte) 0x5f, (byte) 0xff, (byte) 0x00}, + {(byte) 0x5f, (byte) 0xff, (byte) 0x5f}, + {(byte) 0x5f, (byte) 0xff, (byte) 0x87}, + {(byte) 0x5f, (byte) 0xff, (byte) 0xaf}, + {(byte) 0x5f, (byte) 0xff, (byte) 0xd7}, + {(byte) 0x5f, (byte) 0xff, (byte) 0xff}, + {(byte) 0x87, (byte) 0x00, (byte) 0x00}, + {(byte) 0x87, (byte) 0x00, (byte) 0x5f}, + {(byte) 0x87, (byte) 0x00, (byte) 0x87}, + {(byte) 0x87, (byte) 0x00, (byte) 0xaf}, + {(byte) 0x87, (byte) 0x00, (byte) 0xd7}, + {(byte) 0x87, (byte) 0x00, (byte) 0xff}, + {(byte) 0x87, (byte) 0x5f, (byte) 0x00}, + {(byte) 0x87, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0x87, (byte) 0x5f, (byte) 0x87}, + {(byte) 0x87, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0x87, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0x87, (byte) 0x5f, (byte) 0xff}, + {(byte) 0x87, (byte) 0x87, (byte) 0x00}, + {(byte) 0x87, (byte) 0x87, (byte) 0x5f}, + {(byte) 0x87, (byte) 0x87, (byte) 0x87}, + {(byte) 0x87, (byte) 0x87, (byte) 0xaf}, + {(byte) 0x87, (byte) 0x87, (byte) 0xd7}, + {(byte) 0x87, (byte) 0x87, (byte) 0xff}, + {(byte) 0x87, (byte) 0xaf, (byte) 0x00}, + {(byte) 0x87, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0x87, (byte) 0xaf, (byte) 0x87}, + {(byte) 0x87, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0x87, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0x87, (byte) 0xaf, (byte) 0xff}, + {(byte) 0x87, (byte) 0xd7, (byte) 0x00}, + {(byte) 0x87, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0x87, (byte) 0xd7, (byte) 0x87}, + {(byte) 0x87, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0x87, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0x87, (byte) 0xd7, (byte) 0xff}, + {(byte) 0x87, (byte) 0xff, (byte) 0x00}, + {(byte) 0x87, (byte) 0xff, (byte) 0x5f}, + {(byte) 0x87, (byte) 0xff, (byte) 0x87}, + {(byte) 0x87, (byte) 0xff, (byte) 0xaf}, + {(byte) 0x87, (byte) 0xff, (byte) 0xd7}, + {(byte) 0x87, (byte) 0xff, (byte) 0xff}, + {(byte) 0xaf, (byte) 0x00, (byte) 0x00}, + {(byte) 0xaf, (byte) 0x00, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0x00, (byte) 0x87}, + {(byte) 0xaf, (byte) 0x00, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0x00, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0x00, (byte) 0xff}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0x00}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0x87}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0x5f, (byte) 0xff}, + {(byte) 0xaf, (byte) 0x87, (byte) 0x00}, + {(byte) 0xaf, (byte) 0x87, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0x87, (byte) 0x87}, + {(byte) 0xaf, (byte) 0x87, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0x87, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0x87, (byte) 0xff}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0x00}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0x87}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0xaf, (byte) 0xff}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0x00}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0x87}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0xd7, (byte) 0xff}, + {(byte) 0xaf, (byte) 0xff, (byte) 0x00}, + {(byte) 0xaf, (byte) 0xff, (byte) 0x5f}, + {(byte) 0xaf, (byte) 0xff, (byte) 0x87}, + {(byte) 0xaf, (byte) 0xff, (byte) 0xaf}, + {(byte) 0xaf, (byte) 0xff, (byte) 0xd7}, + {(byte) 0xaf, (byte) 0xff, (byte) 0xff}, + {(byte) 0xd7, (byte) 0x00, (byte) 0x00}, + {(byte) 0xd7, (byte) 0x00, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0x00, (byte) 0x87}, + {(byte) 0xd7, (byte) 0x00, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0x00, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0x00, (byte) 0xff}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0x00}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0x87}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0x5f, (byte) 0xff}, + {(byte) 0xd7, (byte) 0x87, (byte) 0x00}, + {(byte) 0xd7, (byte) 0x87, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0x87, (byte) 0x87}, + {(byte) 0xd7, (byte) 0x87, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0x87, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0x87, (byte) 0xff}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0x00}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0x87}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0xaf, (byte) 0xff}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0x00}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0x87}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0xd7, (byte) 0xff}, + {(byte) 0xd7, (byte) 0xff, (byte) 0x00}, + {(byte) 0xd7, (byte) 0xff, (byte) 0x5f}, + {(byte) 0xd7, (byte) 0xff, (byte) 0x87}, + {(byte) 0xd7, (byte) 0xff, (byte) 0xaf}, + {(byte) 0xd7, (byte) 0xff, (byte) 0xd7}, + {(byte) 0xd7, (byte) 0xff, (byte) 0xff}, + {(byte) 0xff, (byte) 0x00, (byte) 0x00}, + {(byte) 0xff, (byte) 0x00, (byte) 0x5f}, + {(byte) 0xff, (byte) 0x00, (byte) 0x87}, + {(byte) 0xff, (byte) 0x00, (byte) 0xaf}, + {(byte) 0xff, (byte) 0x00, (byte) 0xd7}, + {(byte) 0xff, (byte) 0x00, (byte) 0xff}, + {(byte) 0xff, (byte) 0x5f, (byte) 0x00}, + {(byte) 0xff, (byte) 0x5f, (byte) 0x5f}, + {(byte) 0xff, (byte) 0x5f, (byte) 0x87}, + {(byte) 0xff, (byte) 0x5f, (byte) 0xaf}, + {(byte) 0xff, (byte) 0x5f, (byte) 0xd7}, + {(byte) 0xff, (byte) 0x5f, (byte) 0xff}, + {(byte) 0xff, (byte) 0x87, (byte) 0x00}, + {(byte) 0xff, (byte) 0x87, (byte) 0x5f}, + {(byte) 0xff, (byte) 0x87, (byte) 0x87}, + {(byte) 0xff, (byte) 0x87, (byte) 0xaf}, + {(byte) 0xff, (byte) 0x87, (byte) 0xd7}, + {(byte) 0xff, (byte) 0x87, (byte) 0xff}, + {(byte) 0xff, (byte) 0xaf, (byte) 0x00}, + {(byte) 0xff, (byte) 0xaf, (byte) 0x5f}, + {(byte) 0xff, (byte) 0xaf, (byte) 0x87}, + {(byte) 0xff, (byte) 0xaf, (byte) 0xaf}, + {(byte) 0xff, (byte) 0xaf, (byte) 0xd7}, + {(byte) 0xff, (byte) 0xaf, (byte) 0xff}, + {(byte) 0xff, (byte) 0xd7, (byte) 0x00}, + {(byte) 0xff, (byte) 0xd7, (byte) 0x5f}, + {(byte) 0xff, (byte) 0xd7, (byte) 0x87}, + {(byte) 0xff, (byte) 0xd7, (byte) 0xaf}, + {(byte) 0xff, (byte) 0xd7, (byte) 0xd7}, + {(byte) 0xff, (byte) 0xd7, (byte) 0xff}, + {(byte) 0xff, (byte) 0xff, (byte) 0x00}, + {(byte) 0xff, (byte) 0xff, (byte) 0x5f}, + {(byte) 0xff, (byte) 0xff, (byte) 0x87}, + {(byte) 0xff, (byte) 0xff, (byte) 0xaf}, + {(byte) 0xff, (byte) 0xff, (byte) 0xd7}, + {(byte) 0xff, (byte) 0xff, (byte) 0xff}, + + //Grey-scale ramp from 232 + {(byte) 0x08, (byte) 0x08, (byte) 0x08}, + {(byte) 0x12, (byte) 0x12, (byte) 0x12}, + {(byte) 0x1c, (byte) 0x1c, (byte) 0x1c}, + {(byte) 0x26, (byte) 0x26, (byte) 0x26}, + {(byte) 0x30, (byte) 0x30, (byte) 0x30}, + {(byte) 0x3a, (byte) 0x3a, (byte) 0x3a}, + {(byte) 0x44, (byte) 0x44, (byte) 0x44}, + {(byte) 0x4e, (byte) 0x4e, (byte) 0x4e}, + {(byte) 0x58, (byte) 0x58, (byte) 0x58}, + {(byte) 0x62, (byte) 0x62, (byte) 0x62}, + {(byte) 0x6c, (byte) 0x6c, (byte) 0x6c}, + {(byte) 0x76, (byte) 0x76, (byte) 0x76}, + {(byte) 0x80, (byte) 0x80, (byte) 0x80}, + {(byte) 0x8a, (byte) 0x8a, (byte) 0x8a}, + {(byte) 0x94, (byte) 0x94, (byte) 0x94}, + {(byte) 0x9e, (byte) 0x9e, (byte) 0x9e}, + {(byte) 0xa8, (byte) 0xa8, (byte) 0xa8}, + {(byte) 0xb2, (byte) 0xb2, (byte) 0xb2}, + {(byte) 0xbc, (byte) 0xbc, (byte) 0xbc}, + {(byte) 0xc6, (byte) 0xc6, (byte) 0xc6}, + {(byte) 0xd0, (byte) 0xd0, (byte) 0xd0}, + {(byte) 0xda, (byte) 0xda, (byte) 0xda}, + {(byte) 0xe4, (byte) 0xe4, (byte) 0xe4}, + {(byte) 0xee, (byte) 0xee, (byte) 0xee} + }; + + private final int colorIndex; + + /** + * Creates a new TextColor using the XTerm 256 color indexed mode, with the specified index value. You must + * choose a value between 0 and 255. + * + * @param colorIndex Index value to use for this color. + */ + public Indexed(int colorIndex) { + if (colorIndex > 255 || colorIndex < 0) { + throw new IllegalArgumentException("Cannot create a Color.Indexed with a color index of " + colorIndex + + ", must be in the range of 0-255"); + } + this.colorIndex = colorIndex; + } + + @Override + public byte[] getForegroundSGRSequence() { + return ("38;5;" + colorIndex).getBytes(); + } + + @Override + public byte[] getBackgroundSGRSequence() { + return ("48;5;" + colorIndex).getBytes(); + } + + @Override + public int getRed() { + return COLOR_TABLE[colorIndex][0] & 0x000000ff; + } + + @Override + public int getGreen() { + return COLOR_TABLE[colorIndex][1] & 0x000000ff; + } + + @Override + public int getBlue() { + return COLOR_TABLE[colorIndex][2] & 0x000000ff; + } + + @Override + public Color toColor() { + return new Color(getRed(), getGreen(), getBlue()); + } + + @Override + public String toString() { + return "{IndexedColor:" + colorIndex + "}"; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 43 * hash + this.colorIndex; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Indexed other = (Indexed) obj; + return this.colorIndex == other.colorIndex; + } + + /** + * Picks out a color approximated from the supplied RGB components + * + * @param red Red intensity, from 0 to 255 + * @param green Red intensity, from 0 to 255 + * @param blue Red intensity, from 0 to 255 + * @return Nearest color from the 6x6x6 RGB color cube or from the 24 entries grey-scale ramp (whichever is closest) + */ + public static Indexed fromRGB(int red, int green, int blue) { + if (red < 0 || red > 255) { + throw new IllegalArgumentException("fromRGB: red is outside of valid range (0-255)"); + } + if (green < 0 || green > 255) { + throw new IllegalArgumentException("fromRGB: green is outside of valid range (0-255)"); + } + if (blue < 0 || blue > 255) { + throw new IllegalArgumentException("fromRGB: blue is outside of valid range (0-255)"); + } + + int rescaledRed = (int) (((double) red / 255.0) * 5.0); + int rescaledGreen = (int) (((double) green / 255.0) * 5.0); + int rescaledBlue = (int) (((double) blue / 255.0) * 5.0); + + int index = rescaledBlue + (6 * rescaledGreen) + (36 * rescaledRed) + 16; + Indexed fromColorCube = new Indexed(index); + Indexed fromGreyRamp = fromGreyRamp((red + green + blue) / 3); + + //Now figure out which one is closest + int coloredDistance = ((red - fromColorCube.getRed()) * (red - fromColorCube.getRed())) + + ((green - fromColorCube.getGreen()) * (green - fromColorCube.getGreen())) + + ((blue - fromColorCube.getBlue()) * (blue - fromColorCube.getBlue())); + int greyDistance = ((red - fromGreyRamp.getRed()) * (red - fromGreyRamp.getRed())) + + ((green - fromGreyRamp.getGreen()) * (green - fromGreyRamp.getGreen())) + + ((blue - fromGreyRamp.getBlue()) * (blue - fromGreyRamp.getBlue())); + if (coloredDistance < greyDistance) { + return fromColorCube; + } else { + return fromGreyRamp; + } + } + + /** + * Picks out a color from the grey-scale ramp area of the color index. + * + * @param intensity Intensity, 0 - 255 + * @return Indexed color from the grey-scale ramp which is the best match for the supplied intensity + */ + private static Indexed fromGreyRamp(int intensity) { + int rescaled = (int) (((double) intensity / 255.0) * 23.0) + 232; + return new Indexed(rescaled); + } + } + + /** + * This class can be used to specify a color in 24-bit color space (RGB with 8-bit resolution per color). Please be + * aware that only a few terminal support 24-bit color control codes, please avoid using this class unless you know + * all users will have compatible terminals. For details, please see + * + * this commit log. Behavior on terminals that don't support these codes is undefined. + */ + class RGB implements TextColor { + private final int red; + private final int green; + private final int blue; + + /** + * This class can be used to specify a color in 24-bit color space (RGB with 8-bit resolution per color). Please be + * aware that only a few terminal support 24-bit color control codes, please avoid using this class unless you know + * all users will have compatible terminals. For details, please see + * + * this commit log. Behavior on terminals that don't support these codes is undefined. + * + * @param r Red intensity, from 0 to 255 + * @param g Green intensity, from 0 to 255 + * @param b Blue intensity, from 0 to 255 + */ + public RGB(int r, int g, int b) { + if (r < 0 || r > 255) { + throw new IllegalArgumentException("RGB: r is outside of valid range (0-255)"); + } + if (g < 0 || g > 255) { + throw new IllegalArgumentException("RGB: g is outside of valid range (0-255)"); + } + if (b < 0 || b > 255) { + throw new IllegalArgumentException("RGB: b is outside of valid range (0-255)"); + } + this.red = r; + this.green = g; + this.blue = b; + } + + @Override + public byte[] getForegroundSGRSequence() { + return ("38;2;" + getRed() + ";" + getGreen() + ";" + getBlue()).getBytes(); + } + + @Override + public byte[] getBackgroundSGRSequence() { + return ("48;2;" + getRed() + ";" + getGreen() + ";" + getBlue()).getBytes(); + } + + @Override + public int getRed() { + return red; + } + + @Override + public int getGreen() { + return green; + } + + @Override + public int getBlue() { + return blue; + } + + @Override + public Color toColor() { + return new Color(getRed(), getGreen(), getBlue()); + } + + @Override + public String toString() { + return "{RGB:" + getRed() + "," + getGreen() + "," + getBlue() + "}"; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + red; + hash = 29 * hash + green; + hash = 29 * hash + blue; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final RGB other = (RGB) obj; + return this.red == other.red + && this.green == other.green + && this.blue == other.blue; + } + } + + /** + * Utility class to instantiate colors from other types and definitions + */ + class Factory { + private static final Pattern INDEXED_COLOR = Pattern.compile("#[0-9]{1,3}"); + private static final Pattern RGB_COLOR = Pattern.compile("#[0-9a-fA-F]{6}"); + + private Factory() { + } + + /** + * Parses a string into a color. The string can have one of three formats: + *

    + *
  • blue - Constant value from the {@link ANSI} enum
  • + *
  • #17 - Hash character followed by one to three numbers; picks the color with that index from + * the 256 color palette
  • + *
  • #1a1a1a - Hash character followed by three hex-decimal tuples; creates an RGB color entry by + * parsing the tuples as Red, Green and Blue
  • + *
+ * + * @param value The string value to parse + * @return A {@link TextColor} that is either an {@link ANSI}, an {@link Indexed} or an {@link RGB} depending on + * the format of the string, or {@code null} if {@code value} is {@code null}. + */ + public static TextColor fromString(String value) { + if (value == null) { + return null; + } + value = value.trim(); + if (RGB_COLOR.matcher(value).matches()) { + int r = Integer.parseInt(value.substring(1, 3), 16); + int g = Integer.parseInt(value.substring(3, 5), 16); + int b = Integer.parseInt(value.substring(5, 7), 16); + return new TextColor.RGB(r, g, b); + } else if (INDEXED_COLOR.matcher(value).matches()) { + int index = Integer.parseInt(value.substring(1)); + return new TextColor.Indexed(index); + } + try { + return TextColor.ANSI.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unknown color definition \"" + value + "\"", e); + } + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/BundleLocator.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/BundleLocator.java new file mode 100644 index 0000000000000000000000000000000000000000..6451b5d6601ed4a7ec567fa2efe534ae7b37680e --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/BundleLocator.java @@ -0,0 +1,116 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.bundle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +/** + * This class permits to deal easily with bundles. + * + * @author silveryocha + */ +public abstract class BundleLocator { + + private final String bundleName; + private static final ClassLoader loader = BundleLocator.class.getClassLoader(); + + /** + * Hidden constructor. + * + * @param bundleName the name of the bundle. + */ + protected BundleLocator(final String bundleName) { + this.bundleName = bundleName; + } + + /** + * Method that centralizes the way to get the value associated to a bundle key. + * + * @param locale the locale. + * @param key the key searched for. + * @param parameters the parameters to apply to the value associated to the key. + * @return the formatted value associated to the given key. Empty string if no value exists for + * the given key. + */ + protected String getBundleKeyValue(Locale locale, String key, Object... parameters) { + String value = null; + try { + value = getBundle(locale).getString(key); + } catch (Exception ignore) { + } + return value != null ? MessageFormat.format(value, parameters) : null; + } + + /** + * Gets the right bundle.
+ * A cache is handled as well as the concurrent accesses. + * + * @param locale the locale. + * @return the instance of the bundle. + */ + private ResourceBundle getBundle(Locale locale) { + return ResourceBundle.getBundle(bundleName, locale, loader, new UTF8Control()); + } + + // Taken from: + // http://stackoverflow.com/questions/4659929/how-to-use-utf-8-in-resource-properties-with-resourcebundle + // I politely refuse to use ISO-8859-1 in these *multi-lingual* property files + // All credits to poster BalusC (http://stackoverflow.com/users/157882/balusc) + private static class UTF8Control extends ResourceBundle.Control { + public ResourceBundle newBundle + (String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + throws IOException { + // The below is a copy of the default implementation. + String bundleName = toBundleName(baseName, locale); + String resourceName = toResourceName(bundleName, "properties"); + ResourceBundle bundle = null; + InputStream stream = null; + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + stream = connection.getInputStream(); + } + } + } else { + stream = loader.getResourceAsStream(resourceName); + } + if (stream != null) { + try { + // Only this line is changed to make it to read properties files as UTF-8. + bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } finally { + stream.close(); + } + } + return bundle; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/DefaultTheme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/DefaultTheme.java new file mode 100644 index 0000000000000000000000000000000000000000..6f27dc5ab9f7e016059dc68166cdcba0e08b42f8 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/DefaultTheme.java @@ -0,0 +1,194 @@ +package com.googlecode.lanterna.bundle; + +import com.googlecode.lanterna.graphics.PropertyTheme; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; + +class DefaultTheme extends PropertyTheme { + DefaultTheme() { + super(definitionAsProperty(), false); + } + + private static Properties definitionAsProperty() { + Properties properties = new Properties(); + try { + properties.load(new StringReader(definition)); + return properties; + } catch (IOException e) { + // We should never get here! + throw new RuntimeException("Unexpected I/O error", e); + } + } + + private static final String definition = "# This is the default properties\n" + + "# If you want to modify this theme, you must do so both here and in DefaultTheme.java\n" + + "\n" + + "foreground = black\n" + + "background = white\n" + + "sgr =\n" + + "foreground[SELECTED] = white\n" + + "background[SELECTED] = blue\n" + + "sgr[SELECTED] = bold\n" + + "foreground[PRELIGHT] = white\n" + + "background[PRELIGHT] = blue\n" + + "sgr[PRELIGHT] = bold\n" + + "foreground[ACTIVE] = white\n" + + "background[ACTIVE] = blue\n" + + "sgr[ACTIVE] = bold\n" + + "foreground[INSENSITIVE] = white\n" + + "background[INSENSITIVE] = blue\n" + + "sgr[INSENSITIVE] =\n" + + "\n" + + "# By default use the shadow post-renderer\n" + + "postrenderer = com.googlecode.lanterna.gui2.WindowShadowRenderer\n" + + "\n" + + "#Borders\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.background[PRELIGHT] = white\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.foreground[ACTIVE] = black\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.background[ACTIVE] = white\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.sgr[ACTIVE] =\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.AbstractBorder.background[INSENSITIVE] = white\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[HORIZONTAL_LINE] = \\u2500\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[VERTICAL_LINE] = \\u2502\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[BOTTOM_LEFT_CORNER] = \\u2514\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[TOP_LEFT_CORNER] = \\u250c\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[BOTTOM_RIGHT_CORNER] = \\u2518\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[TOP_RIGHT_CORNER] = \\u2510\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[TITLE_LEFT] = \\u2500\n" + + "com.googlecode.lanterna.gui2.Borders$SingleLine.char[TITLE_RIGHT] = \\u2500\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[HORIZONTAL_LINE] = \\u2550\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[VERTICAL_LINE] = \\u2551\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[BOTTOM_LEFT_CORNER] = \\u255a\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[TOP_LEFT_CORNER] = \\u2554\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[BOTTOM_RIGHT_CORNER] = \\u255d\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[TOP_RIGHT_CORNER] = \\u2557\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[TITLE_LEFT] = \\u2550\n" + + "com.googlecode.lanterna.gui2.Borders$DoubleLine.char[TITLE_RIGHT] = \\u2550\n" + + "\n" + + "#Button\n" + + "com.googlecode.lanterna.gui2.Button.renderer = com.googlecode.lanterna.gui2.Button$DefaultButtonRenderer\n" + + "com.googlecode.lanterna.gui2.Button.sgr = bold\n" + + "com.googlecode.lanterna.gui2.Button.foreground[SELECTED] = yellow\n" + + "com.googlecode.lanterna.gui2.Button.foreground[PRELIGHT] = red\n" + + "com.googlecode.lanterna.gui2.Button.background[PRELIGHT] = white\n" + + "com.googlecode.lanterna.gui2.Button.sgr[PRELIGHT] =\n" + + "com.googlecode.lanterna.gui2.Button.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.Button.background[INSENSITIVE] = white\n" + + "com.googlecode.lanterna.gui2.Button.char[LEFT_BORDER] = <\n" + + "com.googlecode.lanterna.gui2.Button.char[RIGHT_BORDER] = >\n" + + "\n" + + "# CheckBox\n" + + "com.googlecode.lanterna.gui2.CheckBox.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.CheckBox.background[INSENSITIVE] = white\n" + + "com.googlecode.lanterna.gui2.CheckBox.char[MARKER] = x\n" + + "\n" + + "# CheckBoxList\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.foreground[SELECTED] = black\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.background[SELECTED] = white\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.sgr[SELECTED] =\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.char[LEFT_BRACKET] = [\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.char[RIGHT_BRACKET] = ]\n" + + "com.googlecode.lanterna.gui2.CheckBoxList.char[MARKER] = x\n" + + "\n" + + "# ComboBox\n" + + "com.googlecode.lanterna.gui2.ComboBox.sgr[PRELIGHT] =\n" + + "com.googlecode.lanterna.gui2.ComboBox.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.ComboBox.background[INSENSITIVE] = white\n" + + "com.googlecode.lanterna.gui2.ComboBox.foreground[SELECTED] = black\n" + + "com.googlecode.lanterna.gui2.ComboBox.background[SELECTED] = white\n" + + "\n" + + "# Default color and style for the window decoration renderer\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.foreground[ACTIVE] = black\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.background[ACTIVE] = white\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.sgr[ACTIVE] =\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.background[INSENSITIVE] = white\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.background[PRELIGHT] = white\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[HORIZONTAL_LINE] = \\u2500\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[VERTICAL_LINE] = \\u2502\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[BOTTOM_LEFT_CORNER] = \\u2514\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[TOP_LEFT_CORNER] = \\u250c\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[BOTTOM_RIGHT_CORNER] = \\u2518\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[TOP_RIGHT_CORNER] = \\u2510\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[TITLE_SEPARATOR_LEFT] = \\u2500\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.char[TITLE_SEPARATOR_RIGHT] = \\u2500\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.property[TITLE_PADDING] = false\n" + + "com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer.property[CENTER_TITLE] = false\n" + + "\n" + + "# GUI Backdrop\n" + + "com.googlecode.lanterna.gui2.GUIBackdrop.foreground = cyan\n" + + "com.googlecode.lanterna.gui2.GUIBackdrop.background = blue\n" + + "com.googlecode.lanterna.gui2.GUIBackdrop.sgr = bold\n" + + "\n" + + "# List boxes default\n" + + "com.googlecode.lanterna.gui2.AbstractListBox.foreground[INSENSITIVE] = black\n" + + "com.googlecode.lanterna.gui2.AbstractListBox.background[INSENSITIVE] = white\n" + + "\n" + + "# Menu\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.foreground[PRELIGHT] = red\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.background[PRELIGHT] = white\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.sgr[PRELIGHT] =\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.foreground[ACTIVE] = red\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.background[ACTIVE] = green\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.sgr[ACTIVE] =\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.foreground[SELECTED] = black\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.background[SELECTED] = green\n" + + "com.googlecode.lanterna.gui2.menu.MenuItem.sgr[SELECTED] =\n" + + "\n" + + "# ProgressBar\n" + + "com.googlecode.lanterna.gui2.ProgressBar.foreground = white\n" + + "com.googlecode.lanterna.gui2.ProgressBar.background = blue\n" + + "com.googlecode.lanterna.gui2.ProgressBar.sgr = bold\n" + + "com.googlecode.lanterna.gui2.ProgressBar.background[ACTIVE] = red\n" + + "com.googlecode.lanterna.gui2.ProgressBar.foreground[PRELIGHT] = red\n" + + "com.googlecode.lanterna.gui2.ProgressBar.sgr[PRELIGHT] =\n" + + "com.googlecode.lanterna.gui2.ProgressBar.char[FILLER] =\n" + + "\n" + + "# RadioBoxList\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.foreground[SELECTED] = black\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.background[SELECTED] = white\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.sgr[SELECTED] =\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.char[LEFT_BRACKET] = <\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.char[RIGHT_BRACKET] = >\n" + + "com.googlecode.lanterna.gui2.RadioBoxList.char[MARKER] = o\n" + + "\n" + + "# ScrollBar\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[UP_ARROW]=\\u25b2\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[DOWN_ARROW]=\\u25bc\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[LEFT_ARROW]=\\u25c4\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[RIGHT_ARROW]=\\u25ba\n" + + "\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[VERTICAL_BACKGROUND]=\\u2592\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[VERTICAL_SMALL_TRACKER]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[VERTICAL_TRACKER_BACKGROUND]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[VERTICAL_TRACKER_TOP]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[VERTICAL_TRACKER_BOTTOM]=\\u2588\n" + + "\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[HORIZONTAL_BACKGROUND]=\\u2592\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[HORIZONTAL_SMALL_TRACKER]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[HORIZONTAL_TRACKER_BACKGROUND]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[HORIZONTAL_TRACKER_LEFT]=\\u2588\n" + + "com.googlecode.lanterna.gui2.ScrollBar.char[HORIZONTAL_TRACKER_RIGHT]=\\u2588\n" + + "\n" + + "# Separator\n" + + "com.googlecode.lanterna.gui2.Separator.sgr = bold\n" + + "\n" + + "# Table\n" + + "com.googlecode.lanterna.gui2.table.Table.sgr[HEADER] = underline,bold\n" + + "com.googlecode.lanterna.gui2.table.Table.foreground[SELECTED] = black\n" + + "com.googlecode.lanterna.gui2.table.Table.background[SELECTED] = white\n" + + "com.googlecode.lanterna.gui2.table.Table.sgr[SELECTED] =\n" + + "\n" + + "# TextBox\n" + + "com.googlecode.lanterna.gui2.TextBox.foreground = white\n" + + "com.googlecode.lanterna.gui2.TextBox.background = blue\n" + + "\n" + + "# Window shadow\n" + + "com.googlecode.lanterna.gui2.WindowShadowRenderer.background = black\n" + + "com.googlecode.lanterna.gui2.WindowShadowRenderer.sgr = bold\n" + + "com.googlecode.lanterna.gui2.WindowShadowRenderer.property[DOUBLE_WIDTH] = true\n" + + "com.googlecode.lanterna.gui2.WindowShadowRenderer.property[TRANSPARENT] = true"; +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LanternaThemes.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LanternaThemes.java new file mode 100644 index 0000000000000000000000000000000000000000..622c5aa00c15127e12facd45a87a4848c346785f --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LanternaThemes.java @@ -0,0 +1,134 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.bundle; + +import com.googlecode.lanterna.graphics.PropertyTheme; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.gui2.AbstractTextGUI; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Catalog of available themes, this class will initially contain the themes bundled with Lanterna but it is possible to + * add additional themes as well. + */ +public class LanternaThemes { + private LanternaThemes() { + // No instantiation + } + + private static final ConcurrentHashMap REGISTERED_THEMES = new ConcurrentHashMap<>(); + + static { + // Register the default theme first + Properties defaultThemeProperties = loadPropTheme("default-theme.properties"); + if (defaultThemeProperties != null) { + registerPropTheme("default", defaultThemeProperties); + } else { + // If we couldn't load it from file, use the hard-coded one instead + registerTheme("default", new DefaultTheme()); + } + + // Now register all bundled themes that are defined in property files (if found) + registerPropTheme("bigsnake", loadPropTheme("bigsnake-theme.properties")); + registerPropTheme("businessmachine", loadPropTheme("businessmachine-theme.properties")); + registerPropTheme("conqueror", loadPropTheme("conqueror-theme.properties")); + registerPropTheme("defrost", loadPropTheme("defrost-theme.properties")); + registerPropTheme("blaster", loadPropTheme("blaster-theme.properties")); + } + + /** + * Returns a collection of all themes registered with this class, by their name. To get the associated {@link Theme} + * object, please use {@link #getRegisteredTheme(String)}. + * + * @return Collection of theme names + */ + public static Collection getRegisteredThemes() { + return new ArrayList<>(REGISTERED_THEMES.keySet()); + } + + /** + * Returns the {@link Theme} registered with this class under {@code name}, or {@code null} if there is no such + * registration. + * + * @param name Name of the theme to retrieve + * @return {@link Theme} registered with the supplied name, or {@code null} if none + */ + public static Theme getRegisteredTheme(String name) { + return REGISTERED_THEMES.get(name); + } + + /** + * Registers a {@link Theme} with this class under a certain name so that calling + * {@link #getRegisteredTheme(String)} on that name will return this theme and calling + * {@link #getRegisteredThemes()} will return a collection including this name. + * + * @param name Name to register the theme under + * @param theme Theme to register with this name + */ + public static void registerTheme(String name, Theme theme) { + if (theme == null) { + throw new IllegalArgumentException("Name cannot be null"); + } else if (name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be empty"); + } + Theme result = REGISTERED_THEMES.putIfAbsent(name, theme); + if (result != null && result != theme) { + throw new IllegalArgumentException("There is already a theme registered with the name '" + name + "'"); + } + } + + /** + * Returns lanterna's default theme which is used if no other theme is selected. + * + * @return Lanterna's default theme, as a {@link Theme} + */ + public static Theme getDefaultTheme() { + return REGISTERED_THEMES.get("default"); + } + + private static void registerPropTheme(String name, Properties properties) { + if (properties != null) { + registerTheme(name, new PropertyTheme(properties, false)); + } + } + + private static Properties loadPropTheme(String resourceFileName) { + Properties properties = new Properties(); + try { + ClassLoader classLoader = AbstractTextGUI.class.getClassLoader(); + InputStream resourceAsStream = classLoader.getResourceAsStream(resourceFileName); + if (resourceAsStream == null) { + resourceAsStream = new FileInputStream("src/main/resources/" + resourceFileName); + } + properties.load(resourceAsStream); + resourceAsStream.close(); + return properties; + } catch (IOException e) { + // Failed to load theme, return null + return null; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LocalizedUIBundle.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LocalizedUIBundle.java new file mode 100644 index 0000000000000000000000000000000000000000..caebd95e4c72ddde29f9c4425726e1ee288e343d --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LocalizedUIBundle.java @@ -0,0 +1,43 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.bundle; + +import java.util.Locale; + +/** + * This class permits to get easily localized strings about the UI. + * + * @author silveryocha + */ +public class LocalizedUIBundle extends BundleLocator { + + private static final LocalizedUIBundle MY_BUNDLE = new LocalizedUIBundle("multilang.lanterna-ui"); + + public static String get(String key, String... parameters) { + return get(Locale.getDefault(), key, parameters); + } + + public static String get(Locale locale, String key, String... parameters) { + return MY_BUNDLE.getBundleKeyValue(locale, key, (Object[]) parameters); + } + + private LocalizedUIBundle(final String bundleName) { + super(bundleName); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..3d3086f6cd8a4f8022aafcd64df438cf07de32a1 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTextGraphics.java @@ -0,0 +1,383 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.*; +import com.googlecode.lanterna.screen.TabBehaviour; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; + +/** + * This class hold the default logic for drawing the basic text graphic as exposed by TextGraphic. All implementations + * rely on a setCharacter method being implemented in subclasses. + * + * @author Martin + */ +public abstract class AbstractTextGraphics implements TextGraphics { + protected TextColor foregroundColor; + protected TextColor backgroundColor; + protected TabBehaviour tabBehaviour; + protected final EnumSet activeModifiers; + private final ShapeRenderer shapeRenderer; + + protected AbstractTextGraphics() { + this.activeModifiers = EnumSet.noneOf(SGR.class); + this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4; + this.foregroundColor = TextColor.ANSI.DEFAULT; + this.backgroundColor = TextColor.ANSI.DEFAULT; + this.shapeRenderer = new DefaultShapeRenderer(this::setCharacter); + } + + @Override + public TextColor getBackgroundColor() { + return backgroundColor; + } + + @Override + public TextGraphics setBackgroundColor(final TextColor backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + @Override + public TextColor getForegroundColor() { + return foregroundColor; + } + + @Override + public TextGraphics setForegroundColor(final TextColor foregroundColor) { + this.foregroundColor = foregroundColor; + return this; + } + + @Override + public TextGraphics enableModifiers(SGR... modifiers) { + enableModifiers(Arrays.asList(modifiers)); + return this; + } + + private void enableModifiers(Collection modifiers) { + this.activeModifiers.addAll(modifiers); + } + + @Override + public TextGraphics disableModifiers(SGR... modifiers) { + disableModifiers(Arrays.asList(modifiers)); + return this; + } + + private void disableModifiers(Collection modifiers) { + this.activeModifiers.removeAll(modifiers); + } + + @Override + public synchronized TextGraphics setModifiers(EnumSet modifiers) { + activeModifiers.clear(); + activeModifiers.addAll(modifiers); + return this; + } + + @Override + public TextGraphics clearModifiers() { + this.activeModifiers.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return activeModifiers; + } + + @Override + public TabBehaviour getTabBehaviour() { + return tabBehaviour; + } + + @Override + public TextGraphics setTabBehaviour(TabBehaviour tabBehaviour) { + if (tabBehaviour != null) { + this.tabBehaviour = tabBehaviour; + } + return this; + } + + @Override + public TextGraphics fill(char c) { + fillRectangle(TerminalPosition.TOP_LEFT_CORNER, getSize(), c); + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, char character) { + return setCharacter(column, row, newTextCharacter(character)); + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) { + setCharacter(position.getColumn(), position.getRow(), textCharacter); + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, char character) { + return setCharacter(position.getColumn(), position.getRow(), character); + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPosition, TerminalPosition toPoint, char character) { + return drawLine(fromPosition, toPoint, newTextCharacter(character)); + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) { + shapeRenderer.drawLine(fromPoint, toPoint, character); + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) { + return drawLine(fromX, fromY, toX, toY, newTextCharacter(character)); + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) { + return drawLine(new TerminalPosition(fromX, fromY), new TerminalPosition(toX, toY), character); + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return drawTriangle(p1, p2, p3, newTextCharacter(character)); + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + shapeRenderer.drawTriangle(p1, p2, p3, character); + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return fillTriangle(p1, p2, p3, newTextCharacter(character)); + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + shapeRenderer.fillTriangle(p1, p2, p3, character); + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return drawRectangle(topLeft, size, newTextCharacter(character)); + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + shapeRenderer.drawRectangle(topLeft, size, character); + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return fillRectangle(topLeft, size, newTextCharacter(character)); + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + shapeRenderer.fillRectangle(topLeft, size, character); + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) { + return drawImage(topLeft, image, TerminalPosition.TOP_LEFT_CORNER, image.getSize()); + } + + @Override + public TextGraphics drawImage( + TerminalPosition topLeft, + TextImage image, + TerminalPosition sourceImageTopLeft, + TerminalSize sourceImageSize) { + + // If the source image position is negative, offset the whole image + if (sourceImageTopLeft.getColumn() < 0) { + topLeft = topLeft.withRelativeColumn(-sourceImageTopLeft.getColumn()); + sourceImageSize = sourceImageSize.withRelativeColumns(sourceImageTopLeft.getColumn()); + sourceImageTopLeft = sourceImageTopLeft.withColumn(0); + } + if (sourceImageTopLeft.getRow() < 0) { + topLeft = topLeft.withRelativeRow(-sourceImageTopLeft.getRow()); + sourceImageSize = sourceImageSize.withRelativeRows(sourceImageTopLeft.getRow()); + sourceImageTopLeft = sourceImageTopLeft.withRow(0); + } + + // cropping specified image-subrectangle to the image itself: + int fromRow = Math.max(sourceImageTopLeft.getRow(), 0); + int untilRow = Math.min(sourceImageTopLeft.getRow() + sourceImageSize.getRows(), image.getSize().getRows()); + int fromColumn = Math.max(sourceImageTopLeft.getColumn(), 0); + int untilColumn = Math.min(sourceImageTopLeft.getColumn() + sourceImageSize.getColumns(), image.getSize().getColumns()); + + // difference between position in image and position on target: + int diffRow = topLeft.getRow() - sourceImageTopLeft.getRow(); + int diffColumn = topLeft.getColumn() - sourceImageTopLeft.getColumn(); + + // top/left-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate) + fromRow = Math.max(fromRow, -diffRow); + fromColumn = Math.max(fromColumn, -diffColumn); + + // bot/right-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate) + untilRow = Math.min(untilRow, getSize().getRows() - diffRow); + untilColumn = Math.min(untilColumn, getSize().getColumns() - diffColumn); + + if (fromRow >= untilRow || fromColumn >= untilColumn) { + return this; + } + for (int row = fromRow; row < untilRow; row++) { + for (int column = fromColumn; column < untilColumn; column++) { + setCharacter(column + diffColumn, row + diffRow, image.getCharacterAt(column, row)); + } + } + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string) { + string = prepareStringForPut(column, string); + int offset = 0; + for (int i = 0; i < string.length(); i++) { + char character = string.charAt(i); + setCharacter(column + offset, row, newTextCharacter(character)); + offset += getOffsetToNextCharacter(character); + } + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string) { + putString(position.getColumn(), position.getRow(), string); + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + clearModifiers(); + return putString(column, row, string, EnumSet.of(extraModifier, optionalExtraModifiers)); + } + + @Override + public TextGraphics putString(int column, int row, String string, Collection extraModifiers) { + Collection newModifiers = EnumSet.copyOf(extraModifiers); + newModifiers.removeAll(activeModifiers); + enableModifiers(newModifiers); + putString(column, row, string); + disableModifiers(newModifiers); + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + putString(position.getColumn(), position.getRow(), string, extraModifier, optionalExtraModifiers); + return this; + } + + @Override + public synchronized TextGraphics putCSIStyledString(int column, int row, String string) { + StyleSet.Set original = new StyleSet.Set(this); + string = prepareStringForPut(column, string); + int offset = 0; + for (int i = 0; i < string.length(); i++) { + char character = string.charAt(i); + String controlSequence = TerminalTextUtils.getANSIControlSequenceAt(string, i); + if (controlSequence != null) { + TerminalTextUtils.updateModifiersFromCSICode(controlSequence, this, original); + + // Skip the control sequence, leaving one extra, since we'll add it when we loop + i += controlSequence.length() - 1; + continue; + } + + setCharacter(column + offset, row, newTextCharacter(character)); + offset += getOffsetToNextCharacter(character); + } + + setStyleFrom(original); + return this; + } + + @Override + public TextGraphics putCSIStyledString(TerminalPosition position, String string) { + return putCSIStyledString(position.getColumn(), position.getRow(), string); + } + + @Override + public TextCharacter getCharacter(TerminalPosition position) { + return getCharacter(position.getColumn(), position.getRow()); + } + + @Override + public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException { + TerminalSize writableArea = getSize(); + if (topLeftCorner.getColumn() + size.getColumns() <= 0 || + topLeftCorner.getColumn() >= writableArea.getColumns() || + topLeftCorner.getRow() + size.getRows() <= 0 || + topLeftCorner.getRow() >= writableArea.getRows()) { + //The area selected is completely outside of this TextGraphics, so we can return a "null" object that doesn't + //do anything because it is impossible to change anything anyway + return new NullTextGraphics(size); + } + return new SubTextGraphics(this, topLeftCorner, size); + } + + private TextCharacter newTextCharacter(char character) { + return new TextCharacter(character, foregroundColor, backgroundColor, activeModifiers); + } + + private String prepareStringForPut(int column, String string) { + if (string.contains("\n")) { + string = string.substring(0, string.indexOf("\n")); + } + if (string.contains("\r")) { + string = string.substring(0, string.indexOf("\r")); + } + string = tabBehaviour.replaceTabs(string, column); + return string; + } + + private int getOffsetToNextCharacter(char character) { + if (TerminalTextUtils.isCharDoubleWidth(character)) { + //CJK characters are twice the normal characters in width, so next character position is two columns forward + return 2; + } else { + //For "normal" characters we advance to the next column + return 1; + } + } + + @Override + public TextGraphics setStyleFrom(StyleSet source) { + setBackgroundColor(source.getBackgroundColor()); + setForegroundColor(source.getForegroundColor()); + setModifiers(source.getActiveModifiers()); + return this; + } + +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTheme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTheme.java new file mode 100644 index 0000000000000000000000000000000000000000..ee129282fa1837ca977816c16f1bf30eca1250bb --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTheme.java @@ -0,0 +1,462 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.*; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Abstract {@link Theme} implementation that manages a hierarchical tree of theme nodes ties to Class objects. + * Sub-classes will inherit their theme properties from super-class definitions, the java.lang.Object class is + * considered the root of the tree and as such is the fallback for all other classes. + *

+ * You normally use this class through {@link PropertyTheme}, which is the default implementation bundled with Lanterna. + * + * @author Martin + */ +public abstract class AbstractTheme implements Theme { + private static final String STYLE_NORMAL = ""; + private static final String STYLE_PRELIGHT = "PRELIGHT"; + private static final String STYLE_SELECTED = "SELECTED"; + private static final String STYLE_ACTIVE = "ACTIVE"; + private static final String STYLE_INSENSITIVE = "INSENSITIVE"; + private static final Pattern STYLE_FORMAT = Pattern.compile("([a-zA-Z]+)(\\[([a-zA-Z0-9-_]+)])?"); + + private final ThemeTreeNode rootNode; + private final WindowPostRenderer windowPostRenderer; + private final WindowDecorationRenderer windowDecorationRenderer; + + protected AbstractTheme(WindowPostRenderer postRenderer, + WindowDecorationRenderer decorationRenderer) { + + this.rootNode = new ThemeTreeNode(Object.class, null); + this.windowPostRenderer = postRenderer; + this.windowDecorationRenderer = decorationRenderer; + + rootNode.foregroundMap.put(STYLE_NORMAL, TextColor.ANSI.WHITE); + rootNode.backgroundMap.put(STYLE_NORMAL, TextColor.ANSI.BLACK); + classloadStandardRenderersForGraal(); + } + + private void classloadStandardRenderersForGraal() { + // This will make graal know about these classes which would otherwise only + // be loaded through reflection + WindowShadowRenderer.class.toString(); + Button.DefaultButtonRenderer.class.toString(); + Button.FlatButtonRenderer.class.toString(); + Button.BorderedButtonRenderer.class.toString(); + } + + protected boolean addStyle(String definition, String style, String value) { + ThemeTreeNode node = getNode(definition); + if (node == null) { + return false; + } + node.apply(style, value); + return true; + } + + private ThemeTreeNode getNode(String definition) { + try { + if (definition == null || definition.trim().isEmpty()) { + return getNode(Object.class); + } else { + return getNode(Class.forName(definition)); + } + } catch (ClassNotFoundException e) { + return null; + } + } + + private ThemeTreeNode getNode(Class definition) { + if (definition == Object.class) { + return rootNode; + } + ThemeTreeNode parent = getNode(definition.getSuperclass()); + if (parent.childMap.containsKey(definition)) { + return parent.childMap.get(definition); + } + + ThemeTreeNode node = new ThemeTreeNode(definition, parent); + parent.childMap.put(definition, node); + return node; + } + + @Override + public ThemeDefinition getDefaultDefinition() { + return new DefinitionImpl(rootNode); + } + + @Override + public ThemeDefinition getDefinition(Class clazz) { + LinkedList> hierarchy = new LinkedList<>(); + while (clazz != null && clazz != Object.class) { + hierarchy.addFirst(clazz); + clazz = clazz.getSuperclass(); + } + + ThemeTreeNode node = rootNode; + for (Class aClass : hierarchy) { + if (node.childMap.containsKey(aClass)) { + node = node.childMap.get(aClass); + } else { + break; + } + } + return new DefinitionImpl(node); + } + + @Override + public WindowPostRenderer getWindowPostRenderer() { + return windowPostRenderer; + } + + @Override + public WindowDecorationRenderer getWindowDecorationRenderer() { + return windowDecorationRenderer; + } + + protected static Object instanceByClassName(String className) { + if (className == null || className.trim().isEmpty()) { + return null; + } + try { + return Class.forName(className).newInstance(); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a list of redundant theme entries in this theme. A redundant entry means that it doesn't need to be + * specified because there is a parent node in the hierarchy which has the same property so if the redundant entry + * wasn't there, the parent node would be picked up and the end result would be the same. + * + * @return List of redundant theme entries + */ + public List findRedundantDeclarations() { + List result = new ArrayList<>(); + for (ThemeTreeNode node : rootNode.childMap.values()) { + findRedundantDeclarations(result, node); + } + Collections.sort(result); + return result; + } + + private void findRedundantDeclarations(List result, ThemeTreeNode node) { + for (String style : node.foregroundMap.keySet()) { + String formattedStyle = "[" + style + "]"; + if (formattedStyle.length() == 2) { + formattedStyle = ""; + } + TextColor color = node.foregroundMap.get(style); + TextColor colorFromParent = new StyleImpl(node.parent, style).getForeground(); + if (color.equals(colorFromParent)) { + result.add(node.clazz.getName() + ".foreground" + formattedStyle); + } + } + for (String style : node.backgroundMap.keySet()) { + String formattedStyle = "[" + style + "]"; + if (formattedStyle.length() == 2) { + formattedStyle = ""; + } + TextColor color = node.backgroundMap.get(style); + TextColor colorFromParent = new StyleImpl(node.parent, style).getBackground(); + if (color.equals(colorFromParent)) { + result.add(node.clazz.getName() + ".background" + formattedStyle); + } + } + for (String style : node.sgrMap.keySet()) { + String formattedStyle = "[" + style + "]"; + if (formattedStyle.length() == 2) { + formattedStyle = ""; + } + EnumSet sgrs = node.sgrMap.get(style); + EnumSet sgrsFromParent = new StyleImpl(node.parent, style).getSGRs(); + if (sgrs.equals(sgrsFromParent)) { + result.add(node.clazz.getName() + ".sgr" + formattedStyle); + } + } + + for (ThemeTreeNode childNode : node.childMap.values()) { + findRedundantDeclarations(result, childNode); + } + } + + private class DefinitionImpl implements ThemeDefinition { + final ThemeTreeNode node; + + public DefinitionImpl(ThemeTreeNode node) { + this.node = node; + } + + @Override + public ThemeStyle getNormal() { + return new StyleImpl(node, STYLE_NORMAL); + } + + @Override + public ThemeStyle getPreLight() { + return new StyleImpl(node, STYLE_PRELIGHT); + } + + @Override + public ThemeStyle getSelected() { + return new StyleImpl(node, STYLE_SELECTED); + } + + @Override + public ThemeStyle getActive() { + return new StyleImpl(node, STYLE_ACTIVE); + } + + @Override + public ThemeStyle getInsensitive() { + return new StyleImpl(node, STYLE_INSENSITIVE); + } + + @Override + public ThemeStyle getCustom(String name) { + return new StyleImpl(node, name); + } + + @Override + public ThemeStyle getCustom(String name, ThemeStyle defaultValue) { + ThemeStyle customStyle = getCustom(name); + if (customStyle == null) { + customStyle = defaultValue; + } + return customStyle; + } + + @Override + public char getCharacter(String name, char fallback) { + Character character = node.characterMap.get(name); + if (character == null) { + if (node == rootNode) { + return fallback; + } else { + return new DefinitionImpl(node.parent).getCharacter(name, fallback); + } + } + return character; + } + + @Override + public boolean isCursorVisible() { + Boolean cursorVisible = node.cursorVisible; + if (cursorVisible == null) { + if (node == rootNode) { + return true; + } else { + return new DefinitionImpl(node.parent).isCursorVisible(); + } + } + return cursorVisible; + } + + @Override + public boolean getBooleanProperty(String name, boolean defaultValue) { + String propertyValue = node.propertyMap.get(name); + if (propertyValue == null) { + if (node == rootNode) { + return defaultValue; + } else { + return new DefinitionImpl(node.parent).getBooleanProperty(name, defaultValue); + } + } + return Boolean.parseBoolean(propertyValue); + } + + @SuppressWarnings("unchecked") + @Override + public ComponentRenderer getRenderer(Class type) { + String rendererClass = node.renderer; + if (rendererClass == null) { + if (node == rootNode) { + return null; + } else { + return new DefinitionImpl(node.parent).getRenderer(type); + } + } + return (ComponentRenderer) instanceByClassName(rendererClass); + } + } + + private class StyleImpl implements ThemeStyle { + private final ThemeTreeNode styleNode; + private final String name; + + private StyleImpl(ThemeTreeNode node, String name) { + this.styleNode = node; + this.name = name; + } + + @Override + public TextColor getForeground() { + ThemeTreeNode node = styleNode; + while (node != null) { + if (node.foregroundMap.containsKey(name)) { + return node.foregroundMap.get(name); + } + node = node.parent; + } + TextColor fallback = rootNode.foregroundMap.get(STYLE_NORMAL); + if (fallback == null) { + fallback = TextColor.ANSI.WHITE; + } + return fallback; + } + + @Override + public TextColor getBackground() { + ThemeTreeNode node = styleNode; + while (node != null) { + if (node.backgroundMap.containsKey(name)) { + return node.backgroundMap.get(name); + } + node = node.parent; + } + TextColor fallback = rootNode.backgroundMap.get(STYLE_NORMAL); + if (fallback == null) { + fallback = TextColor.ANSI.BLACK; + } + return fallback; + } + + @Override + public EnumSet getSGRs() { + ThemeTreeNode node = styleNode; + while (node != null) { + if (node.sgrMap.containsKey(name)) { + return EnumSet.copyOf(node.sgrMap.get(name)); + } + node = node.parent; + } + EnumSet fallback = rootNode.sgrMap.get(STYLE_NORMAL); + if (fallback == null) { + fallback = EnumSet.noneOf(SGR.class); + } + return EnumSet.copyOf(fallback); + } + } + + private static class ThemeTreeNode { + private final Class clazz; + private final ThemeTreeNode parent; + private final Map, ThemeTreeNode> childMap; + private final Map foregroundMap; + private final Map backgroundMap; + private final Map> sgrMap; + private final Map characterMap; + private final Map propertyMap; + private Boolean cursorVisible; + private String renderer; + + private ThemeTreeNode(Class clazz, ThemeTreeNode parent) { + this.clazz = clazz; + this.parent = parent; + this.childMap = new HashMap<>(); + this.foregroundMap = new HashMap<>(); + this.backgroundMap = new HashMap<>(); + this.sgrMap = new HashMap<>(); + this.characterMap = new HashMap<>(); + this.propertyMap = new HashMap<>(); + this.cursorVisible = true; + this.renderer = null; + } + + private void apply(String style, String value) { + value = value.trim(); + Matcher matcher = STYLE_FORMAT.matcher(style); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unknown style declaration: " + style); + } + String styleComponent = matcher.group(1); + String group = matcher.groupCount() > 2 ? matcher.group(3) : null; + switch (styleComponent.toLowerCase().trim()) { + case "foreground": + foregroundMap.put(getCategory(group), parseValue(value)); + break; + case "background": + backgroundMap.put(getCategory(group), parseValue(value)); + break; + case "sgr": + sgrMap.put(getCategory(group), parseSGR(value)); + break; + case "char": + characterMap.put(getCategory(group), value.isEmpty() ? ' ' : value.charAt(0)); + break; + case "cursor": + cursorVisible = Boolean.parseBoolean(value); + break; + case "property": + propertyMap.put(getCategory(group), value.isEmpty() ? null : value.trim()); + break; + case "renderer": + renderer = value.trim().isEmpty() ? null : value.trim(); + break; + case "postrenderer": + case "windowdecoration": + // Don't do anything with this now, we might use it later + break; + default: + throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\""); + } + } + + private TextColor parseValue(String value) { + return TextColor.Factory.fromString(value); + } + + private EnumSet parseSGR(String value) { + value = value.trim(); + String[] sgrEntries = value.split(","); + EnumSet sgrSet = EnumSet.noneOf(SGR.class); + for (String entry : sgrEntries) { + entry = entry.trim().toUpperCase(); + if (!entry.isEmpty()) { + try { + sgrSet.add(SGR.valueOf(entry)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unknown SGR code \"" + entry + "\"", e); + } + } + } + return sgrSet; + } + + private String getCategory(String group) { + if (group == null) { + return STYLE_NORMAL; + } + for (String style : Arrays.asList(STYLE_ACTIVE, STYLE_INSENSITIVE, STYLE_PRELIGHT, STYLE_NORMAL, STYLE_SELECTED)) { + if (group.toUpperCase().equals(style)) { + return style; + } + } + return group; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/BasicTextImage.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/BasicTextImage.java new file mode 100644 index 0000000000000000000000000000000000000000..8c48469086a47ca2b6b3783a12e698cb6a563e86 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/BasicTextImage.java @@ -0,0 +1,352 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.TextColor; + +import java.util.Arrays; + +/** + * Simple implementation of TextImage that keeps the content as a two-dimensional TextCharacter array. Copy operations + * between two BasicTextImage classes are semi-optimized by using System.arraycopy instead of iterating over each + * character and copying them over one by one. + * + * @author martin + */ +public class BasicTextImage implements TextImage { + private final TerminalSize size; + private final TextCharacter[][] buffer; + + /** + * Creates a new BasicTextImage with the specified size and fills it initially with space characters using the + * default foreground and background color + * + * @param columns Size of the image in number of columns + * @param rows Size of the image in number of rows + */ + public BasicTextImage(int columns, int rows) { + this(new TerminalSize(columns, rows)); + } + + /** + * Creates a new BasicTextImage with the specified size and fills it initially with space characters using the + * default foreground and background color + * + * @param size Size to make the image + */ + public BasicTextImage(TerminalSize size) { + this(size, new TextCharacter(' ', TextColor.ANSI.DEFAULT, TextColor.ANSI.DEFAULT)); + } + + /** + * Creates a new BasicTextImage with a given size and a TextCharacter to initially fill it with + * + * @param size Size of the image + * @param initialContent What character to set as the initial content + */ + public BasicTextImage(TerminalSize size, TextCharacter initialContent) { + this(size, new TextCharacter[0][], initialContent); + } + + /** + * Creates a new BasicTextImage by copying a region of a two-dimensional array of TextCharacter:s. If the area to be + * copied to larger than the source array, a filler character is used. + * + * @param size Size to create the new BasicTextImage as (and size to copy from the array) + * @param toCopy Array to copy initial data from + * @param initialContent Filler character to use if the source array is smaller than the requested size + */ + private BasicTextImage(TerminalSize size, TextCharacter[][] toCopy, TextCharacter initialContent) { + if (size == null || toCopy == null || initialContent == null) { + throw new IllegalArgumentException("Cannot create BasicTextImage with null " + + (size == null ? "size" : (toCopy == null ? "toCopy" : "filler"))); + } + this.size = size; + + int rows = size.getRows(); + int columns = size.getColumns(); + buffer = new TextCharacter[rows][]; + for (int y = 0; y < rows; y++) { + buffer[y] = new TextCharacter[columns]; + for (int x = 0; x < columns; x++) { + if (y < toCopy.length && x < toCopy[y].length) { + buffer[y][x] = toCopy[y][x]; + } else { + buffer[y][x] = initialContent; + } + } + } + } + + @Override + public TerminalSize getSize() { + return size; + } + + @Override + public void setAll(TextCharacter character) { + if (character == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setAll(..) with null character"); + } + for (TextCharacter[] line : buffer) { + Arrays.fill(line, character); + } + } + + @Override + public BasicTextImage resize(TerminalSize newSize, TextCharacter filler) { + if (newSize == null || filler == null) { + throw new IllegalArgumentException("Cannot resize BasicTextImage with null " + + (newSize == null ? "newSize" : "filler")); + } + if (newSize.getRows() == buffer.length && + (buffer.length == 0 || newSize.getColumns() == buffer[0].length)) { + return this; + } + return new BasicTextImage(newSize, buffer, filler); + } + + @Override + public void setCharacterAt(TerminalPosition position, TextCharacter character) { + if (position == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setCharacterAt(..) with null position"); + } + setCharacterAt(position.getColumn(), position.getRow(), character); + } + + @Override + public void setCharacterAt(int column, int row, TextCharacter character) { + if (character == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.setCharacterAt(..) with null character"); + } + if (column < 0 || row < 0 || row >= buffer.length || column >= buffer[0].length) { + return; + } + + // Double width character adjustments + if (column > 0 && buffer[row][column - 1].isDoubleWidth()) { + buffer[row][column - 1] = buffer[row][column - 1].withCharacter(' '); + } + + // Assign the character at location we specified + buffer[row][column] = character; + + // Double width character adjustments + if (character.isDoubleWidth() && column + 1 < buffer[0].length) { + buffer[row][column + 1] = character.withCharacter(' '); + } + } + + @Override + public TextCharacter getCharacterAt(TerminalPosition position) { + if (position == null) { + throw new IllegalArgumentException("Cannot call BasicTextImage.getCharacterAt(..) with null position"); + } + return getCharacterAt(position.getColumn(), position.getRow()); + } + + @Override + public TextCharacter getCharacterAt(int column, int row) { + if (column < 0 || row < 0 || row >= buffer.length || column >= buffer[0].length) { + return null; + } + + return buffer[row][column]; + } + + @Override + public void copyTo(TextImage destination) { + if (buffer.length > 0) { + copyTo(destination, 0, buffer.length, 0, buffer[0].length, 0, 0); + } + } + + @Override + public void copyTo( + TextImage destination, + int startRowIndex, + int rows, + int startColumnIndex, + int columns, + int destinationRowOffset, + int destinationColumnOffset) { + + // If the source image position is negative, offset the whole image + if (startColumnIndex < 0) { + destinationColumnOffset += -startColumnIndex; + columns += startColumnIndex; + startColumnIndex = 0; + } + if (startRowIndex < 0) { + destinationRowOffset += -startRowIndex; + rows += startRowIndex; + startRowIndex = 0; + } + + // If the destination offset is negative, adjust the source start indexes + if (destinationColumnOffset < 0) { + startColumnIndex -= destinationColumnOffset; + columns += destinationColumnOffset; + destinationColumnOffset = 0; + } + if (destinationRowOffset < 0) { + startRowIndex -= destinationRowOffset; + rows += destinationRowOffset; + destinationRowOffset = 0; + } + + //Make sure we can't copy more than is available + rows = Math.min(buffer.length - startRowIndex, rows); + columns = rows > 0 ? Math.min(buffer[0].length - startColumnIndex, columns) : 0; + + //Adjust target lengths as well + columns = Math.min(destination.getSize().getColumns() - destinationColumnOffset, columns); + rows = Math.min(destination.getSize().getRows() - destinationRowOffset, rows); + + if (columns <= 0 || rows <= 0) { + return; + } + + TerminalSize destinationSize = destination.getSize(); + if (destination instanceof BasicTextImage) { + int targetRow = destinationRowOffset; + for (int y = startRowIndex; y < startRowIndex + rows && targetRow < destinationSize.getRows(); y++) { + System.arraycopy(buffer[y], startColumnIndex, ((BasicTextImage) destination).buffer[targetRow++], destinationColumnOffset, columns); + } + } else { + //Manually copy character by character + for (int y = startRowIndex; y < startRowIndex + rows; y++) { + for (int x = startColumnIndex; x < startColumnIndex + columns; x++) { + TextCharacter character = buffer[y][x]; + if (character.isDoubleWidth()) { + // If we're about to put a double-width character, first reset the character next to it + if (x + 1 < startColumnIndex + columns) { + destination.setCharacterAt( + x - startColumnIndex + destinationColumnOffset, + y - startRowIndex + destinationRowOffset, + character.withCharacter(' ')); + } + // If the last character is a double-width character, it would exceed the dimension so reset it + else if (x + 1 == startColumnIndex + columns) { + character = character.withCharacter(' '); + } + } + destination.setCharacterAt( + x - startColumnIndex + destinationColumnOffset, + y - startRowIndex + destinationRowOffset, + character); + if (character.isDoubleWidth()) { + x++; + } + } + } + } + + // If the character immediately to the left in the destination is double-width, then reset it + if (destinationColumnOffset > 0) { + int destinationX = destinationColumnOffset - 1; + for (int y = startRowIndex; y < startRowIndex + rows; y++) { + int destinationY = y - startRowIndex + destinationRowOffset; + TextCharacter neighbour = destination.getCharacterAt(destinationX, destinationY); + if (neighbour.isDoubleWidth()) { + destination.setCharacterAt(destinationX, destinationY, neighbour.withCharacter(' ')); + } + } + } + } + + @Override + public TextGraphics newTextGraphics() { + return new AbstractTextGraphics() { + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + BasicTextImage.this.setCharacterAt(columnIndex, rowIndex, textCharacter); + return this; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + return BasicTextImage.this.getCharacterAt(column, row); + } + + @Override + public TerminalSize getSize() { + return size; + } + }; + } + + private TextCharacter[] newBlankLine() { + TextCharacter[] line = new TextCharacter[size.getColumns()]; + Arrays.fill(line, TextCharacter.DEFAULT_CHARACTER); + return line; + } + + @Override + public void scrollLines(int firstLine, int lastLine, int distance) { + if (firstLine < 0) { + firstLine = 0; + } + if (lastLine >= size.getRows()) { + lastLine = size.getRows() - 1; + } + if (firstLine < lastLine) { + if (distance > 0) { + // scrolling up: start with first line as target: + int curLine = firstLine; + // copy lines from further "below": + for (; curLine <= lastLine - distance; curLine++) { + buffer[curLine] = buffer[curLine + distance]; + } + // blank out the remaining lines: + for (; curLine <= lastLine; curLine++) { + buffer[curLine] = newBlankLine(); + } + } else if (distance < 0) { + // scrolling down: start with last line as target: + int curLine = lastLine; + distance = -distance; + // copy lines from further "above": + for (; curLine >= firstLine + distance; curLine--) { + buffer[curLine] = buffer[curLine - distance]; + } + // blank out the remaining lines: + for (; curLine >= firstLine; curLine--) { + buffer[curLine] = newBlankLine(); + } + } /* else: distance == 0 => no-op */ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(size.getRows() * (size.getColumns() + 1) + 50); + sb.append('{').append(size.getColumns()).append('x').append(size.getRows()).append('}').append('\n'); + for (TextCharacter[] line : buffer) { + for (TextCharacter tc : line) { + sb.append(tc.getCharacterString()); + } + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultMutableThemeStyle.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultMutableThemeStyle.java new file mode 100644 index 0000000000000000000000000000000000000000..8c419082eb9fcaed7b3c5486aa83297d2b78ce1a --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultMutableThemeStyle.java @@ -0,0 +1,123 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TextColor; + +import java.util.Arrays; +import java.util.EnumSet; + +/** + * This basic implementation of ThemeStyle keeps the styles in its internal state and allows you to mutate them. It can + * be used to more easily override an existing theme and make small changes programmatically to it, see Issue409 in the + * test section for an example of how to do this. + * + * @see DelegatingThemeDefinition + * @see DelegatingTheme + * @see Theme + */ +public class DefaultMutableThemeStyle implements ThemeStyle { + private TextColor foreground; + private TextColor background; + private EnumSet sgrs; + + /** + * Creates a new {@link DefaultMutableThemeStyle} based on an existing {@link ThemeStyle}. The values of this style + * that is passed in will be copied into the new object that is created. + * + * @param themeStyleToCopy {@link ThemeStyle} object to copy the style parameters from + */ + public DefaultMutableThemeStyle(ThemeStyle themeStyleToCopy) { + this(themeStyleToCopy.getForeground(), + themeStyleToCopy.getBackground(), + themeStyleToCopy.getSGRs()); + } + + /** + * Creates a new {@link DefaultMutableThemeStyle} with a specified style (foreground, background and SGR state) + * + * @param foreground Foreground color of the text with this style + * @param background Background color of the text with this style + * @param sgrs Modifiers to apply to the text with this style + */ + public DefaultMutableThemeStyle(TextColor foreground, TextColor background, SGR... sgrs) { + this(foreground, background, sgrs.length > 0 ? EnumSet.copyOf(Arrays.asList(sgrs)) : EnumSet.noneOf(SGR.class)); + } + + private DefaultMutableThemeStyle(TextColor foreground, TextColor background, EnumSet sgrs) { + if (foreground == null) { + throw new IllegalArgumentException("Cannot set SimpleTheme's style foreground to null"); + } + if (background == null) { + throw new IllegalArgumentException("Cannot set SimpleTheme's style background to null"); + } + this.foreground = foreground; + this.background = background; + this.sgrs = EnumSet.copyOf(sgrs); + } + + @Override + public TextColor getForeground() { + return foreground; + } + + @Override + public TextColor getBackground() { + return background; + } + + @Override + public EnumSet getSGRs() { + return EnumSet.copyOf(sgrs); + } + + /** + * Modifies the foreground color of this {@link DefaultMutableThemeStyle} to the value passed in + * + * @param foreground New foreground color for this theme style + * @return Itself + */ + public DefaultMutableThemeStyle setForeground(TextColor foreground) { + this.foreground = foreground; + return this; + } + + /** + * Modifies the background color of this {@link DefaultMutableThemeStyle} to the value passed in + * + * @param background New background color for this theme style + * @return Itself + */ + public DefaultMutableThemeStyle setBackground(TextColor background) { + this.background = background; + return this; + } + + /** + * Modifies the SGR modifiers of this {@link DefaultMutableThemeStyle} to the values passed it. + * + * @param sgrs New SGR modifiers for this theme style, the values in this set will be copied into the internal state + * @return Itself + */ + public DefaultMutableThemeStyle setSGRs(EnumSet sgrs) { + this.sgrs = EnumSet.copyOf(sgrs); + return this; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..eb218c7e0e3f7752fcb65a1f0662e28aed11b067 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java @@ -0,0 +1,194 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * Default implementation of ShapeRenderer. This class (and the interface) is mostly here to make the code cleaner in + * {@code AbstractTextGraphics}. + * + * @author Martin + */ +class DefaultShapeRenderer implements ShapeRenderer { + interface Callback { + void onPoint(int column, int row, TextCharacter character); + } + + private final Callback callback; + + DefaultShapeRenderer(Callback callback) { + this.callback = callback; + } + + @Override + public void drawLine(TerminalPosition p1, TerminalPosition p2, TextCharacter character) { + //http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm + //Implementation from Graphics Programming Black Book by Michael Abrash + //Available at http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/graphics-programming-black-book-r1698 + if (p1.getRow() > p2.getRow()) { + TerminalPosition temp = p1; + p1 = p2; + p2 = temp; + } + int deltaX = p2.getColumn() - p1.getColumn(); + int deltaY = p2.getRow() - p1.getRow(); + if (deltaX > 0) { + if (deltaX > deltaY) { + drawLine0(p1, deltaX, deltaY, true, character); + } else { + drawLine1(p1, deltaX, deltaY, true, character); + } + } else { + deltaX = Math.abs(deltaX); + if (deltaX > deltaY) { + drawLine0(p1, deltaX, deltaY, false, character); + } else { + drawLine1(p1, deltaX, deltaY, false, character); + } + } + } + + private void drawLine0(TerminalPosition start, int deltaX, int deltaY, boolean leftToRight, TextCharacter character) { + int x = start.getColumn(); + int y = start.getRow(); + int deltaYx2 = deltaY * 2; + int deltaYx2MinusDeltaXx2 = deltaYx2 - (deltaX * 2); + int errorTerm = deltaYx2 - deltaX; + callback.onPoint(x, y, character); + while (deltaX-- > 0) { + if (errorTerm >= 0) { + y++; + errorTerm += deltaYx2MinusDeltaXx2; + } else { + errorTerm += deltaYx2; + } + x += leftToRight ? 1 : -1; + callback.onPoint(x, y, character); + } + } + + private void drawLine1(TerminalPosition start, int deltaX, int deltaY, boolean leftToRight, TextCharacter character) { + int x = start.getColumn(); + int y = start.getRow(); + int deltaXx2 = deltaX * 2; + int deltaXx2MinusDeltaYx2 = deltaXx2 - (deltaY * 2); + int errorTerm = deltaXx2 - deltaY; + callback.onPoint(x, y, character); + while (deltaY-- > 0) { + if (errorTerm >= 0) { + x += leftToRight ? 1 : -1; + errorTerm += deltaXx2MinusDeltaYx2; + } else { + errorTerm += deltaXx2; + } + y++; + callback.onPoint(x, y, character); + } + } + + @Override + public void drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + drawLine(p1, p2, character); + drawLine(p2, p3, character); + drawLine(p3, p1, character); + } + + @Override + public void drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + TerminalPosition topRight = topLeft.withRelativeColumn(size.getColumns() - 1); + TerminalPosition bottomRight = topRight.withRelativeRow(size.getRows() - 1); + TerminalPosition bottomLeft = topLeft.withRelativeRow(size.getRows() - 1); + drawLine(topLeft, topRight, character); + drawLine(topRight, bottomRight, character); + drawLine(bottomRight, bottomLeft, character); + drawLine(bottomLeft, topLeft, character); + } + + @Override + public void fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + //I've used the algorithm described here: + //http://www-users.mat.uni.torun.pl/~wrona/3d_tutor/tri_fillers.html + TerminalPosition[] points = new TerminalPosition[]{p1, p2, p3}; + Arrays.sort(points, Comparator.comparingInt(TerminalPosition::getRow)); + + float dx1, dx2, dx3; + if (points[1].getRow() - points[0].getRow() > 0) { + dx1 = (float) (points[1].getColumn() - points[0].getColumn()) / (float) (points[1].getRow() - points[0].getRow()); + } else { + dx1 = 0; + } + if (points[2].getRow() - points[0].getRow() > 0) { + dx2 = (float) (points[2].getColumn() - points[0].getColumn()) / (float) (points[2].getRow() - points[0].getRow()); + } else { + dx2 = 0; + } + if (points[2].getRow() - points[1].getRow() > 0) { + dx3 = (float) (points[2].getColumn() - points[1].getColumn()) / (float) (points[2].getRow() - points[1].getRow()); + } else { + dx3 = 0; + } + + float startX, startY, endX; + startX = endX = points[0].getColumn(); + startY = points[0].getRow(); + if (dx1 > dx2) { + for (; startY <= points[1].getRow(); startY++, startX += dx2, endX += dx1) { + drawLine(new TerminalPosition((int) startX, (int) startY), new TerminalPosition((int) endX, (int) startY), character); + } + endX = points[1].getColumn(); + for (; startY <= points[2].getRow(); startY++, startX += dx2, endX += dx3) { + drawLine(new TerminalPosition((int) startX, (int) startY), new TerminalPosition((int) endX, (int) startY), character); + } + } else { + for (; startY <= points[1].getRow(); startY++, startX += dx1, endX += dx2) { + drawLine(new TerminalPosition((int) startX, (int) startY), new TerminalPosition((int) endX, (int) startY), character); + } + startX = points[1].getColumn(); + startY = points[1].getRow(); + for (; startY <= points[2].getRow(); startY++, startX += dx3, endX += dx2) { + drawLine(new TerminalPosition((int) startX, (int) startY), new TerminalPosition((int) endX, (int) startY), character); + } + } + } + + @Override + public void fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + final boolean characterDoubleWidth = character.isDoubleWidth(); + for (int y = 0; y < size.getRows(); y++) { + for (int x = 0; x < size.getColumns(); x++) { + // Don't put a double-width character at the right edge of the area + if (characterDoubleWidth && x + 1 == size.getColumns()) { + callback.onPoint(topLeft.getColumn() + x, topLeft.getRow() + y, character.withCharacter(' ')); + } else { + // Default case + callback.onPoint(topLeft.getColumn() + x, topLeft.getRow() + y, character); + } + if (characterDoubleWidth) { + x++; + } + } + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingTheme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingTheme.java new file mode 100644 index 0000000000000000000000000000000000000000..ced2e9c90d19519f3289916788fa5a1307dd1d36 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingTheme.java @@ -0,0 +1,65 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.gui2.WindowDecorationRenderer; +import com.googlecode.lanterna.gui2.WindowPostRenderer; + +/** + * Allows you to more easily wrap an existing theme and alter the behaviour in some special cases. You normally create a + * new class that extends from this and override some of the methods to divert the call depending on what you are trying + * to do. For an example, please see Issue409 in the test code. + * + * @see DelegatingThemeDefinition + * @see DefaultMutableThemeStyle + * @see Theme + */ +public class DelegatingTheme implements Theme { + private final Theme theme; + + /** + * Creates a new {@link DelegatingTheme} with a default implementation that will forward all calls to the + * {@link Theme} that is passed in. + * + * @param theme Other theme to delegate all calls to + */ + public DelegatingTheme(Theme theme) { + this.theme = theme; + } + + @Override + public ThemeDefinition getDefaultDefinition() { + return theme.getDefaultDefinition(); + } + + @Override + public ThemeDefinition getDefinition(Class clazz) { + return theme.getDefinition(clazz); + } + + @Override + public WindowPostRenderer getWindowPostRenderer() { + return theme.getWindowPostRenderer(); + } + + @Override + public WindowDecorationRenderer getWindowDecorationRenderer() { + return theme.getWindowDecorationRenderer(); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingThemeDefinition.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingThemeDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..e2830055c109e8efa2b5f0804154624d83a296ea --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingThemeDefinition.java @@ -0,0 +1,100 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.gui2.Component; +import com.googlecode.lanterna.gui2.ComponentRenderer; + +/** + * Allows you to more easily wrap an existing theme definion and alter the behaviour in some special cases. You normally + * create a new class that extends from this and override some of the methods to divert the call depending on what you + * are trying to do. For an example, please see Issue409 in the test code. + * + * @see DelegatingTheme + * @see DefaultMutableThemeStyle + * @see Theme + */ +public class DelegatingThemeDefinition implements ThemeDefinition { + private final ThemeDefinition themeDefinition; + + /** + * Creates a new {@link DelegatingThemeDefinition} with a default implementation that will forward all calls to the + * {@link ThemeDefinition} that is passed in. + * + * @param themeDefinition Other theme definition to delegate all calls to + */ + public DelegatingThemeDefinition(ThemeDefinition themeDefinition) { + this.themeDefinition = themeDefinition; + } + + @Override + public ThemeStyle getNormal() { + return themeDefinition.getNormal(); + } + + @Override + public ThemeStyle getPreLight() { + return themeDefinition.getPreLight(); + } + + @Override + public ThemeStyle getSelected() { + return themeDefinition.getSelected(); + } + + @Override + public ThemeStyle getActive() { + return themeDefinition.getActive(); + } + + @Override + public ThemeStyle getInsensitive() { + return themeDefinition.getInsensitive(); + } + + @Override + public ThemeStyle getCustom(String name) { + return themeDefinition.getCustom(name); + } + + @Override + public ThemeStyle getCustom(String name, ThemeStyle defaultValue) { + return themeDefinition.getCustom(name, defaultValue); + } + + @Override + public boolean getBooleanProperty(String name, boolean defaultValue) { + return themeDefinition.getBooleanProperty(name, defaultValue); + } + + @Override + public boolean isCursorVisible() { + return themeDefinition.isCursorVisible(); + } + + @Override + public char getCharacter(String name, char fallback) { + return themeDefinition.getCharacter(name, fallback); + } + + @Override + public ComponentRenderer getRenderer(Class type) { + return themeDefinition.getRenderer(type); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..cff9b80510414191bbb031a9c2237cd2510536a0 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java @@ -0,0 +1,64 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +/** + * This TextGraphics implementation wraps another TextGraphics and forwards all operations to it, but with a few + * differences. First of all, each individual character being printed is printed twice. Secondly, if you call + * {@code getSize()}, it will return a size that has half the width of the underlying TextGraphics. This presents the + * writable view as somewhat squared, since normally terminal characters are twice as tall as wide. You can see some + * examples of how this looks by running the Triangle test in {@code com.googlecode.lanterna.screen.ScreenTriangleTest} + * and compare it when running with the --square parameter and without. + */ +public class DoublePrintingTextGraphics extends AbstractTextGraphics { + private final TextGraphics underlyingTextGraphics; + + /** + * Creates a new {@code DoublePrintingTextGraphics} on top of a supplied {@code TextGraphics} + * + * @param underlyingTextGraphics backend {@code TextGraphics} to forward all the calls to + */ + public DoublePrintingTextGraphics(TextGraphics underlyingTextGraphics) { + this.underlyingTextGraphics = underlyingTextGraphics; + } + + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + columnIndex = columnIndex * 2; + underlyingTextGraphics.setCharacter(columnIndex, rowIndex, textCharacter); + underlyingTextGraphics.setCharacter(columnIndex + 1, rowIndex, textCharacter); + return this; + } + + @Override + public TextCharacter getCharacter(int columnIndex, int rowIndex) { + columnIndex = columnIndex * 2; + return underlyingTextGraphics.getCharacter(columnIndex, rowIndex); + + } + + @Override + public TerminalSize getSize() { + TerminalSize size = underlyingTextGraphics.getSize(); + return size.withColumns(size.getColumns() / 2); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/NullTextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/NullTextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..9a019ef183524a804f2cef044e243e194cd03843 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/NullTextGraphics.java @@ -0,0 +1,275 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.*; +import com.googlecode.lanterna.screen.TabBehaviour; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; + +/** + * TextGraphics implementation that does nothing, but has a pre-defined size + * + * @author martin + */ +class NullTextGraphics implements TextGraphics { + private final TerminalSize size; + private TextColor foregroundColor; + private TextColor backgroundColor; + private TabBehaviour tabBehaviour; + private final EnumSet activeModifiers; + + /** + * Creates a new {@code NullTextGraphics} that will return the specified size value if asked how big it is but other + * than that ignore all other calls. + * + * @param size The size to report + */ + public NullTextGraphics(TerminalSize size) { + this.size = size; + this.foregroundColor = TextColor.ANSI.DEFAULT; + this.backgroundColor = TextColor.ANSI.DEFAULT; + this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4; + this.activeModifiers = EnumSet.noneOf(SGR.class); + } + + @Override + public TerminalSize getSize() { + return size; + } + + @Override + public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException { + return this; + } + + @Override + public TextColor getBackgroundColor() { + return backgroundColor; + } + + @Override + public TextGraphics setBackgroundColor(TextColor backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + @Override + public TextColor getForegroundColor() { + return foregroundColor; + } + + @Override + public TextGraphics setForegroundColor(TextColor foregroundColor) { + this.foregroundColor = foregroundColor; + return this; + } + + @Override + public TextGraphics enableModifiers(SGR... modifiers) { + activeModifiers.addAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphics disableModifiers(SGR... modifiers) { + activeModifiers.removeAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphics setModifiers(EnumSet modifiers) { + clearModifiers(); + activeModifiers.addAll(modifiers); + return this; + } + + @Override + public TextGraphics clearModifiers() { + activeModifiers.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return EnumSet.copyOf(activeModifiers); + } + + @Override + public TabBehaviour getTabBehaviour() { + return tabBehaviour; + } + + @Override + public TextGraphics setTabBehaviour(TabBehaviour tabBehaviour) { + this.tabBehaviour = tabBehaviour; + return this; + } + + @Override + public TextGraphics fill(char c) { + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, char character) { + return this; + } + + @Override + public TextGraphics setCharacter(int column, int row, TextCharacter character) { + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, char character) { + return this; + } + + @Override + public TextGraphics setCharacter(TerminalPosition position, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) { + return this; + } + + @Override + public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) { + return this; + } + + @Override + public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return this; + } + + @Override + public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { + return this; + } + + @Override + public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return this; + } + + @Override + public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { + return this; + } + + @Override + public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) { + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) { + return this; + } + + @Override + public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string) { + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + return this; + } + + @Override + public TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) { + return this; + } + + @Override + public TextGraphics putString(int column, int row, String string, Collection extraModifiers) { + return this; + } + + @Override + public TextGraphics putCSIStyledString(int column, int row, String string) { + return this; + } + + @Override + public TextGraphics putCSIStyledString(TerminalPosition position, String string) { + return this; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + return null; + } + + @Override + public TextCharacter getCharacter(TerminalPosition position) { + return null; + } + + @Override + public TextGraphics setStyleFrom(StyleSet source) { + setBackgroundColor(source.getBackgroundColor()); + setForegroundColor(source.getForegroundColor()); + setModifiers(source.getActiveModifiers()); + return this; + } + +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/PropertyTheme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/PropertyTheme.java new file mode 100644 index 0000000000000000000000000000000000000000..935301bad997f4bbea281cffefc80224dd17086f --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/PropertyTheme.java @@ -0,0 +1,95 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.gui2.WindowDecorationRenderer; +import com.googlecode.lanterna.gui2.WindowPostRenderer; + +import java.util.Properties; + +/** + * {@link Theme} implementation that stores the theme definition in a regular java Properties object. The format is: + *

+ *     foreground = black
+ *     background = white
+ *     sgr =
+ *     com.mypackage.mycomponent.MyClass.foreground = yellow
+ *     com.mypackage.mycomponent.MyClass.background = white
+ *     com.mypackage.mycomponent.MyClass.sgr =
+ *     com.mypackage.mycomponent.MyClass.foreground[ACTIVE] = red
+ *     com.mypackage.mycomponent.MyClass.background[ACTIVE] = black
+ *     com.mypackage.mycomponent.MyClass.sgr[ACTIVE] = bold
+ *     ...
+ * 
+ *

+ * See the documentation on {@link Theme} for further information about different style categories that can be assigned. + * The foreground, background and sgr entries without a class specifier will be tied to the global fallback and is used + * if the libraries tries to apply a theme style that isn't specified in the Properties object and there is no other + * superclass specified either. + */ +public class PropertyTheme extends AbstractTheme { + /** + * Creates a new {@code PropertyTheme} that is initialized by the properties passed in. If the properties refer to + * a class that cannot be resolved, it will throw {@code IllegalArgumentException}. + * + * @param properties Properties to initialize this theme with + */ + public PropertyTheme(Properties properties) { + this(properties, false); + } + + /** + * Creates a new {@code PropertyTheme} that is initialized by the properties value and optionally prevents it from + * throwing an exception if there are invalid definitions in the properties object. + * + * @param properties Properties to initialize this theme with + * @param ignoreUnknownClasses If {@code true}, will not throw an exception if there is an invalid entry in the + * properties object + */ + public PropertyTheme(Properties properties, boolean ignoreUnknownClasses) { + + super((WindowPostRenderer) instanceByClassName(properties.getProperty("postrenderer", "")), + (WindowDecorationRenderer) instanceByClassName(properties.getProperty("windowdecoration", ""))); + + for (String key : properties.stringPropertyNames()) { + String definition = getDefinition(key); + if (!addStyle(definition, getStyle(key), properties.getProperty(key))) { + if (!ignoreUnknownClasses) { + throw new IllegalArgumentException("Unknown class encountered when parsing theme: '" + definition + "'"); + } + } + } + } + + private String getDefinition(String propertyName) { + if (!propertyName.contains(".")) { + return ""; + } else { + return propertyName.substring(0, propertyName.lastIndexOf(".")); + } + } + + private String getStyle(String propertyName) { + if (!propertyName.contains(".")) { + return propertyName; + } else { + return propertyName.substring(propertyName.lastIndexOf(".") + 1); + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Scrollable.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Scrollable.java new file mode 100644 index 0000000000000000000000000000000000000000..15af42f68cb4ce9316839c487e8228a5bf118113 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Scrollable.java @@ -0,0 +1,47 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import java.io.IOException; + +/** + * Describes an area that can be 'scrolled', by moving a range of lines up or down. Certain terminals will implement + * this through extensions and are much faster than if lanterna tries to manually erase and re-print the text. + * + * @author Andreas + */ +public interface Scrollable { + /** + * Scroll a range of lines of this Scrollable according to given distance. + *

+ * If scroll-range is empty (firstLine > lastLine || distance == 0) then + * this method does nothing. + *

+ * Lines that are scrolled away from are cleared. + *

+ * If absolute value of distance is equal or greater than number of lines + * in range, then all lines within the range will be cleared. + * + * @param firstLine first line of the range to be scrolled (top line is 0) + * @param lastLine last (inclusive) line of the range to be scrolled + * @param distance if > 0: move lines up, else if < 0: move lines down. + * @throws IOException If there was an I/O error when running the operation + */ + void scrollLines(int firstLine, int lastLine, int distance) throws IOException; +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ShapeRenderer.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ShapeRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..dc8c002189a6bbc3ed01c7fbe6a731b317a7c62e --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ShapeRenderer.java @@ -0,0 +1,41 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +/** + * This package private interface exposes methods for translating abstract lines, triangles and rectangles to discreet + * points on a grid. + * + * @author Martin + */ +interface ShapeRenderer { + void drawLine(TerminalPosition p1, TerminalPosition p2, TextCharacter character); + + void drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + + void drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); + + void fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + + void fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SimpleTheme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SimpleTheme.java new file mode 100644 index 0000000000000000000000000000000000000000..76cc33e626c9af35831e91c1f980b389a2998e0c --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SimpleTheme.java @@ -0,0 +1,421 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.gui2.table.Table; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Very basic implementation of {@link Theme} that allows you to quickly define a theme in code. It is a very simple + * implementation that doesn't implement any intelligent fallback based on class hierarchy or package names. If a + * particular class has not been defined with an explicit override, it will get the default theme style definition. + * + * @author Martin + */ +public class SimpleTheme implements Theme { + + /** + * Helper method that will quickly setup a new theme with some sensible component overrides. + * + * @param activeIsBold Should focused components also use bold SGR style? + * @param baseForeground The base foreground color of the theme + * @param baseBackground The base background color of the theme + * @param editableForeground Foreground color for editable components, or editable areas of components + * @param editableBackground Background color for editable components, or editable areas of components + * @param selectedForeground Foreground color for the selection marker when a component has multiple selection states + * @param selectedBackground Background color for the selection marker when a component has multiple selection states + * @param guiBackground Background color of the GUI, if this theme is assigned to the {@link TextGUI} + * @return Assembled {@link SimpleTheme} using the parameters from above + */ + public static SimpleTheme makeTheme( + boolean activeIsBold, + TextColor baseForeground, + TextColor baseBackground, + TextColor editableForeground, + TextColor editableBackground, + TextColor selectedForeground, + TextColor selectedBackground, + TextColor guiBackground) { + + SGR[] activeStyle = activeIsBold ? new SGR[]{SGR.BOLD} : new SGR[0]; + + SimpleTheme theme = new SimpleTheme(baseForeground, baseBackground); + theme.getDefaultDefinition().setSelected(baseBackground, baseForeground, activeStyle); + theme.getDefaultDefinition().setActive(selectedForeground, selectedBackground, activeStyle); + + theme.addOverride(AbstractBorder.class, baseForeground, baseBackground) + .setSelected(baseForeground, baseBackground, activeStyle); + theme.addOverride(AbstractListBox.class, baseForeground, baseBackground) + .setSelected(selectedForeground, selectedBackground, activeStyle); + theme.addOverride(Button.class, baseForeground, baseBackground) + .setActive(selectedForeground, selectedBackground, activeStyle) + .setSelected(selectedForeground, selectedBackground, activeStyle); + theme.addOverride(CheckBox.class, baseForeground, baseBackground) + .setActive(selectedForeground, selectedBackground, activeStyle) + .setPreLight(selectedForeground, selectedBackground, activeStyle) + .setSelected(selectedForeground, selectedBackground, activeStyle); + theme.addOverride(CheckBoxList.class, baseForeground, baseBackground) + .setActive(selectedForeground, selectedBackground, activeStyle); + theme.addOverride(ComboBox.class, baseForeground, baseBackground) + .setActive(editableForeground, editableBackground, activeStyle) + .setPreLight(editableForeground, editableBackground); + theme.addOverride(DefaultWindowDecorationRenderer.class, baseForeground, baseBackground) + .setActive(baseForeground, baseBackground, activeStyle); + theme.addOverride(GUIBackdrop.class, baseForeground, guiBackground); + theme.addOverride(RadioBoxList.class, baseForeground, baseBackground) + .setActive(selectedForeground, selectedBackground, activeStyle); + theme.addOverride(Table.class, baseForeground, baseBackground) + .setActive(editableForeground, editableBackground, activeStyle) + .setSelected(baseForeground, baseBackground); + theme.addOverride(TextBox.class, editableForeground, editableBackground) + .setActive(editableForeground, editableBackground, activeStyle) + .setSelected(editableForeground, editableBackground, activeStyle); + + theme.setWindowPostRenderer(new WindowShadowRenderer()); + + return theme; + } + + private final Definition defaultDefinition; + private final Map, Definition> overrideDefinitions; + private WindowPostRenderer windowPostRenderer; + private WindowDecorationRenderer windowDecorationRenderer; + + /** + * Creates a new {@link SimpleTheme} object that uses the supplied constructor arguments as the default style + * + * @param foreground Color to use as the foreground unless overridden + * @param background Color to use as the background unless overridden + * @param styles Extra SGR styles to apply unless overridden + */ + public SimpleTheme(TextColor foreground, TextColor background, SGR... styles) { + this.defaultDefinition = new Definition(new DefaultMutableThemeStyle(foreground, background, styles)); + this.overrideDefinitions = new HashMap<>(); + this.windowPostRenderer = null; + this.windowDecorationRenderer = null; + } + + @Override + public synchronized Definition getDefaultDefinition() { + return defaultDefinition; + } + + @Override + public synchronized Definition getDefinition(Class clazz) { + Definition definition = overrideDefinitions.get(clazz); + if (definition == null) { + return getDefaultDefinition(); + } + return definition; + } + + /** + * Adds an override for a particular class, or overwrites a previously defined override. + * + * @param clazz Class to override the theme for + * @param foreground Color to use as the foreground color for this override style + * @param background Color to use as the background color for this override style + * @param styles SGR styles to apply for this override + * @return The newly created {@link Definition} that corresponds to this override. + */ + public synchronized Definition addOverride(Class clazz, TextColor foreground, TextColor background, SGR... styles) { + Definition definition = new Definition(new DefaultMutableThemeStyle(foreground, background, styles)); + overrideDefinitions.put(clazz, definition); + return definition; + } + + @Override + public synchronized WindowPostRenderer getWindowPostRenderer() { + return windowPostRenderer; + } + + /** + * Changes the {@link WindowPostRenderer} this theme will return. If called with {@code null}, the theme returns no + * post renderer and the GUI system will use whatever is the default. + * + * @param windowPostRenderer Post-renderer to use along with this theme, or {@code null} to remove + * @return Itself + */ + public synchronized SimpleTheme setWindowPostRenderer(WindowPostRenderer windowPostRenderer) { + this.windowPostRenderer = windowPostRenderer; + return this; + } + + @Override + public synchronized WindowDecorationRenderer getWindowDecorationRenderer() { + return windowDecorationRenderer; + } + + /** + * Changes the {@link WindowDecorationRenderer} this theme will return. If called with {@code null}, the theme + * returns no decoration renderer and the GUI system will use whatever is the default. + * + * @param windowDecorationRenderer Decoration renderer to use along with this theme, or {@code null} to remove + * @return Itself + */ + public synchronized SimpleTheme setWindowDecorationRenderer(WindowDecorationRenderer windowDecorationRenderer) { + this.windowDecorationRenderer = windowDecorationRenderer; + return this; + } + + public interface RendererProvider { + ComponentRenderer getRenderer(Class type); + } + + /** + * Internal class inside {@link SimpleTheme} used to allow basic editing of the default style and the optional + * overrides. + */ + public static class Definition implements ThemeDefinition { + private final ThemeStyle normal; + private ThemeStyle preLight; + private ThemeStyle selected; + private ThemeStyle active; + private ThemeStyle insensitive; + private final Map customStyles; + private final Properties properties; + private final Map characterMap; + private final Map, RendererProvider> componentRendererMap; + private boolean cursorVisible; + + private Definition(ThemeStyle normal) { + this.normal = normal; + this.preLight = null; + this.selected = null; + this.active = null; + this.insensitive = null; + this.customStyles = new HashMap<>(); + this.properties = new Properties(); + this.characterMap = new HashMap<>(); + this.componentRendererMap = new HashMap<>(); + this.cursorVisible = true; + } + + @Override + public synchronized ThemeStyle getNormal() { + return normal; + } + + @Override + public synchronized ThemeStyle getPreLight() { + if (preLight == null) { + return normal; + } + return preLight; + } + + /** + * Sets the theme definition style "prelight" + * + * @param foreground Foreground color for this style + * @param background Background color for this style + * @param styles SGR styles to use + * @return Itself + */ + public synchronized Definition setPreLight(TextColor foreground, TextColor background, SGR... styles) { + this.preLight = new DefaultMutableThemeStyle(foreground, background, styles); + return this; + } + + @Override + public synchronized ThemeStyle getSelected() { + if (selected == null) { + return normal; + } + return selected; + } + + /** + * Sets the theme definition style "selected" + * + * @param foreground Foreground color for this style + * @param background Background color for this style + * @param styles SGR styles to use + * @return Itself + */ + public synchronized Definition setSelected(TextColor foreground, TextColor background, SGR... styles) { + this.selected = new DefaultMutableThemeStyle(foreground, background, styles); + return this; + } + + @Override + public synchronized ThemeStyle getActive() { + if (active == null) { + return normal; + } + return active; + } + + /** + * Sets the theme definition style "active" + * + * @param foreground Foreground color for this style + * @param background Background color for this style + * @param styles SGR styles to use + * @return Itself + */ + public synchronized Definition setActive(TextColor foreground, TextColor background, SGR... styles) { + this.active = new DefaultMutableThemeStyle(foreground, background, styles); + return this; + } + + @Override + public synchronized ThemeStyle getInsensitive() { + if (insensitive == null) { + return normal; + } + return insensitive; + } + + /** + * Sets the theme definition style "insensitive" + * + * @param foreground Foreground color for this style + * @param background Background color for this style + * @param styles SGR styles to use + * @return Itself + */ + public synchronized Definition setInsensitive(TextColor foreground, TextColor background, SGR... styles) { + this.insensitive = new DefaultMutableThemeStyle(foreground, background, styles); + return this; + } + + @Override + public synchronized ThemeStyle getCustom(String name) { + return customStyles.get(name); + } + + @Override + public synchronized ThemeStyle getCustom(String name, ThemeStyle defaultValue) { + ThemeStyle themeStyle = customStyles.get(name); + if (themeStyle == null) { + return defaultValue; + } + return themeStyle; + } + + /** + * Adds a custom definition style to the theme using the supplied name. This will be returned using the matching + * call to {@link Definition#getCustom(String)}. + * + * @param name Name of the custom style + * @param foreground Foreground color for this style + * @param background Background color for this style + * @param styles SGR styles to use + * @return Itself + */ + public synchronized Definition setCustom(String name, TextColor foreground, TextColor background, SGR... styles) { + customStyles.put(name, new DefaultMutableThemeStyle(foreground, background, styles)); + return this; + } + + @Override + public synchronized boolean getBooleanProperty(String name, boolean defaultValue) { + return Boolean.parseBoolean(properties.getProperty(name, Boolean.toString(defaultValue))); + } + + /** + * Attaches a boolean value property to this {@link SimpleTheme} that will be returned if calling + * {@link Definition#getBooleanProperty(String, boolean)} with the same name. + * + * @param name Name of the property + * @param value Value to attach to the property name + * @return Itself + */ + public synchronized Definition setBooleanProperty(String name, boolean value) { + properties.setProperty(name, Boolean.toString(value)); + return this; + } + + @Override + public synchronized boolean isCursorVisible() { + return cursorVisible; + } + + /** + * Sets the value that suggests if the cursor should be visible or not (it's still up to the component renderer + * if it's going to honour this or not). + * + * @param cursorVisible If {@code true} then this theme definition would like the text cursor to be displayed, + * {@code false} if not. + * @return Itself + */ + public synchronized Definition setCursorVisible(boolean cursorVisible) { + this.cursorVisible = cursorVisible; + return this; + } + + @Override + public synchronized char getCharacter(String name, char fallback) { + Character character = characterMap.get(name); + if (character == null) { + return fallback; + } + return character; + } + + /** + * Stores a character value in this definition under a specific name. This is used to customize the appearance + * of certain components. It is returned with call to {@link Definition#getCharacter(String, char)} with the + * same name. + * + * @param name Symbolic name for the character + * @param character Character to attach to the symbolic name + * @return Itself + */ + public synchronized Definition setCharacter(String name, char character) { + characterMap.put(name, character); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public synchronized ComponentRenderer getRenderer(Class type) { + RendererProvider rendererProvider = (RendererProvider) componentRendererMap.get(type); + if (rendererProvider == null) { + return null; + } + return rendererProvider.getRenderer(type); + } + + /** + * Registered a callback to get a custom {@link ComponentRenderer} for a particular class. Use this to make a + * certain component (built-in or external) to use a custom renderer. + * + * @param type Class for which to invoke the callback and return the {@link ComponentRenderer} + * @param rendererProvider Callback to invoke when getting a {@link ComponentRenderer} + * @param Type of class + * @return Itself + */ + public synchronized Definition setRenderer(Class type, RendererProvider rendererProvider) { + if (rendererProvider == null) { + componentRendererMap.remove(type); + } else { + componentRendererMap.put(type, rendererProvider); + } + return this; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/StyleSet.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/StyleSet.java new file mode 100644 index 0000000000000000000000000000000000000000..eedc8fe2dca94760cfbfb6021548f3af542e005d --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/StyleSet.java @@ -0,0 +1,161 @@ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TextColor; + +import java.util.Arrays; +import java.util.EnumSet; + +public interface StyleSet> { + + /** + * Returns the current background color + * + * @return Current background color + */ + TextColor getBackgroundColor(); + + /** + * Updates the current background color + * + * @param backgroundColor New background color + * @return Itself + */ + T setBackgroundColor(TextColor backgroundColor); + + /** + * Returns the current foreground color + * + * @return Current foreground color + */ + TextColor getForegroundColor(); + + /** + * Updates the current foreground color + * + * @param foregroundColor New foreground color + * @return Itself + */ + T setForegroundColor(TextColor foregroundColor); + + /** + * Adds zero or more modifiers to the set of currently active modifiers + * + * @param modifiers Modifiers to add to the set of currently active modifiers + * @return Itself + */ + T enableModifiers(SGR... modifiers); + + /** + * Removes zero or more modifiers from the set of currently active modifiers + * + * @param modifiers Modifiers to remove from the set of currently active modifiers + * @return Itself + */ + T disableModifiers(SGR... modifiers); + + /** + * Sets the active modifiers to exactly the set passed in to this method. Any previous state of which modifiers are + * enabled doesn't matter. + * + * @param modifiers Modifiers to set as active + * @return Itself + */ + T setModifiers(EnumSet modifiers); + + /** + * Removes all active modifiers + * + * @return Itself + */ + T clearModifiers(); + + /** + * Returns all the SGR codes that are currently active + * + * @return Currently active SGR modifiers + */ + EnumSet getActiveModifiers(); + + /** + * copy colors and set of SGR codes + * + * @param source Modifiers to set as active + * @return Itself + */ + T setStyleFrom(StyleSet source); + + + class Set implements StyleSet { + private TextColor foregroundColor; + private TextColor backgroundColor; + private final EnumSet style = EnumSet.noneOf(SGR.class); + + public Set() { + } + + public Set(StyleSet source) { + setStyleFrom(source); + } + + @Override + public TextColor getBackgroundColor() { + return backgroundColor; + } + + @Override + public Set setBackgroundColor(TextColor backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + @Override + public TextColor getForegroundColor() { + return foregroundColor; + } + + @Override + public Set setForegroundColor(TextColor foregroundColor) { + this.foregroundColor = foregroundColor; + return this; + } + + @Override + public Set enableModifiers(SGR... modifiers) { + style.addAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public Set disableModifiers(SGR... modifiers) { + style.removeAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public Set setModifiers(EnumSet modifiers) { + style.clear(); + style.addAll(modifiers); + return this; + } + + @Override + public Set clearModifiers() { + style.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return EnumSet.copyOf(style); + } + + @Override + public Set setStyleFrom(StyleSet source) { + setBackgroundColor(source.getBackgroundColor()); + setForegroundColor(source.getForegroundColor()); + setModifiers(source.getActiveModifiers()); + return this; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SubTextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SubTextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..1af96edf73fb56f5096da1377823f45812ff835f --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SubTextGraphics.java @@ -0,0 +1,68 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +/** + * This implementation of TextGraphics will take a 'proper' object and composite a view on top of it, by using a + * top-left position and a size. Any attempts to put text outside of this area will be dropped. + * + * @author Martin + */ +class SubTextGraphics extends AbstractTextGraphics { + private final TextGraphics underlyingTextGraphics; + private final TerminalPosition topLeft; + private final TerminalSize writableAreaSize; + + SubTextGraphics(TextGraphics underlyingTextGraphics, TerminalPosition topLeft, TerminalSize writableAreaSize) { + this.underlyingTextGraphics = underlyingTextGraphics; + this.topLeft = topLeft; + this.writableAreaSize = writableAreaSize; + } + + private TerminalPosition project(int column, int row) { + return topLeft.withRelative(column, row); + } + + @Override + public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { + TerminalSize writableArea = getSize(); + if (columnIndex < 0 || columnIndex >= writableArea.getColumns() || + rowIndex < 0 || rowIndex >= writableArea.getRows()) { + return this; + } + TerminalPosition projectedPosition = project(columnIndex, rowIndex); + underlyingTextGraphics.setCharacter(projectedPosition, textCharacter); + return this; + } + + @Override + public TerminalSize getSize() { + return writableAreaSize; + } + + @Override + public TextCharacter getCharacter(int column, int row) { + TerminalPosition projectedPosition = project(column, row); + return underlyingTextGraphics.getCharacter(projectedPosition.getColumn(), projectedPosition.getRow()); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..2d7a9b52fb87b4896f5650120f3d8fc7d4c6a5be --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphics.java @@ -0,0 +1,472 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.screen.TabBehaviour; + +import java.util.Collection; + +/** + * This interface exposes functionality to 'draw' text graphics on a section of the terminal. It has several + * implementation for the different levels, including one for Terminal, one for Screen and one which is used by the + * TextGUI system to draw components. They are all very similar and has a lot of graphics functionality in + * AbstractTextGraphics. + *

+ * The basic concept behind a TextGraphics implementation is that it keeps a state on four things: + *

    + *
  • Foreground color
  • + *
  • Background color
  • + *
  • Modifiers
  • + *
  • Tab-expanding behaviour
  • + *
+ * These call all be altered through ordinary set* methods, but some will be altered as the result of performing one of + * the 'drawing' operations. See the documentation to each method for further information (for example, putString). + *

+ * Don't hold on to your TextGraphics objects for too long; ideally create them and let them be GC:ed when you are done + * with them. The reason is that not all implementations will handle the underlying terminal changing size. + * + * @author Martin + */ +public interface TextGraphics extends StyleSet { + /** + * Returns the size of the area that this text graphic can write to. Any attempts of placing characters outside of + * this area will be silently ignored. + * + * @return Size of the writable area that this TextGraphics can write too + */ + TerminalSize getSize(); + + /** + * Creates a new TextGraphics of the same type as this one, using the same underlying subsystem. Using this method, + * you need to specify a section of the current TextGraphics valid area that this new TextGraphic shall be + * restricted to. If you call newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, textGraphics.getSize()) + * then the resulting object will be identical to this one, but having a separated state for colors, position and + * modifiers. + * + * @param topLeftCorner Position of this TextGraphics's writable area that is to become the top-left corner (0x0) of + * the new TextGraphics + * @param size How large area, counted from the topLeftCorner, the new TextGraphics can write to. This cannot be + * larger than the current TextGraphics's writable area (adjusted by topLeftCorner) + * @return A new TextGraphics with the same underlying subsystem, that can write to only the specified area + * @throws java.lang.IllegalArgumentException If the size the of new TextGraphics exceeds the dimensions of this + * TextGraphics in any way. + */ + TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException; + + /** + * Retrieves the current tab behaviour, which is what the TextGraphics will use when expanding \t characters to + * spaces. + * + * @return Current behaviour in use for expanding tab to spaces + */ + TabBehaviour getTabBehaviour(); + + /** + * Sets the behaviour to use when expanding tab characters (\t) to spaces + * + * @param tabBehaviour Behaviour to use when expanding tabs to spaces + * @return Itself + */ + TextGraphics setTabBehaviour(TabBehaviour tabBehaviour); + + /** + * Fills the entire writable area with a single character, using current foreground color, background color and modifiers. + * + * @param c Character to fill the writable area with + * @return Itself + */ + TextGraphics fill(char c); + + /** + * Sets the character at the current position to the specified value + * + * @param column column of the location to set the character + * @param row row of the location to set the character + * @param character Character to set at the current position + * @return Itself + */ + TextGraphics setCharacter(int column, int row, char character); + + /** + * Sets the character at the current position to the specified value, without using the current colors and modifiers + * of this TextGraphics. + * + * @param column column of the location to set the character + * @param row row of the location to set the character + * @param character Character data to set at the current position + * @return Itself + */ + TextGraphics setCharacter(int column, int row, TextCharacter character); + + /** + * Sets the character at the current position to the specified value + * + * @param position position of the location to set the character + * @param character Character to set at the current position + * @return Itself + */ + TextGraphics setCharacter(TerminalPosition position, char character); + + /** + * Sets the character at the current position to the specified value, without using the current colors and modifiers + * of this TextGraphics. + * + * @param position position of the location to set the character + * @param character Character data to set at the current position + * @return Itself + */ + TextGraphics setCharacter(TerminalPosition position, TextCharacter character); + + /** + * Draws a line from a specified position to a specified position, using a supplied character. The current + * foreground color, background color and modifiers will be applied. + * + * @param fromPoint From where to draw the line + * @param toPoint Where to draw the line + * @param character Character to use for the line + * @return Itself + */ + TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character); + + /** + * Draws a line from a specified position to a specified position, using a supplied TextCharacter. The current + * foreground color, background color and modifiers of this TextGraphics will not be used and will not be modified + * by this call. + * + * @param fromPoint From where to draw the line + * @param toPoint Where to draw the line + * @param character Character data to use for the line, including character, colors and modifiers + * @return Itself + */ + TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character); + + /** + * Draws a line from a specified position to a specified position, using a supplied character. The current + * foreground color, background color and modifiers will be applied. + * + * @param fromX Column of the starting position to draw the line from (inclusive) + * @param fromY Row of the starting position to draw the line from (inclusive) + * @param toX Column of the end position to draw the line to (inclusive) + * @param toY Row of the end position to draw the line to (inclusive) + * @param character Character to use for the line + * @return Itself + */ + TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character); + + /** + * Draws a line from a specified position to a specified position, using a supplied character. The current + * foreground color, background color and modifiers of this TextGraphics will not be used and will not be modified + * by this call. + * + * @param fromX Column of the starting position to draw the line from (inclusive) + * @param fromY Row of the starting position to draw the line from (inclusive) + * @param toX Column of the end position to draw the line to (inclusive) + * @param toY Row of the end position to draw the line to (inclusive) + * @param character Character data to use for the line, including character, colors and modifiers + * @return Itself + */ + TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character); + + /** + * Draws the outline of a triangle on the screen, using a supplied character. The triangle will begin at p1, go + * through p2 and then p3 and then back to p1. The current foreground color, background color and modifiers will be + * applied. + * + * @param p1 First point on the screen of the triangle + * @param p2 Second point on the screen of the triangle + * @param p3 Third point on the screen of the triangle + * @param character What character to use when drawing the lines of the triangle + * @return Itself + */ + TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character); + + /** + * Draws the outline of a triangle on the screen, using a supplied character. The triangle will begin at p1, go + * through p2 and then p3 and then back to p1. The current foreground color, background color and modifiers of this + * TextGraphics will not be used and will not be modified by this call. + * + * @param p1 First point on the screen of the triangle + * @param p2 Second point on the screen of the triangle + * @param p3 Third point on the screen of the triangle + * @param character What character data to use when drawing the lines of the triangle + * @return Itself + */ + TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + + /** + * Draws a filled triangle, using a supplied character. The triangle will begin at p1, go + * through p2 and then p3 and then back to p1. The current foreground color, background color and modifiers will be + * applied. + * + * @param p1 First point on the screen of the triangle + * @param p2 Second point on the screen of the triangle + * @param p3 Third point on the screen of the triangle + * @param character What character to use when drawing the triangle + * @return Itself + */ + TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character); + + /** + * Draws a filled triangle, using a supplied character. The triangle will begin at p1, go + * through p2 and then p3 and then back to p1. The current foreground color, background color and modifiers of this + * TextGraphics will not be used and will not be modified by this call. + * + * @param p1 First point on the screen of the triangle + * @param p2 Second point on the screen of the triangle + * @param p3 Third point on the screen of the triangle + * @param character What character data to use when drawing the triangle + * @return Itself + */ + TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character); + + /** + * Draws the outline of a rectangle with a particular character (and the currently active colors and + * modifiers). The topLeft coordinate is inclusive. + *

+ * For example, calling drawRectangle with size being the size of the terminal and top-left value being the terminal's + * top-left (0x0) corner will draw a border around the terminal. + *

+ * The current foreground color, background color and modifiers will be applied. + * + * @param topLeft Coordinates of the top-left position of the rectangle + * @param size Size (in columns and rows) of the area to draw + * @param character What character to use when drawing the outline of the rectangle + * @return Itself + */ + TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character); + + /** + * Draws the outline of a rectangle with a particular TextCharacter, ignoring the current colors and modifiers of + * this TextGraphics. + *

+ * For example, calling drawRectangle with size being the size of the terminal and top-left value being the terminal's + * top-left (0x0) corner will draw a border around the terminal. + *

+ * The current foreground color, background color and modifiers will not be modified by this call. + * + * @param topLeft Coordinates of the top-left position of the rectangle + * @param size Size (in columns and rows) of the area to draw + * @param character What character data to use when drawing the outline of the rectangle + * @return Itself + */ + TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); + + /** + * Takes a rectangle and fills it with a particular character (and the currently active colors and + * modifiers). The topLeft coordinate is inclusive. + *

+ * For example, calling fillRectangle with size being the size of the terminal and top-left value being the terminal's + * top-left (0x0) corner will fill the entire terminal with this character. + *

+ * The current foreground color, background color and modifiers will be applied. + * + * @param topLeft Coordinates of the top-left position of the rectangle + * @param size Size (in columns and rows) of the area to draw + * @param character What character to use when filling the rectangle + * @return Itself + */ + TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character); + + /** + * Takes a rectangle and fills it using a particular TextCharacter, ignoring the current colors and modifiers of + * this TextGraphics. The topLeft coordinate is inclusive. + *

+ * For example, calling fillRectangle with size being the size of the terminal and top-left value being the terminal's + * top-left (0x0) corner will fill the entire terminal with this character. + *

+ * The current foreground color, background color and modifiers will not be modified by this call. + * + * @param topLeft Coordinates of the top-left position of the rectangle + * @param size Size (in columns and rows) of the area to draw + * @param character What character data to use when filling the rectangle + * @return Itself + */ + TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character); + + /** + * Takes a TextImage and draws it on the surface this TextGraphics is targeting, given the coordinates on the target + * that is specifying where the top-left corner of the image should be drawn. This is equivalent of calling + * {@code drawImage(topLeft, image, TerminalPosition.TOP_LEFT_CORNER, image.getSize()}. + * + * @param topLeft Position of the top-left corner of the image on the target + * @param image Image to draw + * @return Itself + */ + TextGraphics drawImage(TerminalPosition topLeft, TextImage image); + + /** + * Takes a TextImage and draws it on the surface this TextGraphics is targeting, given the coordinates on the target + * that is specifying where the top-left corner of the image should be drawn. This overload will only draw a portion + * of the image to the target, as specified by the two last parameters. + * + * @param topLeft Position of the top-left corner of the image on the target + * @param image Image to draw + * @param sourceImageTopLeft Position of the top-left corner in the source image to draw at the topLeft position on + * the target + * @param sourceImageSize How much of the source image to draw on the target, counted from the sourceImageTopLeft + * position + * @return Itself + */ + TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! The current foreground color, background color and modifiers will be applied. + * + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @return Itself + */ + TextGraphics putString(int column, int row, String string); + + /** + * Shortcut to calling: + *

+     *  putString(position.getColumn(), position.getRow(), string);
+     * 
+ * + * @param position Position to put the string at + * @param string String to put on the screen + * @return Itself + */ + TextGraphics putString(TerminalPosition position, String string); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! If you supplied any extra modifiers, they will be applied when writing the string + * as well but not recorded into the state of the TextGraphics object. + * + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @param extraModifier Modifier to apply to the string + * @param optionalExtraModifiers Optional extra modifiers to apply to the string + * @return Itself + */ + TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers); + + /** + * Shortcut to calling: + *
+     *  putString(position.getColumn(), position.getRow(), string, modifiers, optionalExtraModifiers);
+     * 
+ * + * @param position Position to put the string at + * @param string String to put on the screen + * @param extraModifier Modifier to apply to the string + * @param optionalExtraModifiers Optional extra modifiers to apply to the string + * @return Itself + */ + TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! If you supplied any extra modifiers, they will be applied when writing the string + * as well but not recorded into the state of the TextGraphics object. + * + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @param extraModifiers Modifier to apply to the string + * @return Itself + */ + TextGraphics putString(int column, int row, String string, Collection extraModifiers); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! + *

+ * This method has an additional functionality to the regular {@link TextGraphics#putString(int, int, String)}; + * if you embed ANSI CSI-style control sequences (like modifying text color or controlling SGR status), they will be + * interpreted as the string is printed and mutates the {@link TextGraphics} object. In this version of Lanterna, + * the following sequences are supported: + *

    + *
  • Set foreground color
  • + *
  • Set background color
  • + *
  • Set/Clear bold style
  • + *
  • Set/Clear underline style
  • + *
  • Set/Clear blink style
  • + *
  • Set/Clear reverse style
  • + *
  • Clear all styles and colors (notice that this will return the state to what it was at the start of the method)
  • + *
+ * When the call is complete, the {@link TextGraphics} object will return to the color/style state it was in at the + * start of the call. + * + * @param column What column to put the string at + * @param row What row to put the string at + * @param string String to put on the screen + * @return Itself + */ + TextGraphics putCSIStyledString(int column, int row, String string); + + /** + * Puts a string on the screen at the specified position with the current colors and modifiers. If the string + * contains newlines (\r and/or \n), the method will stop at the character before that; you have to manage + * multi-line strings yourself! + *

+ * This method has an additional functionality to the regular {@link TextGraphics#putString(int, int, String)}; + * if you embed ANSI CSI-style control sequences (like modifying text color or controlling SGR status), they will be + * interpreted as the string is printed and mutates the {@link TextGraphics} object. In this version of Lanterna, + * the following sequences are supported: + *

    + *
  • Set foreground color
  • + *
  • Set background color
  • + *
  • Set/Clear bold style
  • + *
  • Set/Clear underline style
  • + *
  • Set/Clear blink style
  • + *
  • Set/Clear reverse style
  • + *
  • Clear all styles and colors (notice that this will return the state to what it was at the start of the method)
  • + *
+ * When the call is complete, the {@link TextGraphics} object will return to the color/style state it was in at the + * start of the call. + * + * @param position Position to put the string at + * @param string String to put on the screen + * @return Itself + */ + TextGraphics putCSIStyledString(TerminalPosition position, String string); + + /** + * Returns the character at the specific position in the terminal. May return {@code null} if the TextGraphics + * implementation doesn't support it or doesn't know what the character is. + * + * @param position Position to return the character for + * @return The text character at the specified position or {@code null} if not available + */ + TextCharacter getCharacter(TerminalPosition position); + + /** + * Returns the character at the specific position in the terminal. May return {@code null} if the TextGraphics + * implementation doesn't support it or doesn't know what the character is. + * + * @param column Column to return the character for + * @param row Row to return the character for + * @return The text character at the specified position or {@code null} if not available + */ + TextCharacter getCharacter(int column, int row); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphicsWriter.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphicsWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..c6ba1ba1869cb85cf9bb6cf86a73743ac27514f0 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphicsWriter.java @@ -0,0 +1,320 @@ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.*; +import com.googlecode.lanterna.screen.TabBehaviour; +import com.googlecode.lanterna.screen.WrapBehaviour; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +public class TextGraphicsWriter implements StyleSet { + private final TextGraphics backend; + private TerminalPosition cursorPosition; + private TextColor foregroundColor, backgroundColor; + private final EnumSet style = EnumSet.noneOf(SGR.class); + private WrapBehaviour wrapBehaviour = WrapBehaviour.WORD; + private boolean styleable = true; + + public TextGraphicsWriter(TextGraphics backend) { + this.backend = backend; + setStyleFrom(backend); + cursorPosition = new TerminalPosition(0, 0); + } + + public TextGraphicsWriter putString(String string) { + StringBuilder wordpart = new StringBuilder(); + StyleSet.Set originalStyle = new StyleSet.Set(backend); + backend.setStyleFrom(this); + + int wordlen = 0; // the whole column-length of the word. + for (int i = 0; i < string.length(); i++) { + char ch = string.charAt(i); + switch (ch) { + case '\n': + flush(wordpart, wordlen); + wordlen = 0; + linefeed(-1); // -1 means explicit. + break; + case '\t': + flush(wordpart, wordlen); + wordlen = 0; + if (backend.getTabBehaviour() != TabBehaviour.IGNORE) { + String repl = backend.getTabBehaviour() + .getTabReplacement(cursorPosition.getColumn()); + for (int j = 0; j < repl.length(); j++) { + backend.setCharacter(cursorPosition.withRelativeColumn(j), repl.charAt(j)); + } + cursorPosition = cursorPosition.withRelativeColumn(repl.length()); + } else { + linefeed(2); + putControlChar(ch); + } + break; + case '\033': + if (isStyleable()) { + stash(wordpart, wordlen); + String seq = TerminalTextUtils.getANSIControlSequenceAt(string, i); + TerminalTextUtils.updateModifiersFromCSICode(seq, this, originalStyle); + backend.setStyleFrom(this); + i += seq.length() - 1; + } else { + flush(wordpart, wordlen); + wordlen = 0; + linefeed(2); + putControlChar(ch); + } + break; + default: + if (Character.isISOControl(ch)) { + flush(wordpart, wordlen); + wordlen = 0; + linefeed(1); + putControlChar(ch); + } else if (Character.isWhitespace(ch)) { + flush(wordpart, wordlen); + wordlen = 0; + backend.setCharacter(cursorPosition, ch); + cursorPosition = cursorPosition.withRelativeColumn(1); + } else if (TerminalTextUtils.isCharCJK(ch)) { + flush(wordpart, wordlen); + wordlen = 0; + linefeed(2); + backend.setCharacter(cursorPosition, ch); + cursorPosition = cursorPosition.withRelativeColumn(2); + } else { + if (wrapBehaviour.keepWords()) { + // TODO: if at end of line despite starting at col 0, then split word. + wordpart.append(ch); + wordlen++; + } else { + linefeed(1); + backend.setCharacter(cursorPosition, ch); + cursorPosition = cursorPosition.withRelativeColumn(1); + } + } + } + linefeed(wordlen); + } + flush(wordpart, wordlen); + backend.setStyleFrom(originalStyle); + return this; + } + + private void linefeed(int lenToFit) { + int curCol = cursorPosition.getColumn(); + int spaceLeft = backend.getSize().getColumns() - curCol; + if (wrapBehaviour.allowLineFeed()) { + boolean wantWrap = curCol > 0 && lenToFit > spaceLeft; + if (lenToFit < 0 || (wantWrap && wrapBehaviour.autoWrap())) { + // TODO: clear to end of current line? + cursorPosition = cursorPosition.withColumn(0).withRelativeRow(1); + } + } else { + if (lenToFit < 0) { // encode explicit line feed + putControlChar('\n'); + } + } + } + + public void putControlChar(char ch) { + char subst; + switch (ch) { + case '\033': + subst = '['; + break; + case '\034': + subst = '\\'; + break; + case '\035': + subst = ']'; + break; + case '\036': + subst = '^'; + break; + case '\037': + subst = '_'; + break; + case '\177': + subst = '?'; + break; + default: + if (ch <= 26) { + subst = (char) (ch + '@'); + } else { // normal character - or 0x80-0x9F + // just write it out, anyway: + backend.setCharacter(cursorPosition, ch); + cursorPosition = cursorPosition.withRelativeColumn(1); + return; + } + } + EnumSet style = getActiveModifiers(); + if (style.contains(SGR.REVERSE)) { + style.remove(SGR.REVERSE); + } else { + style.add(SGR.REVERSE); + } + TextCharacter tc = new TextCharacter('^', + getForegroundColor(), getBackgroundColor(), style); + backend.setCharacter(cursorPosition, tc); + cursorPosition = cursorPosition.withRelativeColumn(1); + tc = tc.withCharacter(subst); + backend.setCharacter(cursorPosition, tc); + cursorPosition = cursorPosition.withRelativeColumn(1); + } + + // A word (a sequence of characters that is kept together when word-wrapping) + // may consist of differently styled parts. This class describes one such + // part. + private static class WordPart extends StyleSet.Set { + private final String word; + private final int wordlen; + + WordPart(String word, int wordlen, StyleSet style) { + this.word = word; + this.wordlen = wordlen; + setStyleFrom(style); + } + } + + private final List chunk_queue = new ArrayList<>(); + + private void stash(StringBuilder word, int wordlen) { + if (word.length() > 0) { + WordPart chunk = new WordPart(word.toString(), wordlen, this); + chunk_queue.add(chunk); + // for convenience the StringBuilder is reset: + word.setLength(0); + } + } + + private void flush(StringBuilder word, int wordlen) { + stash(word, wordlen); + if (chunk_queue.isEmpty()) { + return; + } + int row = cursorPosition.getRow(); + int col = cursorPosition.getColumn(); + int offset = 0; + for (WordPart chunk : chunk_queue) { + backend.setStyleFrom(chunk); + backend.putString(col + offset, row, chunk.word); + offset = chunk.wordlen; + } + chunk_queue.clear(); // they're done. + // set cursor right behind the word: + cursorPosition = cursorPosition.withColumn(col + offset); + backend.setStyleFrom(this); + } + + /** + * @return the cursor position + */ + public TerminalPosition getCursorPosition() { + return cursorPosition; + } + + /** + * @param cursorPosition the cursor position to set + */ + public void setCursorPosition(TerminalPosition cursorPosition) { + this.cursorPosition = cursorPosition; + } + + /** + * @return the foreground color + */ + public TextColor getForegroundColor() { + return foregroundColor; + } + + /** + * @param foreground the foreground color to set + */ + public TextGraphicsWriter setForegroundColor(TextColor foreground) { + this.foregroundColor = foreground; + return this; + } + + /** + * @return the background color + */ + public TextColor getBackgroundColor() { + return backgroundColor; + } + + /** + * @param background the background color to set + */ + public TextGraphicsWriter setBackgroundColor(TextColor background) { + this.backgroundColor = background; + return this; + } + + @Override + public TextGraphicsWriter enableModifiers(SGR... modifiers) { + style.addAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphicsWriter disableModifiers(SGR... modifiers) { + style.removeAll(Arrays.asList(modifiers)); + return this; + } + + @Override + public TextGraphicsWriter setModifiers(EnumSet modifiers) { + style.clear(); + style.addAll(modifiers); + return this; + } + + @Override + public TextGraphicsWriter clearModifiers() { + style.clear(); + return this; + } + + @Override + public EnumSet getActiveModifiers() { + return EnumSet.copyOf(style); + } + + @Override + public TextGraphicsWriter setStyleFrom(StyleSet source) { + setBackgroundColor(source.getBackgroundColor()); + setForegroundColor(source.getForegroundColor()); + setModifiers(source.getActiveModifiers()); + return this; + } + + /** + * @return the wrapBehaviour + */ + public WrapBehaviour getWrapBehaviour() { + return wrapBehaviour; + } + + /** + * @param wrapBehaviour the wrapBehaviour to set + */ + public void setWrapBehaviour(WrapBehaviour wrapBehaviour) { + this.wrapBehaviour = wrapBehaviour; + } + + /** + * @return whether styles in strings are handled. + */ + public boolean isStyleable() { + return styleable; + } + + /** + * @param styleable whether styles in strings should be handled. + */ + public void setStyleable(boolean styleable) { + this.styleable = styleable; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextImage.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextImage.java new file mode 100644 index 0000000000000000000000000000000000000000..d2a5a19b693cf9575fcb2224f37b94ff4a95c2f0 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextImage.java @@ -0,0 +1,137 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TextCharacter; + +/** + * An 'image' build up of text characters with color and style information. These are completely in memory and not + * visible anyway, but can be used when drawing with a TextGraphics objects. + * + * @author martin + */ +public interface TextImage extends Scrollable { + /** + * Returns the dimensions of this TextImage, in columns and rows + * + * @return Size of this TextImage + */ + TerminalSize getSize(); + + /** + * Returns the character stored at a particular position in this image + * + * @param position Coordinates of the character + * @return TextCharacter stored at the specified position + */ + TextCharacter getCharacterAt(TerminalPosition position); + + /** + * Returns the character stored at a particular position in this image + * + * @param column Column coordinate of the character + * @param row Row coordinate of the character + * @return TextCharacter stored at the specified position + */ + TextCharacter getCharacterAt(int column, int row); + + /** + * Sets the character at a specific position in the image to a particular TextCharacter. If the position is outside + * of the image's size, this method does nothing. + * + * @param position Coordinates of the character + * @param character What TextCharacter to assign at the specified position + */ + void setCharacterAt(TerminalPosition position, TextCharacter character); + + /** + * Sets the character at a specific position in the image to a particular TextCharacter. If the position is outside + * of the image's size, this method does nothing. + * + * @param column Column coordinate of the character + * @param row Row coordinate of the character + * @param character What TextCharacter to assign at the specified position + */ + void setCharacterAt(int column, int row, TextCharacter character); + + /** + * Sets the text image content to one specified character (including color and style) + * + * @param character The character to fill the image with + */ + void setAll(TextCharacter character); + + /** + * Creates a TextGraphics object that targets this TextImage for all its drawing operations. + * + * @return TextGraphics object for this TextImage + */ + TextGraphics newTextGraphics(); + + /** + * Returns a copy of this image resized to a new size and using a specified filler character if the new size is + * larger than the old and we need to fill in empty areas. The copy will be independent from the one this method is + * invoked on, so modifying one will not affect the other. + * + * @param newSize Size of the new image + * @param filler Filler character to use on the new areas when enlarging the image (is not used when shrinking) + * @return Copy of this image, but resized + */ + TextImage resize(TerminalSize newSize, TextCharacter filler); + + + /** + * Copies this TextImage's content to another TextImage. If the destination TextImage is larger than this + * ScreenBuffer, the areas outside of the area that is written to will be untouched. + * + * @param destination TextImage to copy to + */ + void copyTo(TextImage destination); + + /** + * Copies this TextImage's content to another TextImage. If the destination TextImage is larger than this + * TextImage, the areas outside of the area that is written to will be untouched. + * + * @param destination TextImage to copy to + * @param startRowIndex Which row in this image to copy from + * @param rows How many rows to copy + * @param startColumnIndex Which column in this image to copy from + * @param columns How many columns to copy + * @param destinationRowOffset Offset (in number of rows) in the target image where we want to first copied row to be + * @param destinationColumnOffset Offset (in number of columns) in the target image where we want to first copied column to be + */ + void copyTo( + TextImage destination, + int startRowIndex, + int rows, + int startColumnIndex, + int columns, + int destinationRowOffset, + int destinationColumnOffset); + + /** + * Scroll a range of lines of this TextImage according to given distance. + *

+ * TextImage implementations of this method do not throw IOException. + */ + @Override + void scrollLines(int firstLine, int lastLine, int distance); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Theme.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Theme.java new file mode 100644 index 0000000000000000000000000000000000000000..76b915f1d8baea88958c57263eacddcfa66b0f6d --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Theme.java @@ -0,0 +1,64 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.gui2.WindowDecorationRenderer; +import com.googlecode.lanterna.gui2.WindowPostRenderer; + +/** + * The main theme interface, from which you can retrieve theme definitions + * + * @author Martin + */ +public interface Theme { + /** + * Returns what this theme considers to be the default definition + * + * @return The default theme definition + */ + ThemeDefinition getDefaultDefinition(); + + /** + * Returns the theme definition associated with this class. The implementation of Theme should ensure that this + * call never returns {@code null}, it should always give back a valid value (falling back to the default is nothing + * else can be used). + * + * @param clazz Class to get the theme definition for + * @return The ThemeDefinition for the class passed in + */ + ThemeDefinition getDefinition(Class clazz); + + /** + * Returns a post-renderer to invoke after drawing each window, unless the GUI system or individual windows has + * their own renderers set. If {@code null}, no post-renderer will be done (unless the GUI system or the windows + * has a post-renderer). + * + * @return A {@link com.googlecode.lanterna.gui2.WindowPostRenderer} to invoke after drawing each window unless + * overridden, or {@code null} if none + */ + WindowPostRenderer getWindowPostRenderer(); + + /** + * Returns the {@link WindowDecorationRenderer} to use for windows drawn in this theme. If {@code null} then + * lanterna will fall back to use {@link com.googlecode.lanterna.gui2.DefaultWindowDecorationRenderer}. + * + * @return The decoration renderer to use for this theme, or {@code null} to use system default + */ + WindowDecorationRenderer getWindowDecorationRenderer(); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeDefinition.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..34beea03dcecc264f4d66ad6a77e4bd347d19194 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeDefinition.java @@ -0,0 +1,133 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.gui2.Component; +import com.googlecode.lanterna.gui2.ComponentRenderer; + +/** + * A ThemeDefinition contains a collection of ThemeStyle:s, which defines on a lower level which colors and SGRs to + * apply if you want to draw according to the theme. The different style names are directly inspired from GTK 2. You can + * also fetch character definitions which are stored inside of the theme, for example if you want to draw a border and + * make the characters that make up the border customizable. + * + * @author Martin + */ +public interface ThemeDefinition { + /** + * The normal style of the definition, which can be considered the default to be used. + * + * @return ThemeStyle representation for the normal style + */ + ThemeStyle getNormal(); + + /** + * The pre-light style of this definition, which can be used when a component has input focus but isn't active or + * selected, similar to mouse-hoovering in modern GUIs + * + * @return ThemeStyle representation for the pre-light style + */ + ThemeStyle getPreLight(); + + /** + * The "selected" style of this definition, which can used when a component has been actively selected in some way. + * + * @return ThemeStyle representation for the selected style + */ + ThemeStyle getSelected(); + + /** + * The "active" style of this definition, which can be used when a component is being directly interacted with + * + * @return ThemeStyle representation for the active style + */ + ThemeStyle getActive(); + + /** + * The insensitive style of this definition, which can be used when a component has been disabled or in some other + * way isn't able to be interacted with. + * + * @return ThemeStyle representation for the insensitive style + */ + ThemeStyle getInsensitive(); + + /** + * Retrieves a custom ThemeStyle, if one is available by this name. You can use this if you need more categories + * than the ones available above. + * + * @param name Name of the style to look up + * @return The ThemeStyle associated with the name + */ + ThemeStyle getCustom(String name); + + /** + * Retrieves a custom {@link ThemeStyle}, if one is available by this name. Will return a supplied default value if + * no such style could be found within this {@link ThemeDefinition}. You can use this if you need more categories + * than the ones available above. + * + * @param name Name of the style to look up + * @param defaultValue What to return if the there is no custom style by the given name + * @return The {@link ThemeStyle} associated with the name, or {@code defaultValue} if there was no such style + */ + ThemeStyle getCustom(String name, ThemeStyle defaultValue); + + /** + * Retrieves a custom boolean property, if one is available by this name. Will return a supplied default value if + * no such property could be found within this {@link ThemeDefinition}. + * + * @param name Name of the boolean property to look up + * @param defaultValue What to return if the there is no property with this name + * @return The property value stored in this theme definition, parsed as a boolean + */ + boolean getBooleanProperty(String name, boolean defaultValue); + + /** + * Asks the theme definition for this component if the theme thinks that the text cursor should be visible or not. + * Note that certain components might have a visible state depending on the context and the current data set, in + * those cases it can use {@link #getBooleanProperty(String, boolean)} to allow themes more fine-grained control + * over when cursor should be visible or not. + * + * @return A hint to the renderer as to if this theme thinks the cursor should be visible (returns {@code true}) or + * not (returns {@code false}) + */ + boolean isCursorVisible(); + + /** + * Retrieves a character from this theme definition by the specified name. This method cannot return {@code null} so + * you need to give a fallback in case the definition didn't have any character by this name. + * + * @param name Name of the character to look up + * @param fallback Character to return if there was no character by the name supplied in this definition + * @return The character from this definition by the name entered, or {@code fallback} if the definition didn't have + * any character defined with this name + */ + char getCharacter(String name, char fallback); + + /** + * Returns a {@link ComponentRenderer} attached to this definition for the specified type. Generally one theme + * definition is linked to only one component type so it wouldn't need the type parameter to figure out what to + * return. unlike the other methods of this interface, it will not traverse up in the theme hierarchy if this field + * is not defined, instead the component will use its default component renderer. + * + * @param type Component class to get the theme's renderer for + * @param Type of component + * @return Renderer to use for the {@code type} component or {@code null} to use the default + */ + ComponentRenderer getRenderer(Class type); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeStyle.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeStyle.java new file mode 100644 index 0000000000000000000000000000000000000000..7944355b1fc5c70d3989cbbb00055e968eb34396 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeStyle.java @@ -0,0 +1,55 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +import com.googlecode.lanterna.SGR; +import com.googlecode.lanterna.TextColor; + +import java.util.EnumSet; + +/** + * ThemeStyle is the lowest entry in the theme hierarchy, containing the actual colors and SGRs to use. When drawing a + * component, you would pick out a {@link ThemeDefinition} that applies to the whole component and then choose to + * activate individual {@link ThemeStyle}s when drawing the different parts of the component. + * + * @author Martin + */ +public interface ThemeStyle { + /** + * Returns the foreground color associated with this style + * + * @return foreground color associated with this style + */ + TextColor getForeground(); + + /** + * Returns the background color associated with this style + * + * @return background color associated with this style + */ + TextColor getBackground(); + + /** + * Returns the set of SGR flags associated with this style. This {@code EnumSet} is either unmodifiable or a copy so + * altering it will not change the theme in any way. + * + * @return SGR flags associated with this style + */ + EnumSet getSGRs(); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemedTextGraphics.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemedTextGraphics.java new file mode 100644 index 0000000000000000000000000000000000000000..150590a7786797279e3cb3717bf209459756d306 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemedTextGraphics.java @@ -0,0 +1,35 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.graphics; + +/** + * Expanded TextGraphics that adds methods to interact with themes + * + * @author Martin + */ +public interface ThemedTextGraphics extends TextGraphics { + /** + * Takes a ThemeStyle as applies it to this TextGraphics. This will effectively set the foreground color, the + * background color and all the SGRs. + * + * @param themeStyle ThemeStyle to apply + * @return Itself + */ + ThemedTextGraphics applyThemeStyle(ThemeStyle themeStyle); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbsoluteLayout.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbsoluteLayout.java new file mode 100644 index 0000000000000000000000000000000000000000..152b49d22fc907d99a8dea0069cae1ec6f1d625d --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbsoluteLayout.java @@ -0,0 +1,56 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalSize; + +import java.util.List; + +/** + * Layout manager that places components where they are manually specified to be and sizes them to the size they are + * manually assigned to. When using the AbsoluteLayout, please use setPosition(..) and setSize(..) manually on each + * component to choose where to place them. Components that have not had their position and size explicitly set will + * not be visible. + * + * @author martin + */ +public class AbsoluteLayout implements LayoutManager { + @Override + public TerminalSize getPreferredSize(List components) { + TerminalSize size = TerminalSize.ZERO; + for (Component component : components) { + size = size.max( + new TerminalSize( + component.getPosition().getColumn() + component.getSize().getColumns(), + component.getPosition().getRow() + component.getSize().getRows())); + + } + return size; + } + + @Override + public void doLayout(TerminalSize area, List components) { + //Do nothing + } + + @Override + public boolean hasChanged() { + return false; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java new file mode 100644 index 0000000000000000000000000000000000000000..3bddc6d9003b5930777d3006ded0cda91191c798 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java @@ -0,0 +1,509 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.gui2.Interactable.Result; +import com.googlecode.lanterna.gui2.menu.MenuBar; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import com.googlecode.lanterna.input.MouseAction; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This abstract implementation of {@code BasePane} has the common code shared by all different concrete + * implementations. + */ +public abstract class AbstractBasePane implements BasePane { + protected final ContentHolder contentHolder; + private final CopyOnWriteArrayList> listeners; + protected InteractableLookupMap interactableLookupMap; + private Interactable focusedInteractable; + private boolean invalid; + private boolean strictFocusChange; + private boolean enableDirectionBasedMovements; + private Theme theme; + + private Interactable mouseDownForDrag = null; + + protected AbstractBasePane() { + this.contentHolder = new ContentHolder(); + this.listeners = new CopyOnWriteArrayList<>(); + this.interactableLookupMap = new InteractableLookupMap(new TerminalSize(80, 25)); + this.invalid = false; + this.strictFocusChange = false; + this.enableDirectionBasedMovements = true; + this.theme = null; + } + + @Override + public boolean isInvalid() { + return invalid || contentHolder.isInvalid(); + } + + @Override + public void invalidate() { + invalid = true; + + //Propagate + contentHolder.invalidate(); + } + + @Override + public void draw(TextGUIGraphics graphics) { + graphics.applyThemeStyle(getTheme().getDefinition(Window.class).getNormal()); + graphics.fill(' '); + + if (!interactableLookupMap.getSize().equals(graphics.getSize())) { + interactableLookupMap = new InteractableLookupMap(graphics.getSize()); + } else { + interactableLookupMap.reset(); + } + + contentHolder.draw(graphics); + contentHolder.updateLookupMap(interactableLookupMap); + //interactableLookupMap.debug(); + invalid = false; + } + + @Override + public boolean handleInput(KeyStroke key) { + // Fire events first and decide if the event should be sent to the focused component or not + AtomicBoolean deliverEvent = new AtomicBoolean(true); + for (BasePaneListener listener : listeners) { + listener.onInput(self(), key, deliverEvent); + } + if (!deliverEvent.get()) { + return true; + } + + // Now try to deliver the event to the focused component + boolean handled = doHandleInput(key); + + // If it wasn't handled, fire the listeners and decide what to report to the TextGUI + if (!handled) { + AtomicBoolean hasBeenHandled = new AtomicBoolean(false); + for (BasePaneListener listener : listeners) { + listener.onUnhandledInput(self(), key, hasBeenHandled); + } + handled = hasBeenHandled.get(); + } + return handled; + } + + abstract T self(); + + private boolean doHandleInput(KeyStroke key) { + boolean result = false; + if (key.getKeyType() == KeyType.MouseEvent) { + return handleMouseInput((MouseAction) key); + } + Interactable.FocusChangeDirection direction = Interactable.FocusChangeDirection.TELEPORT; // Default + Interactable nextFocus = null; + if (focusedInteractable == null) { + // If nothing is focused and the user presses certain navigation keys, try to find if there is an + // Interactable component we can move focus to. + MenuBar menuBar = getMenuBar(); + Component baseComponent = getComponent(); + switch (key.getKeyType()) { + case Tab: + case ArrowRight: + case ArrowDown: + direction = Interactable.FocusChangeDirection.NEXT; + // First try the menu, then the actual component + nextFocus = menuBar.nextFocus(null); + if (nextFocus == null) { + if (baseComponent instanceof Container) { + nextFocus = ((Container) baseComponent).nextFocus(null); + } else if (baseComponent instanceof Interactable) { + nextFocus = (Interactable) baseComponent; + } + } + break; + + case ReverseTab: + case ArrowUp: + case ArrowLeft: + direction = Interactable.FocusChangeDirection.PREVIOUS; + if (baseComponent instanceof Container) { + nextFocus = ((Container) baseComponent).previousFocus(null); + } else if (baseComponent instanceof Interactable) { + nextFocus = (Interactable) baseComponent; + } + // If no component can take focus, try the menu + if (nextFocus == null) { + nextFocus = menuBar.previousFocus(null); + } + break; + } + if (nextFocus != null) { + setFocusedInteractable(nextFocus, direction); + result = true; + } + } else { + Interactable.Result handleResult = focusedInteractable.handleInput(key); + if (!enableDirectionBasedMovements) { + if (handleResult == Interactable.Result.MOVE_FOCUS_DOWN || handleResult == Interactable.Result.MOVE_FOCUS_RIGHT) { + handleResult = Interactable.Result.MOVE_FOCUS_NEXT; + } else if (handleResult == Interactable.Result.MOVE_FOCUS_UP || handleResult == Interactable.Result.MOVE_FOCUS_LEFT) { + handleResult = Interactable.Result.MOVE_FOCUS_PREVIOUS; + } + } + switch (handleResult) { + case HANDLED: + result = true; + break; + case UNHANDLED: + //Filter the event recursively through all parent containers until we hit null; give the containers + //a chance to absorb the event + Container parent = focusedInteractable.getParent(); + while (parent != null) { + if (parent.handleInput(key)) { + return true; + } + parent = parent.getParent(); + } + result = false; + break; + case MOVE_FOCUS_NEXT: + nextFocus = contentHolder.nextFocus(focusedInteractable); + if (nextFocus == null) { + nextFocus = contentHolder.nextFocus(null); + } + direction = Interactable.FocusChangeDirection.NEXT; + break; + case MOVE_FOCUS_PREVIOUS: + nextFocus = contentHolder.previousFocus(focusedInteractable); + if (nextFocus == null) { + nextFocus = contentHolder.previousFocus(null); + } + direction = Interactable.FocusChangeDirection.PREVIOUS; + break; + case MOVE_FOCUS_DOWN: + nextFocus = interactableLookupMap.findNextDown(focusedInteractable); + direction = Interactable.FocusChangeDirection.DOWN; + if (nextFocus == null && !strictFocusChange) { + nextFocus = contentHolder.nextFocus(focusedInteractable); + direction = Interactable.FocusChangeDirection.NEXT; + } + break; + case MOVE_FOCUS_LEFT: + nextFocus = interactableLookupMap.findNextLeft(focusedInteractable); + direction = Interactable.FocusChangeDirection.LEFT; + break; + case MOVE_FOCUS_RIGHT: + nextFocus = interactableLookupMap.findNextRight(focusedInteractable); + direction = Interactable.FocusChangeDirection.RIGHT; + break; + case MOVE_FOCUS_UP: + nextFocus = interactableLookupMap.findNextUp(focusedInteractable); + direction = Interactable.FocusChangeDirection.UP; + if (nextFocus == null && !strictFocusChange) { + nextFocus = contentHolder.previousFocus(focusedInteractable); + direction = Interactable.FocusChangeDirection.PREVIOUS; + } + break; + } + } + if (nextFocus != null) { + setFocusedInteractable(nextFocus, direction); + result = true; + } + return result; + } + + private boolean handleMouseInput(MouseAction mouseAction) { + TerminalPosition localCoordinates = fromGlobal(mouseAction.getPosition()); + if (localCoordinates == null) { + return false; + } + Interactable interactable = interactableLookupMap.getInteractableAt(localCoordinates); + if (mouseAction.isMouseDown()) { + mouseDownForDrag = interactable; + } + Interactable wasMouseDownForDrag = mouseDownForDrag; + if (mouseAction.isMouseUp()) { + mouseDownForDrag = null; + } + if (mouseAction.isMouseDrag() && mouseDownForDrag != null) { + return mouseDownForDrag.handleInput(mouseAction) == Result.HANDLED; + } + if (interactable == null) { + return false; + } + if (mouseAction.isMouseUp()) { + // MouseUp only handled by same interactable as MouseDown + if (wasMouseDownForDrag == interactable) { + return interactable.handleInput(mouseAction) == Result.HANDLED; + } + // did not handleInput because mouse up was not on component mouse down was on + return false; + } + return interactable.handleInput(mouseAction) == Result.HANDLED; + } + + @Override + public Component getComponent() { + return contentHolder.getComponent(); + } + + @Override + public void setComponent(Component component) { + contentHolder.setComponent(component); + } + + @Override + public Interactable getFocusedInteractable() { + return focusedInteractable; + } + + @Override + public TerminalPosition getCursorPosition() { + if (focusedInteractable == null) { + return null; + } + TerminalPosition position = focusedInteractable.getCursorLocation(); + if (position == null) { + return null; + } + //Don't allow the component to set the cursor outside of its own boundaries + if (position.getColumn() < 0 || + position.getRow() < 0 || + position.getColumn() >= focusedInteractable.getSize().getColumns() || + position.getRow() >= focusedInteractable.getSize().getRows()) { + return null; + } + return focusedInteractable.toBasePane(position); + } + + @Override + public void setFocusedInteractable(Interactable toFocus) { + setFocusedInteractable(toFocus, + toFocus != null ? + Interactable.FocusChangeDirection.TELEPORT : Interactable.FocusChangeDirection.RESET); + } + + protected void setFocusedInteractable(Interactable toFocus, Interactable.FocusChangeDirection direction) { + if (focusedInteractable == toFocus) { + return; + } + if (toFocus != null && !toFocus.isEnabled()) { + return; + } + if (focusedInteractable != null) { + focusedInteractable.onLeaveFocus(direction, focusedInteractable); + } + Interactable previous = focusedInteractable; + focusedInteractable = toFocus; + if (toFocus != null) { + toFocus.onEnterFocus(direction, previous); + } + invalidate(); + } + + @Override + public void setStrictFocusChange(boolean strictFocusChange) { + this.strictFocusChange = strictFocusChange; + } + + @Override + public void setEnableDirectionBasedMovements(boolean enableDirectionBasedMovements) { + this.enableDirectionBasedMovements = enableDirectionBasedMovements; + } + + @Override + public synchronized Theme getTheme() { + if (theme != null) { + return theme; + } else if (getTextGUI() != null) { + return getTextGUI().getTheme(); + } + return null; + } + + @Override + public synchronized void setTheme(Theme theme) { + this.theme = theme; + invalidate(); + } + + @Override + public MenuBar getMenuBar() { + return contentHolder.getMenuBar(); + } + + @Override + public void setMenuBar(MenuBar menuBar) { + contentHolder.setMenuBar(menuBar); + } + + protected void addBasePaneListener(BasePaneListener basePaneListener) { + listeners.addIfAbsent(basePaneListener); + } + + protected void removeBasePaneListener(BasePaneListener basePaneListener) { + listeners.remove(basePaneListener); + } + + protected List> getBasePaneListeners() { + return listeners; + } + + protected class ContentHolder extends AbstractComposite { + private MenuBar menuBar; + + ContentHolder() { + this.menuBar = new EmptyMenuBar(); + } + + private void setMenuBar(MenuBar menuBar) { + if (menuBar == null) { + menuBar = new EmptyMenuBar(); + } + + if (this.menuBar != menuBar) { + menuBar.onAdded(this); + this.menuBar.onRemoved(this); + this.menuBar = menuBar; + if (focusedInteractable == null) { + setFocusedInteractable(menuBar.nextFocus(null)); + } + invalidate(); + } + } + + private MenuBar getMenuBar() { + return menuBar; + } + + @Override + public boolean isInvalid() { + return super.isInvalid() || menuBar.isInvalid(); + } + + @Override + public void invalidate() { + super.invalidate(); + menuBar.invalidate(); + } + + @Override + public void updateLookupMap(InteractableLookupMap interactableLookupMap) { + super.updateLookupMap(interactableLookupMap); + menuBar.updateLookupMap(interactableLookupMap); + } + + @Override + public void setComponent(Component component) { + if (getComponent() == component) { + return; + } + setFocusedInteractable(null); + super.setComponent(component); + if (focusedInteractable == null && component instanceof Interactable) { + setFocusedInteractable((Interactable) component); + } else if (focusedInteractable == null && component instanceof Container) { + setFocusedInteractable(((Container) component).nextFocus(null)); + } + } + + public boolean removeComponent(Component component) { + boolean removed = super.removeComponent(component); + if (removed) { + focusedInteractable = null; + } + return removed; + } + + @Override + public TextGUI getTextGUI() { + return AbstractBasePane.this.getTextGUI(); + } + + @Override + protected ComponentRenderer createDefaultRenderer() { + return new ComponentRenderer() { + @Override + public TerminalSize getPreferredSize(Container component) { + Component subComponent = getComponent(); + if (subComponent == null) { + return TerminalSize.ZERO; + } + return subComponent.getPreferredSize(); + } + + @Override + public void drawComponent(TextGUIGraphics graphics, Container component) { + if (!(menuBar instanceof EmptyMenuBar)) { + int menuBarHeight = menuBar.getPreferredSize().getRows(); + TextGUIGraphics menuGraphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, graphics.getSize().withRows(menuBarHeight)); + menuBar.draw(menuGraphics); + graphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER.withRelativeRow(menuBarHeight), graphics.getSize().withRelativeRows(-menuBarHeight)); + } + + Component subComponent = getComponent(); + if (subComponent == null) { + return; + } + subComponent.draw(graphics); + } + }; + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + return AbstractBasePane.this.toGlobal(position); + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + return position; + } + + @Override + public BasePane getBasePane() { + return AbstractBasePane.this; + } + } + + private static class EmptyMenuBar extends MenuBar { + @Override + public boolean isInvalid() { + return false; + } + + @Override + public synchronized void onAdded(Container container) { + } + + @Override + public synchronized void onRemoved(Container container) { + } + + @Override + public boolean isEmptyMenuBar() { + return true; + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBorder.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBorder.java new file mode 100644 index 0000000000000000000000000000000000000000..5d016ca5e5755738414f69fd523409bcda8a60cb --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBorder.java @@ -0,0 +1,80 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; + +/** + * Abstract implementation of {@code Border} interface that has some of the methods filled out. If you want to create + * your own {@code Border} implementation, should should probably extend from this. + * + * @author Martin + */ +public abstract class AbstractBorder extends AbstractComposite implements Border { + @Override + public void setComponent(Component component) { + super.setComponent(component); + if (component != null) { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER); + } + } + + @Override + public BorderRenderer getRenderer() { + return (BorderRenderer) super.getRenderer(); + } + + @Override + public Border setSize(TerminalSize size) { + super.setSize(size); + getComponent().setSize(getWrappedComponentSize(size)); + return self(); + } + + @Override + public LayoutData getLayoutData() { + return getComponent().getLayoutData(); + } + + @Override + public Border setLayoutData(LayoutData ld) { + getComponent().setLayoutData(ld); + return this; + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + return super.toBasePane(position).withRelative(getWrappedComponentTopLeftOffset()); + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + return super.toGlobal(position).withRelative(getWrappedComponentTopLeftOffset()); + } + + private TerminalPosition getWrappedComponentTopLeftOffset() { + return getRenderer().getWrappedComponentTopLeftOffset(); + } + + private TerminalSize getWrappedComponentSize(TerminalSize borderSize) { + return getRenderer().getWrappedComponentSize(borderSize); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComponent.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..fa1518e052e9c9f3f19d287b3c2f3c75a2ef1916 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComponent.java @@ -0,0 +1,428 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.bundle.LanternaThemes; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.graphics.ThemeDefinition; + +/** + * AbstractComponent provides some good default behaviour for a {@code Component}, all components in Lanterna extends + * from this class in some way. If you want to write your own component that isn't interactable or theme:able, you + * probably want to extend from this class. + *

+ * The way you want to declare your new {@code Component} is to pass in itself as the generic parameter, like this: + *

+ * {@code
+ *     public class MyComponent extends AbstractComponent {
+ *         ...
+ *     }
+ * }
+ * 
+ * This was, the component renderer will be correctly setup type-wise and you will need to do fewer typecastings when + * you implement the drawing method your new component. + * + * @param Should always be itself, this value will be used for the {@code ComponentRenderer} declaration + * @author Martin + */ +public abstract class AbstractComponent implements Component { + /** + * Manually set renderer + */ + private ComponentRenderer overrideRenderer; + /** + * If overrideRenderer is not set, this is used instead if not null, set by the theme + */ + private ComponentRenderer themeRenderer; + + /** + * To keep track of the theme that created the themeRenderer, so we can reset it if the theme changes + */ + private Theme themeRenderersTheme; + + /** + * If the theme had nothing for this component and no override is set, this is the third fallback + */ + private ComponentRenderer defaultRenderer; + + private Container parent; + private TerminalSize size; + private TerminalSize explicitPreferredSize; //This is keeping the value set by the user (if setPreferredSize() is used) + private TerminalPosition position; + private Theme themeOverride; + private LayoutData layoutData; + private boolean visible; + private boolean invalid; + + /** + * Default constructor + */ + public AbstractComponent() { + size = TerminalSize.ZERO; + position = TerminalPosition.TOP_LEFT_CORNER; + explicitPreferredSize = null; + layoutData = null; + visible = true; + invalid = true; + parent = null; + overrideRenderer = null; + themeRenderer = null; + themeRenderersTheme = null; + defaultRenderer = null; + } + + /** + * When you create a custom component, you need to implement this method and return a Renderer which is responsible + * for taking care of sizing the component, rendering it and choosing where to place the cursor (if Interactable). + * This value is intended to be overridden by custom themes. + * + * @return Renderer to use when sizing and drawing this component + */ + protected abstract ComponentRenderer createDefaultRenderer(); + + /** + * Takes a {@code Runnable} and immediately executes it if this is called on the designated GUI thread, otherwise + * schedules it for later invocation. + * + * @param runnable {@code Runnable} to execute on the GUI thread + */ + protected void runOnGUIThreadIfExistsOtherwiseRunDirect(Runnable runnable) { + if (getTextGUI() != null && getTextGUI().getGUIThread() != null) { + getTextGUI().getGUIThread().invokeLater(runnable); + } else { + runnable.run(); + } + } + + /** + * Explicitly sets the {@code ComponentRenderer} to be used when drawing this component. This will override whatever + * the current theme is suggesting or what the default renderer is. If you call this with {@code null}, the override + * is cleared. + * + * @param renderer {@code ComponentRenderer} to be used when drawing this component + * @return Itself + */ + public T setRenderer(ComponentRenderer renderer) { + this.overrideRenderer = renderer; + return self(); + } + + @Override + public synchronized ComponentRenderer getRenderer() { + // First try the override + if (overrideRenderer != null) { + return overrideRenderer; + } + + // Then try to create and return a renderer from the theme + Theme currentTheme = getTheme(); + if ((themeRenderer == null && getBasePane() != null) || + // Check if the theme has changed + themeRenderer != null && currentTheme != themeRenderersTheme) { + + themeRenderer = currentTheme.getDefinition(getClass()).getRenderer(selfClass()); + if (themeRenderer != null) { + themeRenderersTheme = currentTheme; + } + } + if (themeRenderer != null) { + return themeRenderer; + } + + // Finally, fallback to the default renderer + if (defaultRenderer == null) { + defaultRenderer = createDefaultRenderer(); + if (defaultRenderer == null) { + throw new IllegalStateException(getClass() + " returned a null default renderer"); + } + } + return defaultRenderer; + } + + @Override + public void invalidate() { + invalid = true; + } + + @Override + public synchronized T setSize(TerminalSize size) { + this.size = size; + return self(); + } + + @Override + public TerminalSize getSize() { + return size; + } + + @Override + public final TerminalSize getPreferredSize() { + if (explicitPreferredSize != null) { + return explicitPreferredSize; + } else { + return calculatePreferredSize(); + } + } + + @Override + public final synchronized T setPreferredSize(TerminalSize explicitPreferredSize) { + this.explicitPreferredSize = explicitPreferredSize; + return self(); + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public T setVisible(boolean visible) { + if (this.visible != visible) { + this.visible = visible; + if (visible) { + // This component is now visible, so mark it as invalid so it will be redrawn + invalidate(); + } else { + Container parent = getParent(); + if (parent != null) { + // This component is now invisible, so mark the parent container as needing to be redrawn + parent.invalidate(); + } + } + } + return self(); + } + + /** + * Invokes the component renderer's size calculation logic and returns the result. This value represents the + * preferred size and isn't necessarily what it will eventually be assigned later on. + * + * @return Size that the component renderer believes the component should be + */ + protected synchronized TerminalSize calculatePreferredSize() { + return getRenderer().getPreferredSize(self()); + } + + @Override + public synchronized T setPosition(TerminalPosition position) { + this.position = position; + return self(); + } + + @Override + public TerminalPosition getPosition() { + return position; + } + + @Override + public TerminalPosition getGlobalPosition() { + return toGlobal(TerminalPosition.TOP_LEFT_CORNER); + } + + @Override + public boolean isInvalid() { + return invalid; + } + + @Override + public final synchronized void draw(final TextGUIGraphics graphics) { + //Delegate drawing the component to the renderer + setSize(graphics.getSize()); + onBeforeDrawing(); + getRenderer().drawComponent(graphics, self()); + onAfterDrawing(graphics); + invalid = false; + } + + /** + * This method is called just before the component's renderer is invoked for the drawing operation. You can use this + * hook to do some last-minute adjustments to the component, as an alternative to coding it into the renderer + * itself. The component should have the correct size and position at this point, if you call {@code getSize()} and + * {@code getPosition()}. + */ + protected void onBeforeDrawing() { + //No operation by default + } + + /** + * This method is called immediately after the component's renderer has finished the drawing operation. You can use + * this hook to do some post-processing if you need, as an alternative to coding it into the renderer. The + * {@code TextGUIGraphics} supplied is the same that was fed into the renderer. + * + * @param graphics Graphics object you can use to manipulate the appearance of the component + */ + @SuppressWarnings("EmptyMethod") + protected void onAfterDrawing(TextGUIGraphics graphics) { + //No operation by default + } + + @Override + public synchronized T setLayoutData(LayoutData data) { + if (layoutData != data) { + layoutData = data; + invalidate(); + } + return self(); + } + + @Override + public LayoutData getLayoutData() { + return layoutData; + } + + @Override + public Container getParent() { + return parent; + } + + @Override + public boolean hasParent(Container parent) { + if (this.parent == null) { + return false; + } + Container recursiveParent = this.parent; + while (recursiveParent != null) { + if (recursiveParent == parent) { + return true; + } + recursiveParent = recursiveParent.getParent(); + } + return false; + } + + @Override + public TextGUI getTextGUI() { + if (parent == null) { + return null; + } + return parent.getTextGUI(); + } + + @Override + public synchronized Theme getTheme() { + if (themeOverride != null) { + return themeOverride; + } else if (parent != null) { + return parent.getTheme(); + } else if (getBasePane() != null) { + return getBasePane().getTheme(); + } else { + return LanternaThemes.getDefaultTheme(); + } + } + + @Override + public ThemeDefinition getThemeDefinition() { + return getTheme().getDefinition(getClass()); + } + + @Override + public synchronized Component setTheme(Theme theme) { + themeOverride = theme; + invalidate(); + return this; + } + + @Override + public boolean isInside(Container container) { + Component test = this; + while (test.getParent() != null) { + if (test.getParent() == container) { + return true; + } + test = test.getParent(); + } + return false; + } + + @Override + public BasePane getBasePane() { + if (parent == null) { + return null; + } + return parent.getBasePane(); + } + + @Override + public TerminalPosition toBasePane(TerminalPosition position) { + Container parent = getParent(); + if (parent == null) { + return null; + } + return parent.toBasePane(getPosition().withRelative(position)); + } + + @Override + public TerminalPosition toGlobal(TerminalPosition position) { + Container parent = getParent(); + if (parent == null) { + return null; + } + return parent.toGlobal(getPosition().withRelative(position)); + } + + @Override + public synchronized Border withBorder(Border border) { + border.setComponent(this); + return border; + } + + @Override + public synchronized T addTo(Panel panel) { + panel.addComponent(this); + return self(); + } + + @Override + public synchronized void onAdded(Container container) { + if (parent != container && parent != null) { + // first inform current parent: + parent.removeComponent(this); + } + parent = container; + } + + @Override + public synchronized void onRemoved(Container container) { + if (parent == container) { + parent = null; + themeRenderer = null; + } else { + throw new IllegalStateException(this + " is not " + container + "'s child."); + } + } + + /** + * This is a little hack to avoid doing typecasts all over the place when having to return {@code T}. Credit to + * avl42 for this one! + * + * @return Itself, but as type T + */ + @SuppressWarnings("unchecked") + protected T self() { + return (T) this; + } + + @SuppressWarnings("unchecked") + private Class selfClass() { + return (Class) getClass(); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComposite.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComposite.java new file mode 100644 index 0000000000000000000000000000000000000000..33dfe0df732a9dc59f82f299448e79744c60e955 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComposite.java @@ -0,0 +1,167 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.gui2.menu.MenuBar; +import com.googlecode.lanterna.input.KeyStroke; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * This abstract implementation contains common code for the different {@code Composite} implementations. A + * {@code Composite} component is one that encapsulates a single component, like borders. Because of this, a + * {@code Composite} can be seen as a special case of a {@code Container} and indeed this abstract class does in fact + * implement the {@code Container} interface as well, to make the composites easier to work with internally. + * + * @param Should always be itself, see {@code AbstractComponent} + * @author martin + */ +public abstract class AbstractComposite extends AbstractComponent implements Composite, Container { + + private Component component; + + /** + * Default constructor + */ + public AbstractComposite() { + component = null; + } + + @Override + public void setComponent(Component component) { + Component oldComponent = this.component; + if (oldComponent == component) { + return; + } + if (oldComponent != null) { + removeComponent(oldComponent); + } + if (component != null) { + this.component = component; + component.onAdded(this); + if (getBasePane() != null) { + MenuBar menuBar = getBasePane().getMenuBar(); + if (menuBar == null || menuBar.isEmptyMenuBar()) { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER); + } else { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER.withRelativeRow(1)); + } + } + invalidate(); + } + } + + @Override + public Component getComponent() { + return component; + } + + @Override + public int getChildCount() { + return component != null ? 1 : 0; + } + + @Override + public List getChildrenList() { + if (component != null) { + return Collections.singletonList(component); + } else { + return Collections.emptyList(); + } + } + + @Override + public Collection getChildren() { + return getChildrenList(); + } + + @Override + public boolean containsComponent(Component component) { + return component != null && component.hasParent(this); + } + + @Override + public boolean removeComponent(Component component) { + if (this.component == component) { + this.component = null; + component.onRemoved(this); + invalidate(); + return true; + } + return false; + } + + @Override + public boolean isInvalid() { + return component != null && component.isInvalid(); + } + + @Override + public void invalidate() { + super.invalidate(); + + //Propagate + if (component != null) { + component.invalidate(); + } + } + + @Override + public Interactable nextFocus(Interactable fromThis) { + if (fromThis == null && getComponent() instanceof Interactable) { + Interactable interactable = (Interactable) getComponent(); + if (interactable.isEnabled()) { + return interactable; + } + } else if (getComponent() instanceof Container) { + return ((Container) getComponent()).nextFocus(fromThis); + } + return null; + } + + @Override + public Interactable previousFocus(Interactable fromThis) { + if (fromThis == null && getComponent() instanceof Interactable) { + Interactable interactable = (Interactable) getComponent(); + if (interactable.isEnabled()) { + return interactable; + } + } else if (getComponent() instanceof Container) { + return ((Container) getComponent()).previousFocus(fromThis); + } + return null; + } + + @Override + public boolean handleInput(KeyStroke key) { + return false; + } + + @Override + public void updateLookupMap(InteractableLookupMap interactableLookupMap) { + if (getComponent() instanceof Container) { + ((Container) getComponent()).updateLookupMap(interactableLookupMap); + } else if (getComponent() instanceof Interactable) { + interactableLookupMap.add((Interactable) getComponent()); + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..49a10e3363c53606e105ee361614068864eecd43 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java @@ -0,0 +1,248 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import com.googlecode.lanterna.input.MouseAction; +import com.googlecode.lanterna.input.MouseActionType; + +/** + * Default implementation of Interactable that extends from AbstractComponent. If you want to write your own component + * that is interactable, i.e. can receive keyboard (and mouse) input, you probably want to extend from this class as + * it contains some common implementations of the methods from {@code Interactable} interface + * + * @param Should always be itself, see {@code AbstractComponent} + * @author Martin + */ +public abstract class AbstractInteractableComponent> extends AbstractComponent implements Interactable { + + private InputFilter inputFilter; + private boolean inFocus; + private boolean enabled; + + /** + * Default constructor + */ + protected AbstractInteractableComponent() { + inputFilter = null; + inFocus = false; + enabled = true; + } + + @Override + public T takeFocus() { + if (!isEnabled()) { + return self(); + } + BasePane basePane = getBasePane(); + if (basePane != null) { + basePane.setFocusedInteractable(this); + } + return self(); + } + + /** + * {@inheritDoc} + *

+ * This method is final in {@code AbstractInteractableComponent}, please override {@code afterEnterFocus} instead + */ + @Override + public final void onEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) { + inFocus = true; + afterEnterFocus(direction, previouslyInFocus); + } + + /** + * Called by {@code AbstractInteractableComponent} automatically after this component has received input focus. You + * can override this method if you need to trigger some action based on this. + * + * @param direction How focus was transferred, keep in mind this is from the previous component's point of view so + * if this parameter has value DOWN, focus came in from above + * @param previouslyInFocus Which interactable component had focus previously + */ + @SuppressWarnings("EmptyMethod") + protected void afterEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) { + //By default no action + } + + /** + * {@inheritDoc} + *

+ * This method is final in {@code AbstractInteractableComponent}, please override {@code afterLeaveFocus} instead + */ + @Override + public final void onLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus) { + inFocus = false; + afterLeaveFocus(direction, nextInFocus); + } + + /** + * Called by {@code AbstractInteractableComponent} automatically after this component has lost input focus. You + * can override this method if you need to trigger some action based on this. + * + * @param direction How focus was transferred, keep in mind this is from the this component's point of view so + * if this parameter has value DOWN, focus is moving down to a component below + * @param nextInFocus Which interactable component is going to receive focus + */ + @SuppressWarnings("EmptyMethod") + protected void afterLeaveFocus(FocusChangeDirection direction, Interactable nextInFocus) { + //By default no action + } + + @Override + protected abstract InteractableRenderer createDefaultRenderer(); + + @Override + public InteractableRenderer getRenderer() { + return (InteractableRenderer) super.getRenderer(); + } + + @Override + public boolean isFocused() { + return inFocus; + } + + @Override + public synchronized T setEnabled(boolean enabled) { + this.enabled = enabled; + if (!enabled && isFocused()) { + BasePane basePane = getBasePane(); + if (basePane != null) { + basePane.setFocusedInteractable(null); + } + } + return self(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean isFocusable() { + return true; + } + + @Override + public final synchronized Result handleInput(KeyStroke keyStroke) { + if (inputFilter == null || inputFilter.onInput(this, keyStroke)) { + return handleKeyStroke(keyStroke); + } else { + return Result.UNHANDLED; + } + } + + /** + * This method can be overridden to handle various user input (mostly from the keyboard) when this component is in + * focus. The input method from the interface, {@code handleInput(..)} is final in + * {@code AbstractInteractableComponent} to ensure the input filter is properly handled. If the filter decides that + * this event should be processed, it will call this method. + * + * @param keyStroke What input was entered by the user + * @return Result of processing the key-stroke + */ + protected Result handleKeyStroke(KeyStroke keyStroke) { + // Skip the keystroke if ctrl, alt or shift was down + if (!keyStroke.isAltDown() && !keyStroke.isCtrlDown() && !keyStroke.isShiftDown()) { + switch (keyStroke.getKeyType()) { + case ArrowDown: + return Result.MOVE_FOCUS_DOWN; + case ArrowLeft: + return Result.MOVE_FOCUS_LEFT; + case ArrowRight: + return Result.MOVE_FOCUS_RIGHT; + case ArrowUp: + return Result.MOVE_FOCUS_UP; + case Tab: + return Result.MOVE_FOCUS_NEXT; + case ReverseTab: + return Result.MOVE_FOCUS_PREVIOUS; + case MouseEvent: + if (isMouseMove(keyStroke)) { + // do nothing + return Result.UNHANDLED; + } + getBasePane().setFocusedInteractable(this); + return Result.HANDLED; + default: + } + } + return Result.UNHANDLED; + } + + @Override + public TerminalPosition getCursorLocation() { + return getRenderer().getCursorLocation(self()); + } + + @Override + public InputFilter getInputFilter() { + return inputFilter; + } + + @Override + public synchronized T setInputFilter(InputFilter inputFilter) { + this.inputFilter = inputFilter; + return self(); + } + + public boolean isKeyboardActivationStroke(KeyStroke keyStroke) { + boolean isKeyboardActivation = (keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' ') || keyStroke.getKeyType() == KeyType.Enter; + + return isFocused() && isKeyboardActivation; + } + + public boolean isMouseActivationStroke(KeyStroke keyStroke) { + boolean isMouseActivation = false; + if (keyStroke instanceof MouseAction) { + MouseAction action = (MouseAction) keyStroke; + isMouseActivation = action.getActionType() == MouseActionType.CLICK_DOWN; + } + + return isMouseActivation; + } + + public boolean isActivationStroke(KeyStroke keyStroke) { + boolean isKeyboardActivationStroke = isKeyboardActivationStroke(keyStroke); + boolean isMouseActivationStroke = isMouseActivationStroke(keyStroke); + + return isKeyboardActivationStroke || isMouseActivationStroke; + } + + public boolean isMouseDown(KeyStroke keyStroke) { + return keyStroke.getKeyType() == KeyType.MouseEvent && ((MouseAction) keyStroke).isMouseDown(); + } + + public boolean isMouseDrag(KeyStroke keyStroke) { + return keyStroke.getKeyType() == KeyType.MouseEvent && ((MouseAction) keyStroke).isMouseDrag(); + } + + public boolean isMouseMove(KeyStroke keyStroke) { + return keyStroke.getKeyType() == KeyType.MouseEvent && ((MouseAction) keyStroke).isMouseMove(); + } + + public boolean isMouseUp(KeyStroke keyStroke) { + return keyStroke.getKeyType() == KeyType.MouseEvent && ((MouseAction) keyStroke).isMouseUp(); + } + + +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java new file mode 100644 index 0000000000000000000000000000000000000000..04b296cca94a4fe40560a12e69ab11f2b639514b --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java @@ -0,0 +1,556 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TerminalTextUtils; +import com.googlecode.lanterna.graphics.ThemeDefinition; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.MouseAction; +import com.googlecode.lanterna.input.MouseActionType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for several list box implementations, this will handle things like list of items and the scrollbar. + * + * @param Should always be itself, see {@code AbstractComponent} + * @param Type of items this list box contains + * @author Martin + */ +public abstract class AbstractListBox> extends AbstractInteractableComponent { + private final List items; + private int selectedIndex; + private ListItemRenderer listItemRenderer; + protected TerminalPosition scrollOffset = new TerminalPosition(0, 0); + + /** + * This constructor sets up the component so it has no preferred size but will ask to be as big as the list is. If + * the GUI cannot accommodate this size, scrolling and a vertical scrollbar will be used. + */ + protected AbstractListBox() { + this(null); + } + + /** + * This constructor sets up the component with a preferred size that is will always request, no matter what items + * are in the list box. If there are more items than the size can contain, scrolling and a vertical scrollbar will + * be used. Calling this constructor with a {@code null} value has the same effect as calling the default + * constructor. + * + * @param size Preferred size that the list should be asking for instead of invoking the preferred size calculation, + * or if set to {@code null} will ask to be big enough to display all items. + */ + protected AbstractListBox(TerminalSize size) { + this.items = new ArrayList<>(); + this.selectedIndex = -1; + setPreferredSize(size); + setListItemRenderer(createDefaultListItemRenderer()); + } + + @Override + protected InteractableRenderer createDefaultRenderer() { + return new DefaultListBoxRenderer<>(); + } + + /** + * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the + * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the + * entire list box but for each item, called one by one. + * + * @return {@code ListItemRenderer} to use when drawing the items in the list + */ + protected ListItemRenderer createDefaultListItemRenderer() { + return new ListItemRenderer<>(); + } + + ListItemRenderer getListItemRenderer() { + return listItemRenderer; + } + + /** + * This method overrides the {@code ListItemRenderer} that is used to draw each element in the list box. Note that + * this is not the renderer used for the entire list box but for each item, called one by one. + * + * @param listItemRenderer New renderer to use when drawing the items in the list box + * @return Itself + */ + public synchronized T setListItemRenderer(ListItemRenderer listItemRenderer) { + if (listItemRenderer == null) { + listItemRenderer = createDefaultListItemRenderer(); + if (listItemRenderer == null) { + throw new IllegalStateException("createDefaultListItemRenderer returned null"); + } + } + this.listItemRenderer = listItemRenderer; + return self(); + } + + @Override + public synchronized Result handleKeyStroke(KeyStroke keyStroke) { + try { + switch (keyStroke.getKeyType()) { + case Tab: + return Result.MOVE_FOCUS_NEXT; + + case ReverseTab: + return Result.MOVE_FOCUS_PREVIOUS; + + case ArrowRight: + return Result.MOVE_FOCUS_RIGHT; + + case ArrowLeft: + return Result.MOVE_FOCUS_LEFT; + + case ArrowDown: + if (items.isEmpty() || selectedIndex == items.size() - 1) { + return Result.MOVE_FOCUS_DOWN; + } + selectedIndex++; + return Result.HANDLED; + + case ArrowUp: + if (items.isEmpty() || selectedIndex == 0) { + return Result.MOVE_FOCUS_UP; + } + selectedIndex--; + return Result.HANDLED; + + case Home: + selectedIndex = 0; + return Result.HANDLED; + + case End: + selectedIndex = items.size() - 1; + return Result.HANDLED; + + case PageUp: + if (getSize() != null) { + setSelectedIndex(getSelectedIndex() - getSize().getRows()); + } + return Result.HANDLED; + + case PageDown: + if (getSize() != null) { + setSelectedIndex(getSelectedIndex() + getSize().getRows()); + } + return Result.HANDLED; + + case Character: + if (selectByCharacter(keyStroke.getCharacter())) { + return Result.HANDLED; + } + return Result.UNHANDLED; + case MouseEvent: + MouseAction mouseAction = (MouseAction) keyStroke; + MouseActionType actionType = mouseAction.getActionType(); + if (isMouseMove(keyStroke)) { + takeFocus(); + selectedIndex = getIndexByMouseAction(mouseAction); + return Result.HANDLED; + } + + if (actionType == MouseActionType.CLICK_RELEASE) { + // do nothing, desired actioning has been performed already on CLICK_DOWN and DRAG + return Result.HANDLED; + } else if (actionType == MouseActionType.SCROLL_UP) { + // relying on setSelectedIndex(index) to clip the index to valid values within range + setSelectedIndex(getSelectedIndex() - 1); + return Result.HANDLED; + } else if (actionType == MouseActionType.SCROLL_DOWN) { + // relying on setSelectedIndex(index) to clip the index to valid values within range + setSelectedIndex(getSelectedIndex() + 1); + return Result.HANDLED; + } + + selectedIndex = getIndexByMouseAction(mouseAction); + return super.handleKeyStroke(keyStroke); + default: + } + return Result.UNHANDLED; + } finally { + invalidate(); + } + } + + /** + * By converting {@link TerminalPosition}s to + * {@link #toGlobal(TerminalPosition)} gets index clicked on by mouse action. + * + * @return index of a item that was clicked on with {@link MouseAction} + */ + protected int getIndexByMouseAction(MouseAction click) { + int index = click.getPosition().getRow() - getGlobalPosition().getRow() - scrollOffset.getRow(); + + return Math.min(index, items.size() - 1); + } + + private boolean selectByCharacter(Character character) { + character = Character.toLowerCase(character); + + int selectedIndex = getSelectedIndex(); + for (int i = 0; i < getItemCount(); i++) { + int index = (selectedIndex + i + 1) % getItemCount(); + V item = getItemAt(index); + String label = item != null ? item.toString() : null; + if (label != null && label.length() > 0) { + char firstChar = Character.toLowerCase(label.charAt(0)); + if (firstChar == character) { + setSelectedIndex(index); + return true; + } + } + } + + return false; + } + + @Override + protected synchronized void afterEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) { + if (items.isEmpty()) { + return; + } + + if (direction == FocusChangeDirection.DOWN) { + selectedIndex = 0; + } else if (direction == FocusChangeDirection.UP) { + selectedIndex = items.size() - 1; + } + } + + /** + * Adds one more item to the list box, at the end. + * + * @param item Item to add to the list box + * @return Itself + */ + public synchronized T addItem(V item) { + if (item == null) { + return self(); + } + + items.add(item); + if (selectedIndex == -1) { + selectedIndex = 0; + } + invalidate(); + return self(); + } + + /** + * Removes an item from the list box by its index. The current selection in the list box will be adjusted + * accordingly. + * + * @param index Index of the item to remove + * @return The item that was removed + * @throws IndexOutOfBoundsException if the index is out of bounds in regards to the list of items + */ + public synchronized V removeItem(int index) { + V existing = items.remove(index); + if (index < selectedIndex) { + selectedIndex--; + } + while (selectedIndex >= items.size()) { + selectedIndex--; + } + invalidate(); + return existing; + } + + /** + * Removes all items from the list box + * + * @return Itself + */ + public synchronized T clearItems() { + items.clear(); + selectedIndex = -1; + invalidate(); + return self(); + } + + @Override + public boolean isFocusable() { + if (isEmpty()) { + // These dialog boxes are quite weird when they are empty and receive input focus, so try to avoid that + return false; + } + return super.isFocusable(); + } + + /** + * Looks for the particular item in the list and returns the index within the list (starting from zero) of that item + * if it is found, or -1 otherwise + * + * @param item What item to search for in the list box + * @return Index of the item in the list box or -1 if the list box does not contain the item + */ + public synchronized int indexOf(V item) { + return items.indexOf(item); + } + + /** + * Retrieves the item at the specified index in the list box + * + * @param index Index of the item to fetch + * @return The item at the specified index + * @throws IndexOutOfBoundsException If the index is less than zero or equals/greater than the number of items in + * the list box + */ + public synchronized V getItemAt(int index) { + return items.get(index); + } + + /** + * Checks if the list box has no items + * + * @return {@code true} if the list box has no items, {@code false} otherwise + */ + public synchronized boolean isEmpty() { + return items.isEmpty(); + } + + /** + * Returns the number of items currently in the list box + * + * @return Number of items in the list box + */ + public synchronized int getItemCount() { + return items.size(); + } + + /** + * Returns a copy of the items in the list box as a {@code List} + * + * @return Copy of all the items in this list box + */ + public synchronized List getItems() { + return new ArrayList<>(items); + } + + /** + * Sets which item in the list box that is currently selected. Please note that in this context, selected simply + * means it is the item that currently has input focus. This is not to be confused with list box implementations + * such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. + * This method will clip the supplied index to within 0 to items.size() -1. + * + * @param index Index of the item that should be currently selected + * @return Itself + */ + public synchronized T setSelectedIndex(int index) { + selectedIndex = Math.max(0, Math.min(index, items.size() - 1)); + + invalidate(); + return self(); + } + + /** + * Returns the index of the currently selected item in the list box. Please note that in this context, selected + * simply means it is the item that currently has input focus. This is not to be confused with list box + * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. + * + * @return The index of the currently selected row in the list box, or -1 if there are no items + */ + public int getSelectedIndex() { + return selectedIndex; + } + + /** + * Returns the currently selected item in the list box. Please note that in this context, selected + * simply means it is the item that currently has input focus. This is not to be confused with list box + * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. + * + * @return The currently selected item in the list box, or {@code null} if there are no items + */ + public synchronized V getSelectedItem() { + if (selectedIndex == -1) { + return null; + } else { + return items.get(selectedIndex); + } + } + + /** + * The default renderer for {@code AbstractListBox} and all its subclasses. + * + * @param Type of the items the list box this renderer is for + * @param Type of list box + */ + public static class DefaultListBoxRenderer> implements InteractableRenderer { + private final ScrollBar verticalScrollBar; + private int scrollTopIndex; + + /** + * Default constructor + */ + public DefaultListBoxRenderer() { + this.verticalScrollBar = new ScrollBar(Direction.VERTICAL); + this.scrollTopIndex = 0; + } + + @Override + public TerminalPosition getCursorLocation(T listBox) { + if (!listBox.getThemeDefinition().isCursorVisible()) { + return null; + } + int selectedIndex = listBox.getSelectedIndex(); + int columnAccordingToRenderer = listBox.getListItemRenderer().getHotSpotPositionOnLine(selectedIndex); + if (columnAccordingToRenderer == -1) { + return null; + } + return new TerminalPosition(columnAccordingToRenderer, selectedIndex - scrollTopIndex); + } + + @Override + public TerminalSize getPreferredSize(T listBox) { + int maxWidth = 5; //Set it to something... + int index = 0; + for (V item : listBox.getItems()) { + String itemString = listBox.getListItemRenderer().getLabel(listBox, index++, item); + int stringLengthInColumns = TerminalTextUtils.getColumnWidth(itemString); + if (stringLengthInColumns > maxWidth) { + maxWidth = stringLengthInColumns; + } + } + return new TerminalSize(maxWidth + 1, listBox.getItemCount()); + } + + @Override + public void drawComponent(TextGUIGraphics graphics, T listBox) { + //update the page size, used for page up and page down keys + ThemeDefinition themeDefinition = listBox.getTheme().getDefinition(AbstractListBox.class); + int componentHeight = graphics.getSize().getRows(); + //int componentWidth = graphics.getSize().getColumns(); + int selectedIndex = listBox.getSelectedIndex(); + List items = listBox.getItems(); + ListItemRenderer listItemRenderer = listBox.getListItemRenderer(); + + if (selectedIndex != -1) { + if (selectedIndex < scrollTopIndex) + scrollTopIndex = selectedIndex; + else if (selectedIndex >= componentHeight + scrollTopIndex) + scrollTopIndex = selectedIndex - componentHeight + 1; + } + + //Do we need to recalculate the scroll position? + //This code would be triggered by resizing the window when the scroll + //position is at the bottom + if (items.size() > componentHeight && + items.size() - scrollTopIndex < componentHeight) { + scrollTopIndex = items.size() - componentHeight; + } + + listBox.scrollOffset = new TerminalPosition(0, -scrollTopIndex); + + graphics.applyThemeStyle(themeDefinition.getNormal()); + graphics.fill(' '); + + TerminalSize itemSize = graphics.getSize().withRows(1); + for (int i = scrollTopIndex; i < items.size(); i++) { + if (i - scrollTopIndex >= componentHeight) { + break; + } + listItemRenderer.drawItem( + graphics.newTextGraphics(new TerminalPosition(0, i - scrollTopIndex), itemSize), + listBox, + i, + items.get(i), + selectedIndex == i, + listBox.isFocused()); + } + + graphics.applyThemeStyle(themeDefinition.getNormal()); + if (items.size() > componentHeight) { + verticalScrollBar.onAdded(listBox.getParent()); + verticalScrollBar.setViewSize(componentHeight); + verticalScrollBar.setScrollMaximum(items.size()); + verticalScrollBar.setScrollPosition(scrollTopIndex); + verticalScrollBar.draw(graphics.newTextGraphics( + new TerminalPosition(graphics.getSize().getColumns() - 1, 0), + new TerminalSize(1, graphics.getSize().getRows()))); + } + } + } + + /** + * The default list item renderer class, this can be extended and customized it needed. The instance which is + * assigned to the list box will be called once per item in the list when the list box is drawn. + * + * @param Type of the items in the list box + * @param Type of the list box class itself + */ + public static class ListItemRenderer> { + /** + * Returns where on the line to place the text terminal cursor for a currently selected item. By default this + * will return 0, meaning the first character of the selected line. If you extend {@code ListItemRenderer} you + * can change this by returning a different number. Returning -1 will cause lanterna to hide the cursor. + * + * @param selectedIndex Which item is currently selected + * @return Index of the character in the string we want to place the terminal cursor on, or -1 to hide it + */ + public int getHotSpotPositionOnLine(int selectedIndex) { + return 0; + } + + /** + * Given a list box, an index of an item within that list box and what the item is, this method should return + * what to draw for that item. The default implementation is to return whatever {@code toString()} returns when + * called on the item. + * + * @param listBox List box the item belongs to + * @param index Index of the item + * @param item The item itself + * @return String to draw for this item + */ + public String getLabel(T listBox, int index, V item) { + return item != null ? item.toString() : ""; + } + + /** + * This is the main drawing method for a single list box item, it applies the current theme to setup the colors + * and then calls {@code getLabel(..)} and draws the result using the supplied {@code TextGUIGraphics}. The + * graphics object is created just for this item and is restricted so that it can only draw on the area this + * item is occupying. The top-left corner (0x0) should be the starting point when drawing the item. + * + * @param graphics Graphics object to draw with + * @param listBox List box we are drawing an item from + * @param index Index of the item we are drawing + * @param item The item we are drawing + * @param selected Will be set to {@code true} if the item is currently selected, otherwise {@code false}, but + * please notice what context 'selected' refers to here (see {@code setSelectedIndex}) + * @param focused Will be set to {@code true} if the list box currently has input focus, otherwise {@code false} + */ + public void drawItem(TextGUIGraphics graphics, T listBox, int index, V item, boolean selected, boolean focused) { + ThemeDefinition themeDefinition = listBox.getTheme().getDefinition(AbstractListBox.class); + if (selected && focused) { + graphics.applyThemeStyle(themeDefinition.getSelected()); + } else { + graphics.applyThemeStyle(themeDefinition.getNormal()); + } + String label = getLabel(listBox, index, item); + label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns()); + while (TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) { + label += " "; + } + graphics.putString(0, 0, label); + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUI.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUI.java new file mode 100644 index 0000000000000000000000000000000000000000..b970312867cb6152954b178ed5e17a1c3f28e1ef --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUI.java @@ -0,0 +1,219 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.bundle.LanternaThemes; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import com.googlecode.lanterna.screen.Screen; + +import java.io.EOFException; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This abstract implementation of TextGUI contains some basic management of the underlying Screen and other common code + * that can be shared between different implementations. + * + * @author Martin + */ +public abstract class AbstractTextGUI implements TextGUI { + + private final Screen screen; + private final List listeners; + private boolean blockingIO; + private boolean dirty; + private TextGUIThread textGUIThread; + private Theme guiTheme; + + /** + * Constructor for {@code AbstractTextGUI} that requires a {@code Screen} and a factory for creating the GUI thread + * + * @param textGUIThreadFactory Factory class to use for creating the {@code TextGUIThread} class + * @param screen What underlying {@code Screen} to use for this text GUI + */ + protected AbstractTextGUI(TextGUIThreadFactory textGUIThreadFactory, Screen screen) { + if (screen == null) { + throw new IllegalArgumentException("Creating a TextGUI requires an underlying Screen"); + } + this.screen = screen; + this.listeners = new CopyOnWriteArrayList<>(); + this.blockingIO = false; + this.dirty = false; + this.guiTheme = LanternaThemes.getDefaultTheme(); + this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this); + } + + /** + * Reads one key from the input queue, blocking or non-blocking depending on if blocking I/O has been enabled. To + * enable blocking I/O (disabled by default), use {@code setBlockingIO(true)}. + * + * @return One piece of user input as a {@code KeyStroke} or {@code null} if blocking I/O is disabled and there was + * no input waiting + * @throws IOException In case of an I/O error while reading input + */ + protected KeyStroke readKeyStroke() throws IOException { + return blockingIO ? screen.readInput() : pollInput(); + } + + /** + * Polls the underlying input queue for user input, returning either a {@code KeyStroke} or {@code null} + * + * @return {@code KeyStroke} representing the user input or {@code null} if there was none + * @throws IOException In case of an I/O error while reading input + */ + protected KeyStroke pollInput() throws IOException { + return screen.pollInput(); + } + + @Override + public synchronized boolean processInput() throws IOException { + boolean gotInput = false; + KeyStroke keyStroke = readKeyStroke(); + if (keyStroke != null) { + gotInput = true; + do { + if (keyStroke.getKeyType() == KeyType.EOF) { + throw new EOFException(); + } + boolean handled = handleInput(keyStroke); + if (!handled) { + handled = fireUnhandledKeyStroke(keyStroke); + } + dirty = handled || dirty; + keyStroke = pollInput(); + } while (keyStroke != null); + } + return gotInput; + } + + @Override + public void setTheme(Theme theme) { + if (theme != null) { + this.guiTheme = theme; + } + } + + @Override + public Theme getTheme() { + return guiTheme; + } + + @Override + public synchronized void updateScreen() throws IOException { + screen.doResizeIfNecessary(); + drawGUI(new DefaultTextGUIGraphics(this, screen.newTextGraphics())); + screen.setCursorPosition(getCursorPosition()); + screen.refresh(); + dirty = false; + } + + @Override + public Screen getScreen() { + return screen; + } + + @Override + public boolean isPendingUpdate() { + return screen.doResizeIfNecessary() != null || dirty; + } + + @Override + public TextGUIThread getGUIThread() { + return textGUIThread; + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + /** + * Enables blocking I/O, causing calls to {@code readKeyStroke()} to block until there is input available. Notice + * that you can still poll for input using {@code pollInput()}. + * + * @param blockingIO Set this to {@code true} if blocking I/O should be enabled, otherwise {@code false} + */ + public void setBlockingIO(boolean blockingIO) { + this.blockingIO = blockingIO; + } + + /** + * Checks if blocking I/O is enabled or not + * + * @return {@code true} if blocking I/O is enabled, otherwise {@code false} + */ + public boolean isBlockingIO() { + return blockingIO; + } + + /** + * This method should be called when there was user input that wasn't handled by the GUI. It will fire the + * {@code onUnhandledKeyStroke(..)} method on any registered listener. + * + * @param keyStroke The {@code KeyStroke} that wasn't handled by the GUI + * @return {@code true} if at least one of the listeners handled the key stroke, this will signal to the GUI that it + * needs to be redrawn again. + */ + protected final boolean fireUnhandledKeyStroke(KeyStroke keyStroke) { + boolean handled = false; + for (Listener listener : listeners) { + handled = listener.onUnhandledKeyStroke(this, keyStroke) || handled; + } + return handled; + } + + /** + * Marks the whole text GUI as invalid and that it needs to be redrawn at next opportunity + */ + protected void invalidate() { + dirty = true; + } + + /** + * Draws the entire GUI using a {@code TextGUIGraphics} object + * + * @param graphics Graphics object to draw using + */ + protected abstract void drawGUI(TextGUIGraphics graphics); + + /** + * Top-level method for drilling in to the GUI and figuring out, in global coordinates, where to place the text + * cursor on the screen at this time. + * + * @return Where to place the text cursor, or {@code null} if the cursor should be hidden + */ + protected abstract TerminalPosition getCursorPosition(); + + /** + * This method should take the user input and feed it to the focused component for handling. + * + * @param key {@code KeyStroke} representing the user input + * @return {@code true} if the input was recognized and handled by the GUI, indicating that the GUI should be redrawn + */ + protected abstract boolean handleInput(KeyStroke key); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java new file mode 100644 index 0000000000000000000000000000000000000000..8805b0bb3e87f4c4a9e0d266396e49bb0f3ce31c --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java @@ -0,0 +1,126 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Abstract implementation of {@link TextGUIThread} with common logic for both available concrete implementations. + */ +public abstract class AbstractTextGUIThread implements TextGUIThread { + + protected final TextGUI textGUI; + protected final Queue customTasks; + protected ExceptionHandler exceptionHandler; + + /** + * Sets up this {@link AbstractTextGUIThread} for operations on the supplies {@link TextGUI} + * + * @param textGUI Text GUI this {@link TextGUIThread} implementations will be operating on + */ + public AbstractTextGUIThread(TextGUI textGUI) { + this.exceptionHandler = new ExceptionHandler() { + @Override + public boolean onIOException(IOException e) { + e.printStackTrace(); + return true; + } + + @Override + public boolean onRuntimeException(RuntimeException e) { + e.printStackTrace(); + return true; + } + }; + this.textGUI = textGUI; + this.customTasks = new LinkedBlockingQueue<>(); + } + + @Override + public void invokeLater(Runnable runnable) throws IllegalStateException { + customTasks.add(runnable); + } + + @Override + public void setExceptionHandler(ExceptionHandler exceptionHandler) { + if (exceptionHandler == null) { + throw new IllegalArgumentException("Cannot call setExceptionHandler(null)"); + } + this.exceptionHandler = exceptionHandler; + } + + @Override + public synchronized boolean processEventsAndUpdate() throws IOException { + if (getThread() != Thread.currentThread()) { + throw new IllegalStateException("Calling processEventAndUpdate outside of GUI thread"); + } + try { + textGUI.processInput(); + while (!customTasks.isEmpty()) { + Runnable r = customTasks.poll(); + if (r != null) { + r.run(); + } + } + if (textGUI.isPendingUpdate()) { + textGUI.updateScreen(); + return true; + } + return false; + } catch (EOFException e) { + // Always re-throw EOFExceptions so the UI system knows we've closed the terminal + throw e; + } catch (IOException e) { + if (exceptionHandler != null) { + exceptionHandler.onIOException(e); + } else { + throw e; + } + } catch (RuntimeException e) { + if (exceptionHandler != null) { + exceptionHandler.onRuntimeException(e); + } else { + throw e; + } + } + return true; + } + + @Override + public void invokeAndWait(final Runnable runnable) throws IllegalStateException, InterruptedException { + Thread guiThread = getThread(); + if (guiThread == null || Thread.currentThread() == guiThread) { + runnable.run(); + } else { + final CountDownLatch countDownLatch = new CountDownLatch(1); + invokeLater(() -> { + try { + runnable.run(); + } finally { + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractWindow.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..c644ab4e1ee5e5fca66aba05e94477aa55dd42ab --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractWindow.java @@ -0,0 +1,337 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.menu.MenuBar; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Abstract Window has most of the code requiring for a window to function, all concrete window implementations extends + * from this in one way or another. You can define your own window by extending from this, as an alternative to building + * up the GUI externally by constructing a {@code BasicWindow} and adding components to it. + * + * @author Martin + */ +public abstract class AbstractWindow extends AbstractBasePane implements Window { + private String title; + private WindowBasedTextGUI textGUI; + private boolean visible; + private TerminalSize lastKnownSize; + private TerminalSize lastKnownDecoratedSize; + private TerminalPosition lastKnownPosition; + private TerminalPosition contentOffset; + private Set hints; + private WindowPostRenderer windowPostRenderer; + private boolean closeWindowWithEscape; + + /** + * Default constructor, this creates a window with no title + */ + public AbstractWindow() { + this(""); + } + + /** + * Creates a window with a specific title that will (probably) be drawn in the window decorations + * + * @param title Title of this window + */ + public AbstractWindow(String title) { + super(); + this.title = title; + this.textGUI = null; + this.visible = true; + this.contentOffset = TerminalPosition.TOP_LEFT_CORNER; + this.lastKnownPosition = null; + this.lastKnownSize = null; + this.lastKnownDecoratedSize = null; + this.closeWindowWithEscape = false; + + this.hints = new HashSet<>(); + } + + /** + * Setting this property to {@code true} will cause pressing the ESC key to close the window. This used to be the + * default behaviour of lanterna 3 during the development cycle but is not longer the case. You are encouraged to + * put proper buttons or other kind of components to clearly mark to the user how to close the window instead of + * magically taking ESC, but sometimes it can be useful (when doing testing, for example) to enable this mode. + * + * @param closeWindowWithEscape If {@code true}, this window will self-close if you press ESC key + */ + public void setCloseWindowWithEscape(boolean closeWindowWithEscape) { + this.closeWindowWithEscape = closeWindowWithEscape; + } + + @Override + public void setTextGUI(WindowBasedTextGUI textGUI) { + //This is kind of stupid check, but might cause it to blow up on people using the library incorrectly instead of + //just causing weird behaviour + if (this.textGUI != null && textGUI != null) { + throw new UnsupportedOperationException("Are you calling setTextGUI yourself? Please read the documentation" + + " in that case (this could also be a bug in Lanterna, please report it if you are sure you are " + + "not calling Window.setTextGUI(..) from your code)"); + } + this.textGUI = textGUI; + } + + @Override + public WindowBasedTextGUI getTextGUI() { + return textGUI; + } + + /** + * Alters the title of the window to the supplied string + * + * @param title New title of the window + */ + public void setTitle(String title) { + this.title = title; + invalidate(); + } + + @Override + public String getTitle() { + return title; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public void draw(TextGUIGraphics graphics) { + if (!graphics.getSize().equals(lastKnownSize)) { + getComponent().invalidate(); + } + setSize(graphics.getSize(), false); + super.draw(graphics); + } + + @Override + public boolean handleInput(KeyStroke key) { + boolean handled = super.handleInput(key); + if (!handled && closeWindowWithEscape && key.getKeyType() == KeyType.Escape) { + close(); + return true; + } + return handled; + } + + /** + * @see Window#toGlobalFromContentRelative(TerminalPosition) + */ + @Override + @Deprecated + public TerminalPosition toGlobal(TerminalPosition localPosition) { + return toGlobalFromContentRelative(localPosition); + } + + @Override + public TerminalPosition toGlobalFromContentRelative(TerminalPosition contentLocalPosition) { + if (contentLocalPosition == null) { + return null; + } + return lastKnownPosition.withRelative(contentOffset.withRelative(contentLocalPosition)); + } + + @Override + @Deprecated + public TerminalPosition toGlobalFromDecoratedRelative(TerminalPosition localPosition) { + if (localPosition == null) { + return null; + } + return lastKnownPosition.withRelative(localPosition); + } + + /** + * @see Window#fromGlobalToContentRelative(TerminalPosition) + */ + @Override + @Deprecated + public TerminalPosition fromGlobal(TerminalPosition globalPosition) { + return fromGlobalToContentRelative(globalPosition); + } + + @Override + public TerminalPosition fromGlobalToContentRelative(TerminalPosition globalPosition) { + if (globalPosition == null || lastKnownPosition == null) { + return null; + } + return globalPosition.withRelative( + -lastKnownPosition.getColumn() - contentOffset.getColumn(), + -lastKnownPosition.getRow() - contentOffset.getRow()); + } + + @Override + public TerminalPosition fromGlobalToDecoratedRelative(TerminalPosition globalPosition) { + if (globalPosition == null || lastKnownPosition == null) { + return null; + } + return globalPosition.withRelative( + -lastKnownPosition.getColumn(), + -lastKnownPosition.getRow()); + } + + @Override + public TerminalSize getPreferredSize() { + TerminalSize preferredSize = contentHolder.getPreferredSize(); + MenuBar menuBar = getMenuBar(); + if (menuBar.getMenuCount() > 0) { + TerminalSize menuPreferredSize = menuBar.getPreferredSize(); + preferredSize = preferredSize.withRelativeRows(menuPreferredSize.getRows()) + .withColumns(Math.max(menuPreferredSize.getColumns(), preferredSize.getColumns())); + } + return preferredSize; + } + + @Override + public void setHints(Collection hints) { + this.hints = new HashSet<>(hints); + invalidate(); + } + + @Override + public Set getHints() { + return Collections.unmodifiableSet(hints); + } + + @Override + public WindowPostRenderer getPostRenderer() { + return windowPostRenderer; + } + + @Override + public void addWindowListener(WindowListener windowListener) { + addBasePaneListener(windowListener); + } + + @Override + public void removeWindowListener(WindowListener windowListener) { + removeBasePaneListener(windowListener); + } + + /** + * Sets the post-renderer to use for this window. This will override the default from the GUI system (if there is + * one set, otherwise from the theme). + * + * @param windowPostRenderer Window post-renderer to assign to this window + */ + public void setWindowPostRenderer(WindowPostRenderer windowPostRenderer) { + this.windowPostRenderer = windowPostRenderer; + } + + @Override + public final TerminalPosition getPosition() { + return lastKnownPosition; + } + + @Override + public final void setPosition(TerminalPosition topLeft) { + TerminalPosition oldPosition = this.lastKnownPosition; + this.lastKnownPosition = topLeft; + + // Fire listeners + for (BasePaneListener listener : getBasePaneListeners()) { + if (listener instanceof WindowListener) { + ((WindowListener) listener).onMoved(this, oldPosition, topLeft); + } + } + } + + @Override + public final TerminalSize getSize() { + return lastKnownSize; + } + + @Override + @Deprecated + public void setSize(TerminalSize size) { + setSize(size, true); + } + + @Override + public void setFixedSize(TerminalSize size) { + hints.add(Hint.FIXED_SIZE); + setSize(size); + } + + private void setSize(TerminalSize size, boolean invalidate) { + TerminalSize oldSize = this.lastKnownSize; + this.lastKnownSize = size; + if (invalidate) { + invalidate(); + } + + // Fire listeners + for (BasePaneListener listener : getBasePaneListeners()) { + if (listener instanceof WindowListener) { + ((WindowListener) listener).onResized(this, oldSize, size); + } + } + } + + @Override + public final TerminalSize getDecoratedSize() { + return lastKnownDecoratedSize; + } + + @Override + public final void setDecoratedSize(TerminalSize decoratedSize) { + this.lastKnownDecoratedSize = decoratedSize; + } + + @Override + public void setContentOffset(TerminalPosition offset) { + this.contentOffset = offset; + } + + @Override + public void close() { + if (textGUI != null) { + textGUI.removeWindow(this); + } + setComponent(null); + } + + @Override + public void waitUntilClosed() { + WindowBasedTextGUI textGUI = getTextGUI(); + if (textGUI != null) { + textGUI.waitForWindowToClose(this); + } + } + + Window self() { + return this; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ActionListBox.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ActionListBox.java new file mode 100644 index 0000000000000000000000000000000000000000..521a542dcfcc13da46edf11bd39a6398a3323c09 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ActionListBox.java @@ -0,0 +1,138 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import com.googlecode.lanterna.input.MouseAction; +import com.googlecode.lanterna.input.MouseActionType; + +/** + * This class is a list box implementation that displays a number of items that has actions associated with them. You + * can activate this action by pressing the Enter or Space keys on the keyboard and the action associated with the + * currently selected item will fire. + * + * @author Martin + */ +public class ActionListBox extends AbstractListBox { + + /** + * Default constructor, creates an {@code ActionListBox} with no pre-defined size that will request to be big enough + * to display all items + */ + public ActionListBox() { + this(null); + } + + /** + * Creates a new {@code ActionListBox} with a pre-set size. If the items don't fit in within this size, scrollbars + * will be used to accommodate. Calling {@code new ActionListBox(null)} has the same effect as calling + * {@code new ActionListBox()}. + * + * @param preferredSize Preferred size of this {@link ActionListBox} + */ + public ActionListBox(TerminalSize preferredSize) { + super(preferredSize); + } + + /** + * {@inheritDoc} + *

+ * The label of the item in the list box will be the result of calling {@code .toString()} on the runnable, which + * might not be what you want to have unless you explicitly declare it. Consider using + * {@code addItem(String label, Runnable action} instead, if you want to just set the label easily without having + * to override {@code .toString()}. + * + * @param object Runnable to execute when the action was selected and fired in the list + * @return Itself + */ + @Override + public ActionListBox addItem(Runnable object) { + return super.addItem(object); + } + + /** + * Adds a new item to the list, which is displayed in the list using a supplied label. + * + * @param label Label to use in the list for the new item + * @param action Runnable to invoke when this action is selected and then triggered + * @return Itself + */ + public ActionListBox addItem(final String label, final Runnable action) { + return addItem(new Runnable() { + @Override + public void run() { + action.run(); + } + + @Override + public String toString() { + return label; + } + }); + } + + @Override + public TerminalPosition getCursorLocation() { + return null; + } + + @Override + public Result handleKeyStroke(KeyStroke keyStroke) { + if (isKeyboardActivationStroke(keyStroke)) { + runSelectedItem(); + return Result.HANDLED; + } else if (keyStroke.getKeyType() == KeyType.MouseEvent) { + MouseAction mouseAction = (MouseAction) keyStroke; + MouseActionType actionType = mouseAction.getActionType(); + + if (isMouseMove(keyStroke) + || actionType == MouseActionType.CLICK_RELEASE + || actionType == MouseActionType.SCROLL_UP + || actionType == MouseActionType.SCROLL_DOWN) { + return super.handleKeyStroke(keyStroke); + } + + // includes mouse drag + int existingIndex = getSelectedIndex(); + int newIndex = getIndexByMouseAction(mouseAction); + if (existingIndex != newIndex || !isFocused() || actionType == MouseActionType.CLICK_DOWN) { + // the index has changed, or the focus needs to be obtained, or the user is clicking on the current selection to perform the action again + Result result = super.handleKeyStroke(keyStroke); + runSelectedItem(); + return result; + } + return Result.HANDLED; + } else { + Result result = super.handleKeyStroke(keyStroke); + //runSelectedItem(); + return result; + } + } + + public void runSelectedItem() { + Object selectedItem = getSelectedItem(); + if (selectedItem != null) { + ((Runnable) selectedItem).run(); + } + } + +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AnimatedLabel.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AnimatedLabel.java new file mode 100644 index 0000000000000000000000000000000000000000..0e321fdb89037de688c5d06a81ef6432d1c56d02 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AnimatedLabel.java @@ -0,0 +1,184 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalSize; + +import java.lang.ref.WeakReference; +import java.util.*; + +/** + * This is a special label that contains not just a single text to display but a number of frames that are cycled + * through. The class will manage a timer on its own and ensure the label is updated and redrawn. There is a static + * helper method available to create the classic "spinning bar": {@code createClassicSpinningLine()} + */ +public class AnimatedLabel extends Label { + private static Timer TIMER = null; + private static final WeakHashMap SCHEDULED_TASKS = new WeakHashMap<>(); + + /** + * Creates a classic spinning bar which can be used to signal to the user that an operation in is process. + * + * @return {@code AnimatedLabel} instance which is setup to show a spinning bar + */ + public static AnimatedLabel createClassicSpinningLine() { + return createClassicSpinningLine(150); + } + + /** + * Creates a classic spinning bar which can be used to signal to the user that an operation in is process. + * + * @param speed Delay in between each frame + * @return {@code AnimatedLabel} instance which is setup to show a spinning bar + */ + public static AnimatedLabel createClassicSpinningLine(int speed) { + AnimatedLabel animatedLabel = new AnimatedLabel("-"); + animatedLabel.addFrame("\\"); + animatedLabel.addFrame("|"); + animatedLabel.addFrame("/"); + animatedLabel.startAnimation(speed); + return animatedLabel; + } + + private final List frames; + private TerminalSize combinedMaximumPreferredSize; + private int currentFrame; + + /** + * Creates a new animated label, initially set to one frame. You will need to add more frames and call + * {@code startAnimation()} for this to start moving. + * + * @param firstFrameText The content of the label at the first frame + */ + public AnimatedLabel(String firstFrameText) { + super(firstFrameText); + frames = new ArrayList<>(); + currentFrame = 0; + combinedMaximumPreferredSize = TerminalSize.ZERO; + + String[] lines = splitIntoMultipleLines(firstFrameText); + frames.add(lines); + ensurePreferredSize(lines); + } + + @Override + protected synchronized TerminalSize calculatePreferredSize() { + return super.calculatePreferredSize().max(combinedMaximumPreferredSize); + } + + /** + * Adds one more frame at the end of the list of frames + * + * @param text Text to use for the label at this frame + * @return Itself + */ + public synchronized AnimatedLabel addFrame(String text) { + String[] lines = splitIntoMultipleLines(text); + frames.add(lines); + ensurePreferredSize(lines); + return this; + } + + private void ensurePreferredSize(String[] lines) { + combinedMaximumPreferredSize = combinedMaximumPreferredSize.max(getBounds(lines, combinedMaximumPreferredSize)); + } + + /** + * Advances the animated label to the next frame. You normally don't need to call this manually as it will be done + * by the animation thread. + */ + public synchronized void nextFrame() { + currentFrame++; + if (currentFrame >= frames.size()) { + currentFrame = 0; + } + super.setLines(frames.get(currentFrame)); + invalidate(); + } + + @Override + public void onRemoved(Container container) { + stopAnimation(); + } + + /** + * Starts the animation thread which will periodically call {@code nextFrame()} at the interval specified by the + * {@code millisecondsPerFrame} parameter. After all frames have been cycled through, it will start over from the + * first frame again. + * + * @param millisecondsPerFrame The interval in between every frame + * @return Itself + */ + public synchronized AnimatedLabel startAnimation(long millisecondsPerFrame) { + if (TIMER == null) { + TIMER = new Timer("AnimatedLabel"); + } + AnimationTimerTask animationTimerTask = new AnimationTimerTask(this); + SCHEDULED_TASKS.put(this, animationTimerTask); + TIMER.scheduleAtFixedRate(animationTimerTask, millisecondsPerFrame, millisecondsPerFrame); + return this; + } + + /** + * Halts the animation thread and the label will stop at whatever was the current frame at the time when this was + * called + * + * @return Itself + */ + public synchronized AnimatedLabel stopAnimation() { + removeTaskFromTimer(this); + return this; + } + + private static synchronized void removeTaskFromTimer(AnimatedLabel animatedLabel) { + SCHEDULED_TASKS.get(animatedLabel).cancel(); + SCHEDULED_TASKS.remove(animatedLabel); + canCloseTimer(); + } + + private static synchronized void canCloseTimer() { + if (SCHEDULED_TASKS.isEmpty()) { + TIMER.cancel(); + TIMER = null; + } + } + + private static class AnimationTimerTask extends TimerTask { + private final WeakReference labelRef; + + private AnimationTimerTask(AnimatedLabel label) { + this.labelRef = new WeakReference<>(label); + } + + @Override + public void run() { + AnimatedLabel animatedLabel = labelRef.get(); + if (animatedLabel == null) { + cancel(); + canCloseTimer(); + } else { + if (animatedLabel.getBasePane() == null) { + animatedLabel.stopAnimation(); + } else { + animatedLabel.nextFrame(); + } + } + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java new file mode 100644 index 0000000000000000000000000000000000000000..ef9650ce88f5ed262d922e9f6a49893d38cbf40d --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java @@ -0,0 +1,83 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import java.util.concurrent.TimeUnit; + +/** + * Extended interface of TextGUIThread for implementations that uses a separate thread for all GUI event processing and + * updating. + * + * @author Martin + */ +public interface AsynchronousTextGUIThread extends TextGUIThread { + /** + * Starts the AsynchronousTextGUIThread, typically meaning that the event processing loop will start. + */ + void start(); + + /** + * Requests that the AsynchronousTextGUIThread stops, typically meaning that the event processing loop will exit + */ + void stop(); + + /** + * Blocks until the GUI loop has stopped + * + * @throws InterruptedException In case this thread was interrupted while waiting for the GUI thread to exit + */ + void waitForStop() throws InterruptedException; + + /** + * Blocks until the GUI loop has stopped + * + * @throws InterruptedException In case this thread was interrupted while waiting for the GUI thread to exit + */ + void waitForStop(long time, TimeUnit unit) throws InterruptedException; + + /** + * Returns the current status of this GUI thread + * + * @return Current status of the GUI thread + */ + State getState(); + + /** + * Enum representing the states of the GUI thread life-cycle + */ + enum State { + /** + * The instance has been created but not yet started + */ + CREATED, + /** + * The thread has started an is running + */ + STARTED, + /** + * The thread is trying to stop but is still running + */ + STOPPING, + /** + * The thread has stopped + */ + STOPPED, + ; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePane.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePane.java new file mode 100644 index 0000000000000000000000000000000000000000..4a1fb19519058aaacd3705cc026c0bd492015634 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePane.java @@ -0,0 +1,196 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.gui2.menu.MenuBar; +import com.googlecode.lanterna.input.KeyStroke; + +/** + * BasePane is the base container in a Text GUI. A text gui may have several base panes, although they are + * always independent. One common example of this is a multi-window system where each window is a base pane. Think of + * the base pane as a root container, the ultimate parent of all components added to a GUI. When you use + * {@code MultiWindowTextGUI}, the background space behind the windows is a {@code BasePane} too, just like each of the + * windows. They are all drawn separately and composited together. Every {@code BasePane} has a single component that + * is drawn over the entire area the {@code BasePane} is occupying, it's very likely you want to set this component to + * be a container of some sort, probably a {@code Panel}, that can host multiple child components. + * + * @author Martin + * @see Panel + */ +public interface BasePane extends Composite { + + /** + * Returns the TextGUI this BasePane belongs to or {@code null} if none. One example of when this method returns + * {@code null} is when calling it on a Window that hasn't been displayed yet. + * + * @return The TextGUI this BasePane belongs to + */ + TextGUI getTextGUI(); + + /** + * Called by the GUI system (or something imitating the GUI system) to draw the root container. The TextGUIGraphics + * object should be used to perform the drawing operations. + * + * @param graphics TextGraphics object to draw with + */ + void draw(TextGUIGraphics graphics); + + /** + * Checks if this root container (i.e. any of its child components) has signaled that what it's currently displaying + * is out of date and needs re-drawing. + * + * @return {@code true} if the container's content is invalid and needs redrawing, {@code false} otherwise + */ + boolean isInvalid(); + + /** + * Invalidates the whole root container (including all of its child components) which will cause them all to be + * recalculated (for containers) and redrawn. + */ + void invalidate(); + + /** + * Called by the GUI system to delegate a keyboard input event. The root container will decide what to do with this + * input, usually sending it to one of its sub-components, but if it isn't able to find any handler for this input + * it should return {@code false} so that the GUI system can take further decisions on what to do with it. + * + * @param key Keyboard input + * @return {@code true} If the root container could handle the input, false otherwise + */ + boolean handleInput(KeyStroke key); + + /** + * Returns the component that is the content of the BasePane. This is probably the root of a hierarchy of nested + * Panels but it could also be a single component. + * + * @return Component which is the content of this BasePane + */ + @Override + Component getComponent(); + + /** + * Sets the top-level component inside this BasePane. If you want it to contain only one component, you can set it + * directly, but for more complicated GUIs you probably want to create a hierarchy of panels and set the first one + * here. + * + * @param component Component which this BasePane is using as it's content + */ + @Override + void setComponent(Component component); + + /** + * Returns the component in the root container that currently has input focus. There can only be one component at a + * time being in focus. + * + * @return Interactable component that is currently in receiving input focus + */ + Interactable getFocusedInteractable(); + + /** + * Sets the component currently in focus within this root container, or sets no component in focus if {@code null} + * is passed in. + * + * @param interactable Interactable to focus, or {@code null} to clear focus + */ + void setFocusedInteractable(Interactable interactable); + + /** + * Returns the position of where to put the terminal cursor according to this root container. This is typically + * derived from which component has focus, or {@code null} if no component has focus or if the root container doesn't + * want the cursor to be visible. Note that the coordinates are in local coordinate space, relative to the top-left + * corner of the root container. You can use your TextGUI implementation to translate these to global coordinates. + * + * @return Local position of where to place the cursor, or {@code null} if the cursor shouldn't be visible + */ + TerminalPosition getCursorPosition(); + + /** + * Returns a position in a root container's local coordinate space to global coordinates + * + * @param localPosition The local position to translate + * @return The local position translated to global coordinates + */ + TerminalPosition toGlobal(TerminalPosition localPosition); + + /** + * Returns a position expressed in global coordinates, i.e. row and column offset from the top-left corner of the + * terminal into a position relative to the top-left corner of the base pane. Calling + * {@code fromGlobal(toGlobal(..))} should return the exact same position. + * + * @param position Position expressed in global coordinates to translate to local coordinates of this BasePane + * @return The global coordinates expressed as local coordinates + */ + TerminalPosition fromGlobal(TerminalPosition position); + + /** + * If set to true, up/down array keys will not translate to next/previous if there are no more components + * above/below. + * + * @param strictFocusChange Will not allow relaxed navigation if set to {@code true} + */ + void setStrictFocusChange(boolean strictFocusChange); + + /** + * If set to false, using the keyboard arrows keys will have the same effect as using the tab and reverse tab. + * Lanterna will map arrow down and arrow right to tab, going to the next component, and array up and array left to + * reverse tab, going to the previous component. If set to true, Lanterna will search for the next component + * starting at the cursor position in the general direction of the arrow. By default this is enabled. + *

+ * In Lanterna 2, direction based movements were not available. + * + * @param enableDirectionBasedMovements Should direction based focus movements be enabled? + */ + void setEnableDirectionBasedMovements(boolean enableDirectionBasedMovements); + + /** + * Returns the text GUI {@link Theme} associated with this base pane/window. This is either coming from the + * {@link TextGUI} this object is associated with, the theme set as the override through {@link #setTheme(Theme)} + * or {@code null} if this base pane/window isn't added to any {@link TextGUI} and doesn't have any override. + * + * @return The {@link Theme} this base pane/window is expected to use when drawing the contents + */ + Theme getTheme(); + + /** + * Sets the override {@link Theme} to use for this base pane/window, rather than the default {@link Theme} + * associated with the {@link TextGUI} it is attached to. If called with {@code null}, it will clear the override + * and use the default value instead. + * + * @param theme {@link Theme} to assign to this base pane/window, or {@code null} to reset + */ + void setTheme(Theme theme); + + /** + * Sets the active {@link MenuBar} for this base pane/window. The menu will be rendered at the top (inside the + * window decorations if set on a window), if set. If called with {@code null}, any previously set menu bar is + * removed. + * + * @param menubar The {@link MenuBar} to assign to this pane/window + */ + void setMenuBar(MenuBar menubar); + + /** + * Returns the {@link MenuBar} assigned to this base pane/window, if any, otherwise returns {code null}. + * + * @return The active menu bar or {@code null} + */ + MenuBar getMenuBar(); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePaneListener.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePaneListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ce48080a3dda0cfd478afd203c835da369019918 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePaneListener.java @@ -0,0 +1,55 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.input.KeyStroke; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Base listener interface having callback methods for events relating to {@link BasePane} (and {@link Window}, which + * extends {@link BasePane}) so that you can be notified by a callback when certain events happen. Assume it is the GUI + * thread that will call these methods. You typically use this through {@link WindowListener} and calling + * {@link Window#addWindowListener(WindowListener)} + */ +public interface BasePaneListener { + /** + * Called when a user input is about to be delivered to the focused {@link Interactable} inside the + * {@link BasePane}, but before it is actually delivered. You can catch it and prevent it from being passed into + * the component by using the {@code deliverEvent} parameter and setting it to {@code false}. + * + * @param basePane Base pane that got the input event + * @param keyStroke The actual input event + * @param deliverEvent Set to {@code true} automatically, if you change it to {@code false} it will prevent the GUI + * from passing the input event on to the focused {@link Interactable} + */ + void onInput(T basePane, KeyStroke keyStroke, AtomicBoolean deliverEvent); + + /** + * Called when a user entered some input which wasn't handled by the focused component. This allows you to catch it + * at a {@link BasePane} (or {@link Window}) level and prevent it from being reported to the {@link TextGUI} as an + * unhandled input event. + * + * @param basePane {@link BasePane} that got the input event + * @param keyStroke The unhandled input event + * @param hasBeenHandled Initially set to {@code false}, if you change it to {@code true} then the event + * will not be reported as an unhandled input to the {@link TextGUI} + */ + void onUnhandledInput(T basePane, KeyStroke keyStroke, AtomicBoolean hasBeenHandled); +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasicWindow.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasicWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..c7a5c0bd515c0082a986dc423bbaf3c7bb7c2361 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasicWindow.java @@ -0,0 +1,45 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +/** + * Simple AbstractWindow implementation that you can use as a building block when creating new windows without having + * to create new classes. + * + * @author Martin + */ +public class BasicWindow extends AbstractWindow { + + /** + * Default constructor, creates a new window with no title + */ + public BasicWindow() { + super(); + } + + /** + * This constructor creates a window with a specific title, that is (probably) going to be displayed in the window + * decoration + * + * @param title Title of the window + */ + public BasicWindow(String title) { + super(title); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Border.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Border.java new file mode 100644 index 0000000000000000000000000000000000000000..f3abe6fca15e7de8fade0fa5e19e06d51ca17564 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Border.java @@ -0,0 +1,48 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; + +/** + * Main interface for different border classes, with additional methods to help lanterna figure out the size and offset + * of components wrapped by borders. + * + * @author Martin + */ +public interface Border extends Container, Composite { + interface BorderRenderer extends ComponentRenderer { + /** + * How large is the offset from the top left corner of the border to the top left corner of the wrapped component? + * + * @return Position of the wrapped components top left position, relative to the top left corner of the border + */ + TerminalPosition getWrappedComponentTopLeftOffset(); + + /** + * Given a total size of the border composite and it's wrapped component, how large would the actual wrapped + * component be? + * + * @param borderSize Size to calculate for, this should be the total size of the border and the inner component + * @return Size of the inner component if the total size of inner + border is borderSize + */ + TerminalSize getWrappedComponentSize(TerminalSize borderSize); + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BorderLayout.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BorderLayout.java new file mode 100644 index 0000000000000000000000000000000000000000..60d52c771d72de197c39b84f145e9cc973636375 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BorderLayout.java @@ -0,0 +1,193 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; + +import java.util.*; + +/** + * BorderLayout imitates the BorderLayout class from AWT, allowing you to add a center component with optional + * components around it in top, bottom, left and right locations. The edge components will be sized at their preferred + * size and the center component will take up whatever remains. + * + * @author martin + */ +public class BorderLayout implements LayoutManager { + + /** + * This type is what you use as the layout data for components added to a panel using {@code BorderLayout} for its + * layout manager. This values specified where inside the panel the component should be added. + */ + public enum Location implements LayoutData { + /** + * The component with this value as its layout data will occupy the center space, whatever is remaining after + * the other components (if any) have allocated their space. + */ + CENTER, + /** + * The component with this value as its layout data will occupy the left side of the container, attempting to + * allocate the preferred width of the component and at least the preferred height, but could be more depending + * on the other components added. + */ + LEFT, + /** + * The component with this value as its layout data will occupy the right side of the container, attempting to + * allocate the preferred width of the component and at least the preferred height, but could be more depending + * on the other components added. + */ + RIGHT, + /** + * The component with this value as its layout data will occupy the top side of the container, attempting to + * allocate the preferred height of the component and at least the preferred width, but could be more depending + * on the other components added. + */ + TOP, + /** + * The component with this value as its layout data will occupy the bottom side of the container, attempting to + * allocate the preferred height of the component and at least the preferred width, but could be more depending + * on the other components added. + */ + BOTTOM, + ; + } + + //When components don't have a location, we'll assign an available location based on this order + private static final List AUTO_ASSIGN_ORDER = Collections.unmodifiableList(Arrays.asList( + Location.CENTER, + Location.TOP, + Location.BOTTOM, + Location.LEFT, + Location.RIGHT)); + + @Override + public TerminalSize getPreferredSize(List components) { + EnumMap layout = makeLookupMap(components); + int preferredHeight = + (layout.containsKey(Location.TOP) ? layout.get(Location.TOP).getPreferredSize().getRows() : 0) + + + Math.max( + layout.containsKey(Location.LEFT) ? layout.get(Location.LEFT).getPreferredSize().getRows() : 0, + Math.max( + layout.containsKey(Location.CENTER) ? layout.get(Location.CENTER).getPreferredSize().getRows() : 0, + layout.containsKey(Location.RIGHT) ? layout.get(Location.RIGHT).getPreferredSize().getRows() : 0)) + + + (layout.containsKey(Location.BOTTOM) ? layout.get(Location.BOTTOM).getPreferredSize().getRows() : 0); + + int preferredWidth = + Math.max( + (layout.containsKey(Location.LEFT) ? layout.get(Location.LEFT).getPreferredSize().getColumns() : 0) + + (layout.containsKey(Location.CENTER) ? layout.get(Location.CENTER).getPreferredSize().getColumns() : 0) + + (layout.containsKey(Location.RIGHT) ? layout.get(Location.RIGHT).getPreferredSize().getColumns() : 0), + Math.max( + layout.containsKey(Location.TOP) ? layout.get(Location.TOP).getPreferredSize().getColumns() : 0, + layout.containsKey(Location.BOTTOM) ? layout.get(Location.BOTTOM).getPreferredSize().getColumns() : 0)); + return new TerminalSize(preferredWidth, preferredHeight); + } + + @Override + public void doLayout(TerminalSize area, List components) { + EnumMap layout = makeLookupMap(components); + int availableHorizontalSpace = area.getColumns(); + int availableVerticalSpace = area.getRows(); + + //We'll need this later on + int topComponentHeight = 0; + int leftComponentWidth = 0; + + //First allocate the top + if (layout.containsKey(Location.TOP)) { + Component topComponent = layout.get(Location.TOP); + topComponentHeight = Math.min(topComponent.getPreferredSize().getRows(), availableVerticalSpace); + topComponent.setPosition(TerminalPosition.TOP_LEFT_CORNER); + topComponent.setSize(new TerminalSize(availableHorizontalSpace, topComponentHeight)); + availableVerticalSpace -= topComponentHeight; + } + + //Next allocate the bottom + if (layout.containsKey(Location.BOTTOM)) { + Component bottomComponent = layout.get(Location.BOTTOM); + int bottomComponentHeight = Math.min(bottomComponent.getPreferredSize().getRows(), availableVerticalSpace); + bottomComponent.setPosition(new TerminalPosition(0, area.getRows() - bottomComponentHeight)); + bottomComponent.setSize(new TerminalSize(availableHorizontalSpace, bottomComponentHeight)); + availableVerticalSpace -= bottomComponentHeight; + } + + //Now divide the remaining space between LEFT, CENTER and RIGHT + if (layout.containsKey(Location.LEFT)) { + Component leftComponent = layout.get(Location.LEFT); + leftComponentWidth = Math.min(leftComponent.getPreferredSize().getColumns(), availableHorizontalSpace); + leftComponent.setPosition(new TerminalPosition(0, topComponentHeight)); + leftComponent.setSize(new TerminalSize(leftComponentWidth, availableVerticalSpace)); + availableHorizontalSpace -= leftComponentWidth; + } + if (layout.containsKey(Location.RIGHT)) { + Component rightComponent = layout.get(Location.RIGHT); + int rightComponentWidth = Math.min(rightComponent.getPreferredSize().getColumns(), availableHorizontalSpace); + rightComponent.setPosition(new TerminalPosition(area.getColumns() - rightComponentWidth, topComponentHeight)); + rightComponent.setSize(new TerminalSize(rightComponentWidth, availableVerticalSpace)); + availableHorizontalSpace -= rightComponentWidth; + } + if (layout.containsKey(Location.CENTER)) { + Component centerComponent = layout.get(Location.CENTER); + centerComponent.setPosition(new TerminalPosition(leftComponentWidth, topComponentHeight)); + centerComponent.setSize(new TerminalSize(availableHorizontalSpace, availableVerticalSpace)); + } + + //Set the remaining components to 0x0 + for (Component component : components) { + if (component.isVisible() && !layout.containsValue(component)) { + component.setPosition(TerminalPosition.TOP_LEFT_CORNER); + component.setSize(TerminalSize.ZERO); + } + } + } + + private EnumMap makeLookupMap(List components) { + EnumMap map = new EnumMap<>(Location.class); + List unassignedComponents = new ArrayList<>(); + for (Component component : components) { + if (!component.isVisible()) { + continue; + } + if (component.getLayoutData() instanceof Location) { + map.put((Location) component.getLayoutData(), component); + } else { + unassignedComponents.add(component); + } + } + //Try to assign components to available locations + for (Component component : unassignedComponents) { + for (Location location : AUTO_ASSIGN_ORDER) { + if (!map.containsKey(location)) { + map.put(location, component); + break; + } + } + } + return map; + } + + @Override + public boolean hasChanged() { + //No internal state + return false; + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Borders.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Borders.java new file mode 100644 index 0000000000000000000000000000000000000000..0d192d667febff221d98f30848dd13ce7d8754cb --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Borders.java @@ -0,0 +1,647 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.*; +import com.googlecode.lanterna.graphics.TextGraphics; +import com.googlecode.lanterna.graphics.Theme; +import com.googlecode.lanterna.graphics.ThemeDefinition; + +import java.util.Arrays; +import java.util.List; + +/** + * This class containers a couple of border implementation and utility methods for instantiating them. It also contains + * a utility method for joining border line graphics together with adjacent lines so they blend in together: + * {@code joinLinesWithFrame(..)}. + * + * @author Martin + */ +public class Borders { + private Borders() { + } + + //Different ways to draw the border + private enum BorderStyle { + Solid, + Bevel, + ReverseBevel, + } + + /** + * Creates a {@code Border} that is drawn as a solid color single line surrounding the wrapped component + * + * @return New solid color single line {@code Border} + */ + public static Border singleLine() { + return singleLine(""); + } + + /** + * Creates a {@code Border} that is drawn as a solid color single line surrounding the wrapped component with a + * title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New solid color single line {@code Border} with a title + */ + public static Border singleLine(String title) { + return new SingleLine(title, BorderStyle.Solid); + } + + /** + * Creates a {@code Border} that is drawn as a bevel color single line surrounding the wrapped component + * + * @return New bevel color single line {@code Border} + */ + public static Border singleLineBevel() { + return singleLineBevel(""); + } + + /** + * Creates a {@code Border} that is drawn as a bevel color single line surrounding the wrapped component with a + * title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New bevel color single line {@code Border} with a title + */ + public static Border singleLineBevel(String title) { + return new SingleLine(title, BorderStyle.Bevel); + } + + /** + * Creates a {@code Border} that is drawn as a reverse bevel color single line surrounding the wrapped component + * + * @return New reverse bevel color single line {@code Border} + */ + public static Border singleLineReverseBevel() { + return singleLineReverseBevel(""); + } + + /** + * Creates a {@code Border} that is drawn as a reverse bevel color single line surrounding the wrapped component + * with a title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New reverse bevel color single line {@code Border} with a title + */ + public static Border singleLineReverseBevel(String title) { + return new SingleLine(title, BorderStyle.ReverseBevel); + } + + /** + * Creates a {@code Border} that is drawn as a solid color double line surrounding the wrapped component + * + * @return New solid color double line {@code Border} + */ + public static Border doubleLine() { + return doubleLine(""); + } + + /** + * Creates a {@code Border} that is drawn as a solid color double line surrounding the wrapped component with a + * title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New solid color double line {@code Border} with a title + */ + public static Border doubleLine(String title) { + return new DoubleLine(title, BorderStyle.Solid); + } + + /** + * Creates a {@code Border} that is drawn as a bevel color double line surrounding the wrapped component + * + * @return New bevel color double line {@code Border} + */ + public static Border doubleLineBevel() { + return doubleLineBevel(""); + } + + /** + * Creates a {@code Border} that is drawn as a bevel color double line surrounding the wrapped component with a + * title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New bevel color double line {@code Border} with a title + */ + public static Border doubleLineBevel(String title) { + return new DoubleLine(title, BorderStyle.Bevel); + } + + /** + * Creates a {@code Border} that is drawn as a reverse bevel color double line surrounding the wrapped component + * + * @return New reverse bevel color double line {@code Border} + */ + public static Border doubleLineReverseBevel() { + return doubleLineReverseBevel(""); + } + + /** + * Creates a {@code Border} that is drawn as a reverse bevel color double line surrounding the wrapped component + * with a title string normally drawn at the top-left side + * + * @param title The title to draw on the border + * @return New reverse bevel color double line {@code Border} with a title + */ + public static Border doubleLineReverseBevel(String title) { + return new DoubleLine(title, BorderStyle.ReverseBevel); + } + + private static abstract class StandardBorder extends AbstractBorder { + private final String title; + protected final BorderStyle borderStyle; + + protected StandardBorder(String title, BorderStyle borderStyle) { + if (title == null) { + throw new IllegalArgumentException("Cannot create a border with null title"); + } + this.borderStyle = borderStyle; + this.title = title; + } + + public String getTitle() { + return title; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + title + "}"; + } + } + + private static abstract class AbstractBorderRenderer implements Border.BorderRenderer { + private final BorderStyle borderStyle; + + protected AbstractBorderRenderer(BorderStyle borderStyle) { + this.borderStyle = borderStyle; + } + + @Override + public TerminalSize getPreferredSize(Border component) { + StandardBorder border = (StandardBorder) component; + Component wrappedComponent = border.getComponent(); + TerminalSize preferredSize; + if (wrappedComponent == null) { + preferredSize = TerminalSize.ZERO; + } else { + preferredSize = wrappedComponent.getPreferredSize(); + } + preferredSize = preferredSize.withRelativeColumns(2).withRelativeRows(2); + String borderTitle = border.getTitle(); + return preferredSize.max(new TerminalSize((borderTitle.isEmpty() ? 2 : TerminalTextUtils.getColumnWidth(borderTitle) + 4), 2)); + } + + @Override + public TerminalPosition getWrappedComponentTopLeftOffset() { + return TerminalPosition.OFFSET_1x1; + } + + @Override + public TerminalSize getWrappedComponentSize(TerminalSize borderSize) { + return borderSize + .withRelativeColumns(-Math.min(2, borderSize.getColumns())) + .withRelativeRows(-Math.min(2, borderSize.getRows())); + } + + @Override + public void drawComponent(TextGUIGraphics graphics, Border component) { + StandardBorder border = (StandardBorder) component; + Component wrappedComponent = border.getComponent(); + if (wrappedComponent == null) { + return; + } + TerminalSize drawableArea = graphics.getSize(); + + char horizontalLine = getHorizontalLine(component.getTheme()); + char verticalLine = getVerticalLine(component.getTheme()); + char bottomLeftCorner = getBottomLeftCorner(component.getTheme()); + char topLeftCorner = getTopLeftCorner(component.getTheme()); + char bottomRightCorner = getBottomRightCorner(component.getTheme()); + char topRightCorner = getTopRightCorner(component.getTheme()); + char titleLeft = getTitleLeft(component.getTheme()); + char titleRight = getTitleRight(component.getTheme()); + + ThemeDefinition themeDefinition = component.getTheme().getDefinition(AbstractBorder.class); + if (borderStyle == BorderStyle.Bevel) { + graphics.applyThemeStyle(themeDefinition.getPreLight()); + } else { + graphics.applyThemeStyle(themeDefinition.getNormal()); + } + graphics.setCharacter(0, drawableArea.getRows() - 1, bottomLeftCorner); + if (drawableArea.getRows() > 2) { + graphics.drawLine(new TerminalPosition(0, drawableArea.getRows() - 2), new TerminalPosition(0, 1), verticalLine); + } + graphics.setCharacter(0, 0, topLeftCorner); + if (drawableArea.getColumns() > 2) { + graphics.drawLine(new TerminalPosition(1, 0), new TerminalPosition(drawableArea.getColumns() - 2, 0), horizontalLine); + } + + if (borderStyle == BorderStyle.ReverseBevel) { + graphics.applyThemeStyle(themeDefinition.getPreLight()); + } else { + graphics.applyThemeStyle(themeDefinition.getNormal()); + } + graphics.setCharacter(drawableArea.getColumns() - 1, 0, topRightCorner); + if (drawableArea.getRows() > 2) { + graphics.drawLine(new TerminalPosition(drawableArea.getColumns() - 1, 1), + new TerminalPosition(drawableArea.getColumns() - 1, drawableArea.getRows() - 2), + verticalLine); + } + graphics.setCharacter(drawableArea.getColumns() - 1, drawableArea.getRows() - 1, bottomRightCorner); + if (drawableArea.getColumns() > 2) { + graphics.drawLine(new TerminalPosition(1, drawableArea.getRows() - 1), + new TerminalPosition(drawableArea.getColumns() - 2, drawableArea.getRows() - 1), + horizontalLine); + } + + + if (border.getTitle() != null && !border.getTitle().isEmpty() && + drawableArea.getColumns() >= TerminalTextUtils.getColumnWidth(border.getTitle()) + 4) { + graphics.applyThemeStyle(themeDefinition.getActive()); + graphics.putString(2, 0, border.getTitle()); + + if (borderStyle == BorderStyle.Bevel) { + graphics.applyThemeStyle(themeDefinition.getPreLight()); + } else { + graphics.applyThemeStyle(themeDefinition.getNormal()); + } + graphics.setCharacter(1, 0, titleLeft); + graphics.setCharacter(2 + TerminalTextUtils.getColumnWidth(border.getTitle()), 0, titleRight); + } + + wrappedComponent.draw(graphics.newTextGraphics(getWrappedComponentTopLeftOffset(), getWrappedComponentSize(drawableArea))); + joinLinesWithFrame(graphics); + } + + protected abstract char getHorizontalLine(Theme theme); + + protected abstract char getVerticalLine(Theme theme); + + protected abstract char getBottomLeftCorner(Theme theme); + + protected abstract char getTopLeftCorner(Theme theme); + + protected abstract char getBottomRightCorner(Theme theme); + + protected abstract char getTopRightCorner(Theme theme); + + protected abstract char getTitleLeft(Theme theme); + + protected abstract char getTitleRight(Theme theme); + } + + /** + * This method will attempt to join line drawing characters with the outermost bottom and top rows and left and + * right columns. For example, if a vertical left border character is ║ and the character immediately to the right + * of it is ─, then the border character will be updated to ╟ to join the two together. Please note that this method + * will only join the outer border columns and rows. + * + * @param graphics Graphics to use when inspecting and joining characters + */ + public static void joinLinesWithFrame(TextGraphics graphics) { + TerminalSize drawableArea = graphics.getSize(); + if (drawableArea.getRows() <= 2 || drawableArea.getColumns() <= 2) { + //Too small + return; + } + + int upperRow = 0; + int lowerRow = drawableArea.getRows() - 1; + int leftRow = 0; + int rightRow = drawableArea.getColumns() - 1; + + List junctionFromBelowSingle = Arrays.asList( + Symbols.SINGLE_LINE_VERTICAL, + Symbols.BOLD_FROM_NORMAL_SINGLE_LINE_VERTICAL, + Symbols.BOLD_SINGLE_LINE_VERTICAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_UP); + List junctionFromBelowDouble = Arrays.asList( + Symbols.DOUBLE_LINE_VERTICAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_UP); + List junctionFromAboveSingle = Arrays.asList( + Symbols.SINGLE_LINE_VERTICAL, + Symbols.BOLD_TO_NORMAL_SINGLE_LINE_VERTICAL, + Symbols.BOLD_SINGLE_LINE_VERTICAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_TOP_LEFT_CORNER, + Symbols.SINGLE_LINE_TOP_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN); + List junctionFromAboveDouble = Arrays.asList( + Symbols.DOUBLE_LINE_VERTICAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_TOP_LEFT_CORNER, + Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN); + List junctionFromLeftSingle = Arrays.asList( + Symbols.SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_TO_NORMAL_SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_SINGLE_LINE_HORIZONTAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.SINGLE_LINE_TOP_LEFT_CORNER, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_RIGHT, + Symbols.SINGLE_LINE_T_DOUBLE_UP, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_RIGHT); + List junctionFromLeftDouble = Arrays.asList( + Symbols.DOUBLE_LINE_HORIZONTAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER, + Symbols.DOUBLE_LINE_TOP_LEFT_CORNER, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_RIGHT, + Symbols.DOUBLE_LINE_T_SINGLE_UP, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_RIGHT); + List junctionFromRightSingle = Arrays.asList( + Symbols.SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_FROM_NORMAL_SINGLE_LINE_HORIZONTAL, + Symbols.BOLD_SINGLE_LINE_HORIZONTAL, + Symbols.SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS, + Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.SINGLE_LINE_TOP_RIGHT_CORNER, + Symbols.SINGLE_LINE_T_UP, + Symbols.SINGLE_LINE_T_DOWN, + Symbols.SINGLE_LINE_T_LEFT, + Symbols.SINGLE_LINE_T_DOUBLE_UP, + Symbols.SINGLE_LINE_T_DOUBLE_DOWN, + Symbols.DOUBLE_LINE_T_SINGLE_LEFT); + List junctionFromRightDouble = Arrays.asList( + Symbols.DOUBLE_LINE_HORIZONTAL, + Symbols.DOUBLE_LINE_CROSS, + Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS, + Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER, + Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER, + Symbols.DOUBLE_LINE_T_UP, + Symbols.DOUBLE_LINE_T_DOWN, + Symbols.DOUBLE_LINE_T_LEFT, + Symbols.DOUBLE_LINE_T_SINGLE_UP, + Symbols.DOUBLE_LINE_T_SINGLE_DOWN, + Symbols.SINGLE_LINE_T_DOUBLE_LEFT); + + //Go horizontally and check vertical neighbours if it's possible to extend lines into the border + for (int column = 1; column < drawableArea.getColumns() - 1; column++) { + //Check first row + TextCharacter borderCharacter = graphics.getCharacter(column, upperRow); + if (borderCharacter == null) { + continue; + } + TextCharacter neighbourCharacter = graphics.getCharacter(column, upperRow + 1); + if (neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacterString().charAt(0); + if (borderCharacter.is(Symbols.SINGLE_LINE_HORIZONTAL)) { + if (junctionFromBelowSingle.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOWN)); + } else if (junctionFromBelowDouble.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_DOWN)); + } + } else if (borderCharacter.is(Symbols.DOUBLE_LINE_HORIZONTAL)) { + if (junctionFromBelowSingle.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_DOWN)); + } else if (junctionFromBelowDouble.contains(neighbour)) { + graphics.setCharacter(column, upperRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_DOWN)); + } + } + } + + //Check last row + borderCharacter = graphics.getCharacter(column, lowerRow); + if (borderCharacter == null) { + continue; + } + neighbourCharacter = graphics.getCharacter(column, lowerRow - 1); + if (neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacterString().charAt(0); + if (borderCharacter.is(Symbols.SINGLE_LINE_HORIZONTAL)) { + if (junctionFromAboveSingle.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_UP)); + } else if (junctionFromAboveDouble.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_UP)); + } + } else if (borderCharacter.is(Symbols.DOUBLE_LINE_HORIZONTAL)) { + if (junctionFromAboveSingle.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_UP)); + } else if (junctionFromAboveDouble.contains(neighbour)) { + graphics.setCharacter(column, lowerRow, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_UP)); + } + } + } + } + + //Go vertically and check horizontal neighbours if it's possible to extend lines into the border + for (int row = 1; row < drawableArea.getRows() - 1; row++) { + //Check first column + TextCharacter borderCharacter = graphics.getCharacter(leftRow, row); + if (borderCharacter == null) { + continue; + } + TextCharacter neighbourCharacter = graphics.getCharacter(leftRow + 1, row); + if (neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacterString().charAt(0); + if (borderCharacter.is(Symbols.SINGLE_LINE_VERTICAL)) { + if (junctionFromRightSingle.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_RIGHT)); + } else if (junctionFromRightDouble.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_RIGHT)); + } + } else if (borderCharacter.is(Symbols.DOUBLE_LINE_VERTICAL)) { + if (junctionFromRightSingle.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_RIGHT)); + } else if (junctionFromRightDouble.contains(neighbour)) { + graphics.setCharacter(leftRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_RIGHT)); + } + } + } + + //Check last column + borderCharacter = graphics.getCharacter(rightRow, row); + if (borderCharacter == null) { + continue; + } + neighbourCharacter = graphics.getCharacter(rightRow - 1, row); + if (neighbourCharacter != null) { + char neighbour = neighbourCharacter.getCharacterString().charAt(0); + if (borderCharacter.is(Symbols.SINGLE_LINE_VERTICAL)) { + if (junctionFromLeftSingle.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_LEFT)); + } else if (junctionFromLeftDouble.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.SINGLE_LINE_T_DOUBLE_LEFT)); + } + } else if (borderCharacter.is(Symbols.DOUBLE_LINE_VERTICAL)) { + if (junctionFromLeftSingle.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_SINGLE_LEFT)); + } else if (junctionFromLeftDouble.contains(neighbour)) { + graphics.setCharacter(rightRow, row, borderCharacter.withCharacter(Symbols.DOUBLE_LINE_T_LEFT)); + } + } + } + } + } + + private static class SingleLine extends StandardBorder { + private SingleLine(String title, BorderStyle borderStyle) { + super(title, borderStyle); + } + + @Override + protected BorderRenderer createDefaultRenderer() { + return new SingleLineRenderer(borderStyle); + } + } + + private static class SingleLineRenderer extends AbstractBorderRenderer { + public SingleLineRenderer(BorderStyle borderStyle) { + super(borderStyle); + } + + @Override + protected char getTopRightCorner(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("TOP_RIGHT_CORNER", Symbols.SINGLE_LINE_TOP_RIGHT_CORNER); + } + + @Override + protected char getBottomRightCorner(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER); + } + + @Override + protected char getTopLeftCorner(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("TOP_LEFT_CORNER", Symbols.SINGLE_LINE_TOP_LEFT_CORNER); + } + + @Override + protected char getBottomLeftCorner(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER); + } + + @Override + protected char getVerticalLine(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("VERTICAL_LINE", Symbols.SINGLE_LINE_VERTICAL); + } + + @Override + protected char getHorizontalLine(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("HORIZONTAL_LINE", Symbols.SINGLE_LINE_HORIZONTAL); + } + + @Override + protected char getTitleLeft(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("TITLE_LEFT", Symbols.SINGLE_LINE_HORIZONTAL); + } + + @Override + protected char getTitleRight(Theme theme) { + return theme.getDefinition(SingleLine.class).getCharacter("TITLE_RIGHT", Symbols.SINGLE_LINE_HORIZONTAL); + } + } + + private static class DoubleLine extends StandardBorder { + private DoubleLine(String title, BorderStyle borderStyle) { + super(title, borderStyle); + } + + @Override + protected BorderRenderer createDefaultRenderer() { + return new DoubleLineRenderer(borderStyle); + } + } + + private static class DoubleLineRenderer extends AbstractBorderRenderer { + public DoubleLineRenderer(BorderStyle borderStyle) { + super(borderStyle); + } + + @Override + protected char getTopRightCorner(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("TOP_RIGHT_CORNER", Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER); + } + + @Override + protected char getBottomRightCorner(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("BOTTOM_RIGHT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER); + } + + @Override + protected char getTopLeftCorner(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("TOP_LEFT_CORNER", Symbols.DOUBLE_LINE_TOP_LEFT_CORNER); + } + + @Override + protected char getBottomLeftCorner(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("BOTTOM_LEFT_CORNER", Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER); + } + + @Override + protected char getVerticalLine(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("VERTICAL_LINE", Symbols.DOUBLE_LINE_VERTICAL); + } + + @Override + protected char getHorizontalLine(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("HORIZONTAL_LINE", Symbols.DOUBLE_LINE_HORIZONTAL); + } + + @Override + protected char getTitleLeft(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("TITLE_LEFT", Symbols.DOUBLE_LINE_HORIZONTAL); + } + + @Override + protected char getTitleRight(Theme theme) { + return theme.getDefinition(DoubleLine.class).getCharacter("TITLE_RIGHT", Symbols.DOUBLE_LINE_HORIZONTAL); + } + } +} diff --git a/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Button.java b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Button.java new file mode 100644 index 0000000000000000000000000000000000000000..d6314d8554231f6d984c50da944ba5d809dba092 --- /dev/null +++ b/mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Button.java @@ -0,0 +1,298 @@ +/* + * This file is part of lanterna (https://github.com/mabe02/lanterna). + * + * lanterna is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright (C) 2010-2020 Martin Berglund + */ +package com.googlecode.lanterna.gui2; + +import com.googlecode.lanterna.Symbols; +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TerminalTextUtils; +import com.googlecode.lanterna.graphics.ThemeDefinition; +import com.googlecode.lanterna.input.KeyStroke; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Simple labeled button that the user can trigger by pressing the Enter or the Spacebar key on the keyboard when the + * component is in focus. You can specify an initial action through one of the constructors and you can also add + * additional actions to the button using {@link #addListener(Listener)}. To remove a previously attached action, use + * {@link #removeListener(Listener)}. + * + * @author Martin + */ +public class Button extends AbstractInteractableComponent