From b7c344b9c67d59d7bae0b2c0e5b7684e6867b4a1 Mon Sep 17 00:00:00 2001 From: "C. Alexander Leigh" Date: Mon, 13 Feb 2023 13:42:58 -0800 Subject: [PATCH] Forked Lanterna 3.1.1 --- lc-pips-tc/build.gradle | 2 +- .../lc/pips/tc/AlternativeController.java | 21 +- .../lc/pips/tc/ScreenDecorationRenderer.java | 126 ++ .../main/java/lc/pips/tc/ScreenWindow.java | 28 + .../lc/pips/tc/TerminalControllerService.java | 2 +- mabe-lanterna/.gitattributes | 1 + mabe-lanterna/.gitignore | 7 + mabe-lanterna/CHANGELOG.md | 281 +++++ mabe-lanterna/License.txt | 165 +++ mabe-lanterna/README.md | 69 ++ mabe-lanterna/build.gradle | 25 + mabe-lanterna/docs/GUIGuideComponents.md | 13 + mabe-lanterna/docs/GUIGuideDialogs.md | 9 + mabe-lanterna/docs/GUIGuideMisc.md | 6 + mabe-lanterna/docs/GUIGuideStartTheGUI.md | 42 + mabe-lanterna/docs/GUIGuideWindows.md | 182 +++ mabe-lanterna/docs/Maven.md | 38 + mabe-lanterna/docs/Screenshots.md | 14 + mabe-lanterna/docs/contents.md | 76 ++ .../docs/examples/gui/action_list_box.md | 26 + .../docs/examples/gui/action_list_dialogs.md | 49 + .../examples/gui/basic_form_submission.md | 144 +++ mabe-lanterna/docs/examples/gui/buttons.md | 29 + .../docs/examples/gui/check_boxes.md | 33 + .../docs/examples/gui/combo_boxes.md | 53 + .../docs/examples/gui/component_sizing.md | 22 + .../docs/examples/gui/dir_dialogs.md | 39 + .../docs/examples/gui/file_dialogs.md | 37 + .../docs/examples/gui/hello_world.md | 56 + mabe-lanterna/docs/examples/gui/labels.md | 21 + .../docs/examples/gui/layout_managers.md | 67 ++ mabe-lanterna/docs/examples/gui/menus.md | 71 ++ .../docs/examples/gui/message_dialogs.md | 70 ++ mabe-lanterna/docs/examples/gui/panels.md | 51 + .../docs/examples/gui/radio_boxes.md | 30 + .../gui/screenshots/action_list_box.png | Bin 0 -> 40492 bytes .../gui/screenshots/action_list_dialogs.png | Bin 0 -> 27327 bytes .../docs/examples/gui/screenshots/buttons.png | Bin 0 -> 13620 bytes .../examples/gui/screenshots/calculator.png | Bin 0 -> 25408 bytes .../examples/gui/screenshots/check_boxes.png | Bin 0 -> 41093 bytes .../examples/gui/screenshots/combo_box.png | Bin 0 -> 10671 bytes .../gui/screenshots/combo_box_activated.png | Bin 0 -> 24652 bytes .../examples/gui/screenshots/dir_dialogs.png | Bin 0 -> 20698 bytes .../examples/gui/screenshots/file_dialogs.png | Bin 0 -> 62258 bytes .../examples/gui/screenshots/hello_world.png | Bin 0 -> 20057 bytes .../docs/examples/gui/screenshots/labels.png | Bin 0 -> 11439 bytes .../docs/examples/gui/screenshots/menus.png | Bin 0 -> 9917 bytes .../gui/screenshots/message_dialogs.png | Bin 0 -> 19032 bytes .../screenshots/multiline_input_dialogs.png | Bin 0 -> 27084 bytes .../docs/examples/gui/screenshots/panels.png | Bin 0 -> 19440 bytes .../examples/gui/screenshots/radio_boxes.png | Bin 0 -> 43858 bytes .../docs/examples/gui/screenshots/tables.png | Bin 0 -> 25217 bytes .../examples/gui/screenshots/text_boxes.png | Bin 0 -> 13115 bytes .../gui/screenshots/text_input_dialogs.png | Bin 0 -> 21135 bytes mabe-lanterna/docs/examples/gui/tables.md | 42 + mabe-lanterna/docs/examples/gui/text_boxes.md | 55 + .../docs/examples/gui/text_input_dialogs.md | 68 ++ mabe-lanterna/docs/examples/gui/windows.md | 46 + .../docs/examples/terminal/overview.md | 36 + mabe-lanterna/docs/introduction.md | 81 ++ mabe-lanterna/docs/tutorial/Tutorial01.md | 173 +++ mabe-lanterna/docs/tutorial/Tutorial02.md | 131 ++ mabe-lanterna/docs/tutorial/Tutorial03.md | 191 +++ mabe-lanterna/docs/tutorial/Tutorial04.md | 176 +++ mabe-lanterna/docs/using-gui.md | 11 + mabe-lanterna/docs/using-screen.md | 106 ++ mabe-lanterna/docs/using-terminal.md | 243 ++++ mabe-lanterna/licenseheader.txt | 18 + mabe-lanterna/native-integration/pom.xml | 35 + .../terminal/NativeGNULinuxTerminal.java | 126 ++ .../lanterna/terminal/PosixLibC.java | 125 ++ .../googlecode/lanterna/terminal/WinDef.java | 142 +++ .../googlecode/lanterna/terminal/Wincon.java | 44 + .../lanterna/terminal/WindowsTerminal.java | 137 +++ mabe-lanterna/pom.xml | 238 ++++ .../lanterna/examples/DrawRectangle.java | 40 + .../lanterna/examples/OutputChar.java | 35 + .../lanterna/examples/OutputString.java | 31 + .../java/com/googlecode/lanterna/SGR.java | 75 ++ .../java/com/googlecode/lanterna/Symbols.java | 328 +++++ .../googlecode/lanterna/TerminalPosition.java | 248 ++++ .../lanterna/TerminalRectangle.java | 125 ++ .../com/googlecode/lanterna/TerminalSize.java | 218 ++++ .../lanterna/TerminalTextUtils.java | 483 ++++++++ .../googlecode/lanterna/TextCharacter.java | 452 +++++++ .../com/googlecode/lanterna/TextColor.java | 704 +++++++++++ .../lanterna/bundle/BundleLocator.java | 116 ++ .../lanterna/bundle/DefaultTheme.java | 194 +++ .../lanterna/bundle/LanternaThemes.java | 134 +++ .../lanterna/bundle/LocalizedUIBundle.java | 43 + .../graphics/AbstractTextGraphics.java | 383 ++++++ .../lanterna/graphics/AbstractTheme.java | 462 ++++++++ .../lanterna/graphics/BasicTextImage.java | 352 ++++++ .../graphics/DefaultMutableThemeStyle.java | 123 ++ .../graphics/DefaultShapeRenderer.java | 194 +++ .../lanterna/graphics/DelegatingTheme.java | 65 + .../graphics/DelegatingThemeDefinition.java | 100 ++ .../graphics/DoublePrintingTextGraphics.java | 64 + .../lanterna/graphics/NullTextGraphics.java | 275 +++++ .../lanterna/graphics/PropertyTheme.java | 95 ++ .../lanterna/graphics/Scrollable.java | 47 + .../lanterna/graphics/ShapeRenderer.java | 41 + .../lanterna/graphics/SimpleTheme.java | 421 +++++++ .../lanterna/graphics/StyleSet.java | 161 +++ .../lanterna/graphics/SubTextGraphics.java | 68 ++ .../lanterna/graphics/TextGraphics.java | 472 ++++++++ .../lanterna/graphics/TextGraphicsWriter.java | 320 +++++ .../lanterna/graphics/TextImage.java | 137 +++ .../googlecode/lanterna/graphics/Theme.java | 64 + .../lanterna/graphics/ThemeDefinition.java | 133 +++ .../lanterna/graphics/ThemeStyle.java | 55 + .../lanterna/graphics/ThemedTextGraphics.java | 35 + .../lanterna/gui2/AbsoluteLayout.java | 56 + .../lanterna/gui2/AbstractBasePane.java | 509 ++++++++ .../lanterna/gui2/AbstractBorder.java | 80 ++ .../lanterna/gui2/AbstractComponent.java | 428 +++++++ .../lanterna/gui2/AbstractComposite.java | 167 +++ .../gui2/AbstractInteractableComponent.java | 248 ++++ .../lanterna/gui2/AbstractListBox.java | 556 +++++++++ .../lanterna/gui2/AbstractTextGUI.java | 219 ++++ .../lanterna/gui2/AbstractTextGUIThread.java | 126 ++ .../lanterna/gui2/AbstractWindow.java | 337 ++++++ .../lanterna/gui2/ActionListBox.java | 138 +++ .../lanterna/gui2/AnimatedLabel.java | 184 +++ .../gui2/AsynchronousTextGUIThread.java | 83 ++ .../googlecode/lanterna/gui2/BasePane.java | 196 +++ .../lanterna/gui2/BasePaneListener.java | 55 + .../googlecode/lanterna/gui2/BasicWindow.java | 45 + .../com/googlecode/lanterna/gui2/Border.java | 48 + .../lanterna/gui2/BorderLayout.java | 193 +++ .../com/googlecode/lanterna/gui2/Borders.java | 647 ++++++++++ .../com/googlecode/lanterna/gui2/Button.java | 298 +++++ .../googlecode/lanterna/gui2/CheckBox.java | 231 ++++ .../lanterna/gui2/CheckBoxList.java | 334 ++++++ .../googlecode/lanterna/gui2/ComboBox.java | 713 +++++++++++ .../googlecode/lanterna/gui2/Component.java | 274 +++++ .../lanterna/gui2/ComponentRenderer.java | 50 + .../googlecode/lanterna/gui2/Composite.java | 42 + .../googlecode/lanterna/gui2/Container.java | 127 ++ .../lanterna/gui2/DefaultTextGUIGraphics.java | 284 +++++ .../gui2/DefaultWindowDecorationRenderer.java | 143 +++ .../lanterna/gui2/DefaultWindowManager.java | 195 +++ .../googlecode/lanterna/gui2/Direction.java | 37 + .../googlecode/lanterna/gui2/EmptySpace.java | 109 ++ .../gui2/EmptyWindowDecorationRenderer.java | 44 + .../gui2/FatWindowDecorationRenderer.java | 135 +++ .../googlecode/lanterna/gui2/GUIBackdrop.java | 46 + .../googlecode/lanterna/gui2/GridLayout.java | 868 ++++++++++++++ .../lanterna/gui2/ImageComponent.java | 76 ++ .../googlecode/lanterna/gui2/InputFilter.java | 37 + .../lanterna/gui2/Interactable.java | 233 ++++ .../lanterna/gui2/InteractableLookupMap.java | 306 +++++ .../lanterna/gui2/InteractableRenderer.java | 33 + .../com/googlecode/lanterna/gui2/Label.java | 287 +++++ .../googlecode/lanterna/gui2/LayoutData.java | 27 + .../lanterna/gui2/LayoutManager.java | 65 + .../lanterna/gui2/LinearLayout.java | 519 ++++++++ .../lanterna/gui2/LocalizedString.java | 95 ++ .../lanterna/gui2/MenuPopupWindow.java | 62 + .../lanterna/gui2/MultiWindowTextGUI.java | 589 +++++++++ .../com/googlecode/lanterna/gui2/Panel.java | 435 +++++++ .../com/googlecode/lanterna/gui2/Panels.java | 79 ++ .../googlecode/lanterna/gui2/ProgressBar.java | 450 +++++++ .../lanterna/gui2/RadioBoxList.java | 323 +++++ .../lanterna/gui2/SameTextGUIThread.java | 82 ++ .../googlecode/lanterna/gui2/ScrollBar.java | 290 +++++ .../lanterna/gui2/SeparateTextGUIThread.java | 162 +++ .../googlecode/lanterna/gui2/Separator.java | 90 ++ .../googlecode/lanterna/gui2/SplitPanel.java | 230 ++++ .../com/googlecode/lanterna/gui2/TODO.txt | 22 + .../com/googlecode/lanterna/gui2/TextBox.java | 1028 ++++++++++++++++ .../com/googlecode/lanterna/gui2/TextGUI.java | 141 +++ .../lanterna/gui2/TextGUIElement.java | 42 + .../lanterna/gui2/TextGUIGraphics.java | 149 +++ .../lanterna/gui2/TextGUIThread.java | 104 ++ .../lanterna/gui2/TextGUIThreadFactory.java | 33 + .../com/googlecode/lanterna/gui2/Window.java | 487 ++++++++ .../lanterna/gui2/WindowBasedTextGUI.java | 146 +++ .../gui2/WindowDecorationRenderer.java | 66 ++ .../googlecode/lanterna/gui2/WindowList.java | 172 +++ .../lanterna/gui2/WindowListener.java | 45 + .../lanterna/gui2/WindowListenerAdapter.java | 47 + .../lanterna/gui2/WindowManager.java | 88 ++ .../lanterna/gui2/WindowPostRenderer.java | 44 + .../lanterna/gui2/WindowShadowRenderer.java | 101 ++ .../gui2/dialogs/AbstractDialogBuilder.java | 141 +++ .../gui2/dialogs/ActionListDialog.java | 108 ++ .../gui2/dialogs/ActionListDialogBuilder.java | 175 +++ .../lanterna/gui2/dialogs/DialogWindow.java | 60 + .../gui2/dialogs/DirectoryDialog.java | 190 +++ .../gui2/dialogs/DirectoryDialogBuilder.java | 145 +++ .../lanterna/gui2/dialogs/FileDialog.java | 254 ++++ .../gui2/dialogs/FileDialogBuilder.java | 139 +++ .../gui2/dialogs/ListSelectDialog.java | 166 +++ .../gui2/dialogs/ListSelectDialogBuilder.java | 136 +++ .../lanterna/gui2/dialogs/MessageDialog.java | 108 ++ .../gui2/dialogs/MessageDialogBuilder.java | 114 ++ .../gui2/dialogs/MessageDialogButton.java | 78 ++ .../gui2/dialogs/TextInputDialog.java | 167 +++ .../gui2/dialogs/TextInputDialogBuilder.java | 167 +++ .../TextInputDialogResultValidator.java | 35 + .../lanterna/gui2/dialogs/WaitingDialog.java | 85 ++ .../googlecode/lanterna/gui2/menu/Menu.java | 113 ++ .../lanterna/gui2/menu/MenuBar.java | 211 ++++ .../lanterna/gui2/menu/MenuItem.java | 155 +++ .../gui2/table/DefaultTableCellRenderer.java | 155 +++ .../table/DefaultTableHeaderRenderer.java | 46 + .../gui2/table/DefaultTableRenderer.java | 797 +++++++++++++ .../googlecode/lanterna/gui2/table/Table.java | 542 +++++++++ .../gui2/table/TableCellBorderStyle.java | 58 + .../gui2/table/TableCellRenderer.java | 53 + .../gui2/table/TableHeaderRenderer.java | 52 + .../lanterna/gui2/table/TableModel.java | 373 ++++++ .../lanterna/gui2/table/TableRenderer.java | 87 ++ .../input/AltAndCharacterPattern.java | 45 + .../lanterna/input/BasicCharacterPattern.java | 98 ++ .../lanterna/input/CharacterPattern.java | 98 ++ .../input/CtrlAltAndCharacterPattern.java | 73 ++ .../input/CtrlAndCharacterPattern.java | 70 ++ .../input/DefaultKeyDecodingProfile.java | 64 + .../input/EscapeSequenceCharacterPattern.java | 269 +++++ .../lanterna/input/InputDecoder.java | 246 ++++ .../lanterna/input/InputProvider.java | 50 + .../lanterna/input/KeyDecodingProfile.java | 44 + .../googlecode/lanterna/input/KeyStroke.java | 400 +++++++ .../googlecode/lanterna/input/KeyType.java | 91 ++ .../lanterna/input/MouseAction.java | 102 ++ .../lanterna/input/MouseActionType.java | 38 + .../lanterna/input/MouseCharacterPattern.java | 118 ++ .../input/NormalCharacterPattern.java | 56 + .../lanterna/input/ScreenInfoAction.java | 52 + .../input/ScreenInfoCharacterPattern.java | 68 ++ .../lanterna/screen/AbstractScreen.java | 260 ++++ .../googlecode/lanterna/screen/Screen.java | 272 +++++ .../lanterna/screen/ScreenBuffer.java | 155 +++ .../lanterna/screen/ScreenTextGraphics.java | 61 + .../lanterna/screen/TabBehaviour.java | 113 ++ .../lanterna/screen/TerminalScreen.java | 428 +++++++ .../lanterna/screen/VirtualScreen.java | 357 ++++++ .../lanterna/screen/WrapBehaviour.java | 64 + .../lanterna/terminal/AbstractTerminal.java | 88 ++ .../terminal/DefaultTerminalFactory.java | 506 ++++++++ .../lanterna/terminal/ExtendedTerminal.java | 109 ++ .../terminal/IOSafeExtendedTerminal.java | 58 + .../lanterna/terminal/IOSafeTerminal.java | 100 ++ .../terminal/IOSafeTerminalAdapter.java | 434 +++++++ .../lanterna/terminal/MouseCaptureMode.java | 57 + .../SimpleTerminalResizeListener.java | 78 ++ .../lanterna/terminal/Terminal.java | 314 +++++ .../lanterna/terminal/TerminalFactory.java | 40 + .../terminal/TerminalResizeListener.java | 40 + .../terminal/TerminalTextGraphics.java | 205 ++++ .../lanterna/terminal/ansi/ANSITerminal.java | 458 +++++++ .../terminal/ansi/CygwinTerminal.java | 131 ++ .../ansi/FixedTerminalSizeProvider.java | 48 + .../terminal/ansi/StreamBasedTerminal.java | 340 ++++++ .../terminal/ansi/TelnetProtocol.java | 100 ++ .../terminal/ansi/TelnetTerminal.java | 408 +++++++ .../terminal/ansi/TelnetTerminalServer.java | 121 ++ .../terminal/ansi/UnixLikeTTYTerminal.java | 187 +++ .../terminal/ansi/UnixLikeTerminal.java | 226 ++++ .../lanterna/terminal/ansi/UnixTerminal.java | 100 ++ .../ansi/UnixTerminalSizeQuerier.java | 41 + .../lanterna/terminal/swing/AWTTerminal.java | 396 +++++++ .../swing/AWTTerminalFontConfiguration.java | 367 ++++++ .../terminal/swing/AWTTerminalFrame.java | 321 +++++ .../swing/AWTTerminalImplementation.java | 136 +++ .../GraphicalTerminalImplementation.java | 1052 +++++++++++++++++ .../terminal/swing/ScrollingAWTTerminal.java | 296 +++++ .../swing/ScrollingSwingTerminal.java | 297 +++++ .../terminal/swing/SwingTerminal.java | 387 ++++++ .../swing/SwingTerminalFontConfiguration.java | 73 ++ .../terminal/swing/SwingTerminalFrame.java | 354 ++++++ .../swing/SwingTerminalImplementation.java | 144 +++ .../TerminalEmulatorAutoCloseTrigger.java | 35 + .../TerminalEmulatorColorConfiguration.java | 80 ++ .../TerminalEmulatorDeviceConfiguration.java | 304 +++++ .../swing/TerminalEmulatorPalette.java | 475 ++++++++ .../swing/TerminalInputMethodRequests.java | 60 + .../swing/TerminalScrollController.java | 58 + .../virtual/DefaultVirtualTerminal.java | 448 +++++++ .../lanterna/terminal/virtual/TextBuffer.java | 133 +++ .../terminal/virtual/VirtualTerminal.java | 182 +++ .../virtual/VirtualTerminalListener.java | 45 + .../virtual/VirtualTerminalTextGraphics.java | 67 ++ .../lanterna/terminal/win32/WinDef.java | 139 +++ .../lanterna/terminal/win32/Wincon.java | 24 + .../win32/WindowsConsoleInputStream.java | 153 +++ .../win32/WindowsConsoleOutputStream.java | 60 + .../terminal/win32/WindowsTerminal.java | 143 +++ mabe-lanterna/src/main/java9/module-info.java | 36 + .../main/resources/bigsnake-theme.properties | 148 +++ .../main/resources/blaster-theme.properties | 140 +++ .../businessmachine-theme.properties | 98 ++ .../main/resources/conqueror-theme.properties | 166 +++ .../main/resources/default-theme.properties | 149 +++ .../main/resources/defrost-theme.properties | 127 ++ .../multilang/lanterna-ui.properties | 11 + .../multilang/lanterna-ui_da.properties | 11 + .../multilang/lanterna-ui_de.properties | 11 + .../multilang/lanterna-ui_fi.properties | 11 + .../multilang/lanterna-ui_fr.properties | 11 + .../multilang/lanterna-ui_ja.properties | 11 + .../multilang/lanterna-ui_nb.properties | 11 + .../multilang/lanterna-ui_nn.properties | 11 + .../multilang/lanterna-ui_no.properties | 11 + .../multilang/lanterna-ui_sv.properties | 11 + .../multilang/lanterna-ui_zh-CN.properties | 11 + .../multilang/lanterna-ui_zh-TW.properties | 11 + .../com/googlecode/lanterna/Environment.java | 43 + .../lanterna/TerminalTextUtilsTest.java | 549 +++++++++ .../java/com/googlecode/lanterna/TestACS.java | 58 + .../com/googlecode/lanterna/TestAllCodes.java | 37 + .../googlecode/lanterna/TestShellCommand.java | 47 + .../lanterna/TestTerminalFactory.java | 85 ++ .../com/googlecode/lanterna/TestUtils.java | 53 + .../lanterna/TextCharacterTest.java | 21 + .../lanterna/bundle/DefaultThemeTest.java | 52 + .../RedundantThemeDeclarationsTest.java | 45 + .../lanterna/gui2/ComboBoxTest.java | 105 ++ .../gui2/DialogsTextGUIBasicTest.java | 105 ++ .../lanterna/gui2/DynamicGridLayoutTest.java | 323 +++++ .../lanterna/gui2/FullScreenTextGUITest.java | 237 ++++ .../lanterna/gui2/GUIOverTelnet.java | 149 +++ .../lanterna/gui2/GridLayoutTest.java | 78 ++ .../lanterna/gui2/ImageComponentTest.java | 231 ++++ .../googlecode/lanterna/gui2/InputUITest.java | 99 ++ .../lanterna/gui2/LineWrappingLabelTest.java | 110 ++ .../lanterna/gui2/LinearLayoutTest.java | 70 ++ .../googlecode/lanterna/gui2/ListBoxTest.java | 62 + .../googlecode/lanterna/gui2/MenuTest.java | 131 ++ .../lanterna/gui2/MiscComponentTest.java | 97 ++ .../lanterna/gui2/MultiButtonTest.java | 51 + .../lanterna/gui2/MultiLabelTest.java | 73 ++ .../lanterna/gui2/MultiWindowManagerTest.java | 217 ++++ .../googlecode/lanterna/gui2/PanelTest.java | 74 ++ .../lanterna/gui2/ScrollBarTest.java | 93 ++ .../gui2/SimpleWindowManagerTest.java | 119 ++ .../lanterna/gui2/SplitPanelTest.java | 166 +++ .../googlecode/lanterna/gui2/TableTest.java | 238 ++++ .../lanterna/gui2/TableUnitTests.java | 253 ++++ .../googlecode/lanterna/gui2/TestBase.java | 75 ++ .../googlecode/lanterna/gui2/TextBoxTest.java | 63 + .../googlecode/lanterna/gui2/ThemeTest.java | 448 +++++++ .../lanterna/gui2/WelcomeSplashTest.java | 73 ++ .../lanterna/gui2/WindowManagerTest.java | 47 + .../googlecode/lanterna/issue/Issue150.java | 53 + .../googlecode/lanterna/issue/Issue155.java | 74 ++ .../googlecode/lanterna/issue/Issue190.java | 77 ++ .../googlecode/lanterna/issue/Issue212.java | 65 + .../googlecode/lanterna/issue/Issue216.java | 69 ++ .../googlecode/lanterna/issue/Issue221.java | 60 + .../googlecode/lanterna/issue/Issue249.java | 54 + .../googlecode/lanterna/issue/Issue254.java | 19 + .../googlecode/lanterna/issue/Issue261.java | 63 + .../googlecode/lanterna/issue/Issue274.java | 54 + .../googlecode/lanterna/issue/Issue305.java | 54 + .../googlecode/lanterna/issue/Issue312.java | 37 + .../googlecode/lanterna/issue/Issue313.java | 30 + .../googlecode/lanterna/issue/Issue334.java | 35 + .../googlecode/lanterna/issue/Issue358.java | 33 + .../googlecode/lanterna/issue/Issue359.java | 33 + .../googlecode/lanterna/issue/Issue361.java | 35 + .../googlecode/lanterna/issue/Issue374.java | 49 + .../googlecode/lanterna/issue/Issue376.java | 30 + .../googlecode/lanterna/issue/Issue380.java | 75 ++ .../googlecode/lanterna/issue/Issue384.java | 78 ++ .../googlecode/lanterna/issue/Issue385.java | 27 + .../googlecode/lanterna/issue/Issue387.java | 46 + .../googlecode/lanterna/issue/Issue392.java | 56 + .../googlecode/lanterna/issue/Issue409.java | 131 ++ .../googlecode/lanterna/issue/Issue446.java | 40 + .../googlecode/lanterna/issue/Issue452.java | 176 +++ .../lanterna/issue/Issue452Test.java | 316 +++++ .../googlecode/lanterna/issue/Issue453.java | 76 ++ .../googlecode/lanterna/issue/Issue460.java | 38 + .../googlecode/lanterna/issue/Issue78.java | 43 + .../googlecode/lanterna/issue/Issue95.java | 53 + .../com/googlecode/lanterna/issue/IssueX.java | 66 ++ .../lanterna/screen/CJKScreenTest.java | 57 + .../lanterna/screen/DrawImageTest.java | 80 ++ .../screen/InternationalCharactersTest.java | 53 + .../lanterna/screen/MultiScreenTest.java | 95 ++ .../lanterna/screen/ScreenClearTest.java | 128 ++ .../lanterna/screen/ScreenLineTest.java | 116 ++ .../lanterna/screen/ScreenRectangleTest.java | 90 ++ .../lanterna/screen/ScreenResizeTest.java | 139 +++ .../lanterna/screen/ScreenTabTest.java | 95 ++ .../lanterna/screen/ScreenTriangleTest.java | 129 ++ .../lanterna/screen/SimpleScreenTest.java | 128 ++ .../lanterna/screen/TerminalColorTest.java | 125 ++ .../lanterna/screen/TerminalTest.java | 113 ++ .../screen/TextGraphicsWriterTest.java | 68 ++ .../lanterna/screen/VirtualScreenTest.java | 67 ++ .../lanterna/terminal/BlinkTest.java | 53 + .../lanterna/terminal/CJKTerminalTest.java | 74 ++ .../lanterna/terminal/EmojiTest.java | 19 + .../googlecode/lanterna/terminal/EnqTest.java | 29 + .../terminal/ExtendedTerminalTests.java | 61 + .../lanterna/terminal/InitialSizeTest.java | 61 + .../lanterna/terminal/InputTest.java | 155 +++ .../googlecode/lanterna/terminal/KeyTest.java | 66 ++ .../terminal/NewSwingTerminalTest.form | 39 + .../terminal/NewSwingTerminalTest.java | 123 ++ .../lanterna/terminal/PrivateModeTest.java | 83 ++ .../lanterna/terminal/PseudoTerminal.java | 182 +++ .../lanterna/terminal/RawTerminalTest.java | 47 + .../lanterna/terminal/ResetAllTest.java | 53 + .../googlecode/lanterna/terminal/SGRTest.java | 78 ++ .../terminal/ScrollingAWTTerminalTest.java | 183 +++ .../terminal/ScrollingSwingTerminalTest.form | 123 ++ .../terminal/ScrollingSwingTerminalTest.java | 195 +++ .../lanterna/terminal/SimpleTerminalTest.java | 179 +++ .../lanterna/terminal/SwingTerminalTest.java | 115 ++ .../lanterna/terminal/TelnetTerminalTest.java | 110 ++ .../terminal/Terminal24bitColorTest.java | 67 ++ .../terminal/Terminal4bitColorTest.java | 67 ++ .../Terminal8bitIndexedColorTest.java | 69 ++ .../lanterna/terminal/TerminalInputTest.java | 73 ++ .../lanterna/terminal/TerminalResizeTest.java | 72 ++ .../terminal/TerminalTextGraphicsTest.java | 74 ++ .../virtual/DefaultVirtualTerminalTest.java | 592 ++++++++++ .../lanterna/tutorial/Tutorial01.java | 193 +++ .../lanterna/tutorial/Tutorial02.java | 146 +++ .../lanterna/tutorial/Tutorial03.java | 218 ++++ .../lanterna/tutorial/Tutorial04.java | 193 +++ settings.gradle | 2 +- 427 files changed, 59058 insertions(+), 9 deletions(-) create mode 100644 lc-pips-tc/src/main/java/lc/pips/tc/ScreenDecorationRenderer.java create mode 100644 lc-pips-tc/src/main/java/lc/pips/tc/ScreenWindow.java create mode 100644 mabe-lanterna/.gitattributes create mode 100644 mabe-lanterna/.gitignore create mode 100644 mabe-lanterna/CHANGELOG.md create mode 100644 mabe-lanterna/License.txt create mode 100644 mabe-lanterna/README.md create mode 100644 mabe-lanterna/build.gradle create mode 100644 mabe-lanterna/docs/GUIGuideComponents.md create mode 100644 mabe-lanterna/docs/GUIGuideDialogs.md create mode 100644 mabe-lanterna/docs/GUIGuideMisc.md create mode 100644 mabe-lanterna/docs/GUIGuideStartTheGUI.md create mode 100644 mabe-lanterna/docs/GUIGuideWindows.md create mode 100644 mabe-lanterna/docs/Maven.md create mode 100644 mabe-lanterna/docs/Screenshots.md create mode 100644 mabe-lanterna/docs/contents.md create mode 100644 mabe-lanterna/docs/examples/gui/action_list_box.md create mode 100644 mabe-lanterna/docs/examples/gui/action_list_dialogs.md create mode 100644 mabe-lanterna/docs/examples/gui/basic_form_submission.md create mode 100644 mabe-lanterna/docs/examples/gui/buttons.md create mode 100644 mabe-lanterna/docs/examples/gui/check_boxes.md create mode 100644 mabe-lanterna/docs/examples/gui/combo_boxes.md create mode 100644 mabe-lanterna/docs/examples/gui/component_sizing.md create mode 100644 mabe-lanterna/docs/examples/gui/dir_dialogs.md create mode 100644 mabe-lanterna/docs/examples/gui/file_dialogs.md create mode 100644 mabe-lanterna/docs/examples/gui/hello_world.md create mode 100644 mabe-lanterna/docs/examples/gui/labels.md create mode 100644 mabe-lanterna/docs/examples/gui/layout_managers.md create mode 100644 mabe-lanterna/docs/examples/gui/menus.md create mode 100644 mabe-lanterna/docs/examples/gui/message_dialogs.md create mode 100644 mabe-lanterna/docs/examples/gui/panels.md create mode 100644 mabe-lanterna/docs/examples/gui/radio_boxes.md create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/action_list_box.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/action_list_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/buttons.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/calculator.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/check_boxes.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/combo_box.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/combo_box_activated.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/dir_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/file_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/hello_world.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/labels.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/menus.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/message_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/multiline_input_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/panels.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/radio_boxes.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/tables.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/text_boxes.png create mode 100644 mabe-lanterna/docs/examples/gui/screenshots/text_input_dialogs.png create mode 100644 mabe-lanterna/docs/examples/gui/tables.md create mode 100644 mabe-lanterna/docs/examples/gui/text_boxes.md create mode 100644 mabe-lanterna/docs/examples/gui/text_input_dialogs.md create mode 100644 mabe-lanterna/docs/examples/gui/windows.md create mode 100644 mabe-lanterna/docs/examples/terminal/overview.md create mode 100644 mabe-lanterna/docs/introduction.md create mode 100644 mabe-lanterna/docs/tutorial/Tutorial01.md create mode 100644 mabe-lanterna/docs/tutorial/Tutorial02.md create mode 100644 mabe-lanterna/docs/tutorial/Tutorial03.md create mode 100644 mabe-lanterna/docs/tutorial/Tutorial04.md create mode 100644 mabe-lanterna/docs/using-gui.md create mode 100644 mabe-lanterna/docs/using-screen.md create mode 100644 mabe-lanterna/docs/using-terminal.md create mode 100644 mabe-lanterna/licenseheader.txt create mode 100644 mabe-lanterna/native-integration/pom.xml create mode 100644 mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/NativeGNULinuxTerminal.java create mode 100644 mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/PosixLibC.java create mode 100644 mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WinDef.java create mode 100644 mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/Wincon.java create mode 100644 mabe-lanterna/native-integration/src/main/java/com/googlecode/lanterna/terminal/WindowsTerminal.java create mode 100644 mabe-lanterna/pom.xml create mode 100644 mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/DrawRectangle.java create mode 100644 mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputChar.java create mode 100644 mabe-lanterna/src/examples/java/com/googlecode/lanterna/examples/OutputString.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/SGR.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/Symbols.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalPosition.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalRectangle.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalSize.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TerminalTextUtils.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TextCharacter.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/TextColor.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/BundleLocator.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/DefaultTheme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LanternaThemes.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/bundle/LocalizedUIBundle.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/AbstractTheme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/BasicTextImage.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultMutableThemeStyle.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DefaultShapeRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingTheme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DelegatingThemeDefinition.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/DoublePrintingTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/NullTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/PropertyTheme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Scrollable.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ShapeRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SimpleTheme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/StyleSet.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/SubTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextGraphicsWriter.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/TextImage.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/Theme.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeDefinition.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemeStyle.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/graphics/ThemedTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbsoluteLayout.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractBorder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComponent.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractComposite.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUI.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractTextGUIThread.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AbstractWindow.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ActionListBox.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AnimatedLabel.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/AsynchronousTextGUIThread.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePane.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasePaneListener.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BasicWindow.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Border.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/BorderLayout.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Borders.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Button.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/CheckBox.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/CheckBoxList.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ComboBox.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Component.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ComponentRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Composite.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Container.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/DefaultTextGUIGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/DefaultWindowDecorationRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/DefaultWindowManager.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Direction.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/EmptySpace.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/EmptyWindowDecorationRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/FatWindowDecorationRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/GUIBackdrop.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/GridLayout.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ImageComponent.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/InputFilter.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Interactable.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/InteractableLookupMap.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/InteractableRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Label.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/LayoutData.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/LayoutManager.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/LinearLayout.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/LocalizedString.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/MenuPopupWindow.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/MultiWindowTextGUI.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Panel.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Panels.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ProgressBar.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/RadioBoxList.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/SameTextGUIThread.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/ScrollBar.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/SeparateTextGUIThread.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Separator.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/SplitPanel.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TODO.txt create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextBox.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextGUI.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextGUIElement.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextGUIGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextGUIThread.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/TextGUIThreadFactory.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/Window.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowBasedTextGUI.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowDecorationRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowList.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowListener.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowListenerAdapter.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowManager.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowPostRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/WindowShadowRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/AbstractDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/ActionListDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/ActionListDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/DialogWindow.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/ListSelectDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/ListSelectDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/TextInputDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/TextInputDialogBuilder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/TextInputDialogResultValidator.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/dialogs/WaitingDialog.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/menu/Menu.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/menu/MenuBar.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/menu/MenuItem.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/DefaultTableCellRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/DefaultTableHeaderRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/DefaultTableRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/Table.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/TableCellBorderStyle.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/TableCellRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/TableHeaderRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/TableModel.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/gui2/table/TableRenderer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/AltAndCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/BasicCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/CharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/CtrlAltAndCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/CtrlAndCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/DefaultKeyDecodingProfile.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/EscapeSequenceCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/InputDecoder.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/InputProvider.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/KeyDecodingProfile.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/KeyStroke.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/KeyType.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/MouseAction.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/MouseActionType.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/MouseCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/NormalCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/ScreenInfoAction.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/input/ScreenInfoCharacterPattern.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/AbstractScreen.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/Screen.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/ScreenBuffer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/ScreenTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/TabBehaviour.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/TerminalScreen.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/VirtualScreen.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/screen/WrapBehaviour.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/AbstractTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/DefaultTerminalFactory.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ExtendedTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/IOSafeExtendedTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/IOSafeTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/IOSafeTerminalAdapter.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/MouseCaptureMode.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/SimpleTerminalResizeListener.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/Terminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/TerminalFactory.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/TerminalResizeListener.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/TerminalTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/ANSITerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/CygwinTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/FixedTerminalSizeProvider.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/StreamBasedTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/TelnetProtocol.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/TelnetTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/TelnetTerminalServer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/UnixLikeTTYTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/UnixLikeTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/UnixTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/ansi/UnixTerminalSizeQuerier.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/AWTTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/AWTTerminalFontConfiguration.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/AWTTerminalFrame.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/AWTTerminalImplementation.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/GraphicalTerminalImplementation.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/ScrollingAWTTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/ScrollingSwingTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/SwingTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/SwingTerminalFontConfiguration.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/SwingTerminalFrame.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/SwingTerminalImplementation.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalEmulatorAutoCloseTrigger.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalEmulatorColorConfiguration.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalEmulatorDeviceConfiguration.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalEmulatorPalette.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalInputMethodRequests.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/swing/TerminalScrollController.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/virtual/DefaultVirtualTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/virtual/TextBuffer.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/virtual/VirtualTerminal.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/virtual/VirtualTerminalListener.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/virtual/VirtualTerminalTextGraphics.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/win32/WinDef.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/win32/Wincon.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/win32/WindowsConsoleInputStream.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/win32/WindowsConsoleOutputStream.java create mode 100644 mabe-lanterna/src/main/java/com/googlecode/lanterna/terminal/win32/WindowsTerminal.java create mode 100644 mabe-lanterna/src/main/java9/module-info.java create mode 100644 mabe-lanterna/src/main/resources/bigsnake-theme.properties create mode 100644 mabe-lanterna/src/main/resources/blaster-theme.properties create mode 100644 mabe-lanterna/src/main/resources/businessmachine-theme.properties create mode 100644 mabe-lanterna/src/main/resources/conqueror-theme.properties create mode 100644 mabe-lanterna/src/main/resources/default-theme.properties create mode 100644 mabe-lanterna/src/main/resources/defrost-theme.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_da.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_de.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_fi.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_fr.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_ja.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_nb.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_nn.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_no.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_sv.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_zh-CN.properties create mode 100644 mabe-lanterna/src/main/resources/multilang/lanterna-ui_zh-TW.properties create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/Environment.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TerminalTextUtilsTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TestACS.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TestAllCodes.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TestShellCommand.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TestTerminalFactory.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TestUtils.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/TextCharacterTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/bundle/DefaultThemeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/graphics/RedundantThemeDeclarationsTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/ComboBoxTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/DialogsTextGUIBasicTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/DynamicGridLayoutTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/FullScreenTextGUITest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/GUIOverTelnet.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/GridLayoutTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/ImageComponentTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/InputUITest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/LineWrappingLabelTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/LinearLayoutTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/ListBoxTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/MenuTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/MiscComponentTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/MultiButtonTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/MultiLabelTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/MultiWindowManagerTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/PanelTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/ScrollBarTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/SimpleWindowManagerTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/SplitPanelTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/TableTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/TableUnitTests.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/TestBase.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/TextBoxTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/ThemeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/WelcomeSplashTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/gui2/WindowManagerTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue150.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue155.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue190.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue212.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue216.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue221.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue249.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue254.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue261.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue274.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue305.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue312.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue313.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue334.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue358.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue359.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue361.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue374.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue376.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue380.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue384.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue385.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue387.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue392.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue409.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue446.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue452.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue452Test.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue453.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue460.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue78.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/Issue95.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/issue/IssueX.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/CJKScreenTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/DrawImageTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/InternationalCharactersTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/MultiScreenTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenClearTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenLineTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenRectangleTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenResizeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenTabTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/ScreenTriangleTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/SimpleScreenTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/TerminalColorTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/TerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/TextGraphicsWriterTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/screen/VirtualScreenTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/BlinkTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/CJKTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/EmojiTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/EnqTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/ExtendedTerminalTests.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/InitialSizeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/InputTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/KeyTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/NewSwingTerminalTest.form create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/NewSwingTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/PrivateModeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/PseudoTerminal.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/RawTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/ResetAllTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/SGRTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/ScrollingAWTTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/ScrollingSwingTerminalTest.form create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/ScrollingSwingTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/SimpleTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/SwingTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/TelnetTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/Terminal24bitColorTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/Terminal4bitColorTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/Terminal8bitIndexedColorTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/TerminalInputTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/TerminalResizeTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/TerminalTextGraphicsTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/terminal/virtual/DefaultVirtualTerminalTest.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/tutorial/Tutorial01.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/tutorial/Tutorial02.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/tutorial/Tutorial03.java create mode 100644 mabe-lanterna/src/test/java/com/googlecode/lanterna/tutorial/Tutorial04.java diff --git a/lc-pips-tc/build.gradle b/lc-pips-tc/build.gradle index 4ec1eef15..576a68802 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 739daee19..068533449 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 000000000..9b9050e5c --- /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 000000000..b5e34e499 --- /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 80bb9d9c0..b6a7e9023 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 000000000..589c67721 --- /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 000000000..9d0b97e6a --- /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 000000000..6866ccf3c --- /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 000000000..02bbb60bc --- /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 000000000..cfb22eba9 --- /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 000000000..e443f5146 --- /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 000000000..fab811549 --- /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 000000000..21972ee3c --- /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 000000000..238596fea --- /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 000000000..595f24c77 --- /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 000000000..d3d23c254 --- /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 000000000..9bffc66b4 --- /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 000000000..c10dbeaa4 --- /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 000000000..787dee7f8 --- /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 000000000..11a757558 --- /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 000000000..11b97c28b --- /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 000000000..591b7a8be --- /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 000000000..49eb6f70f --- /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 000000000..b8e274554 --- /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 000000000..a2fae3718 --- /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 000000000..f6f4af374 --- /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 000000000..6c0144512 --- /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 000000000..ac78efe0a --- /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 000000000..7ecab8185 --- /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 000000000..bef27f226 --- /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 000000000..cfce6fd02 --- /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 000000000..319631ff5 --- /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 000000000..faf020d5a --- /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 000000000..2c3fea623 --- /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 000000000..ae3e07a19 --- /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 GIT binary patch literal 40492 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 z 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�@=x^pniF>6=KA2* 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 literal 0 HcmV?d00001 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 GIT binary patch literal 27327 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 Gj`}x?M|;?c( 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 literal 0 HcmV?d00001 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 GIT binary patch literal 25408 zcmb@tbySpX*EdYcAT@vz(k>3jKmx#FRtd7f+1 z>!b6-D70=vL?AwGOq7r3{s@&&Zo42k`qX2rLKF-k9LrJb^@kD+^l~U^A8z*!cf~$M zdGtakw+dG7^dE`NLxfONFnYe`L3~JhVo<+K&Y)RQqrkW-rgHOHz$?vQb{slclvgi! zHPWr|cr`MtpT(+sy%X+{LP@UV$l!U5BI+JwCnf&4FNIjf`Rn@rIe0H@Ix)X*S!N#R z;9X6P^Q+ZLFa=8}Da(l)Z)(5IN2AIWqnvn!0jV~KEVI+@o$>2CPUGz2529XHC1u#? z^u40pEt4-^V8!$Pe*Nh%wHrZa5OGeK#PET1WzL~EEKBo$?4{&0*N zYz_bZtsB!}++F9by|NUy5U#c8t5y&G;t1v7J<@mnF^MxE#T4t&OK_(;;iaOQ3(sWD z{D(Fk9HD1?nQT4O{tn})Iu;ynb17)}`oguJXO8ZGJo}5b~xHgxHMVj>qf4bYtKns>8eX5a6O31kyy+VuOhfebagMRvrtdI&; z+h}+h>iILVWh^5ZkPa$oR+3|%#4;m8(0KwH=91&H67IfPxl|J&V_E{EJEokWd78tQS3ycA9%UXrB^+=1 z7Zc9llS$fboz;X`r(Mz}*6j82)5hD3KGaUVIVi;9;es}q^)}WL9Yoa63=q@KDMYU1 zJos+IeIG|ZE*>69+r3QW1FJ0v53G!SC3XYtId?(mxCJ?rHn2MfAa=9`F1)c0E)KR> zVdqy|H$iPyDjjp66@;*v1_g>nuU7V@djEIyr9Z?dfA%^yj?KGw8cD)z%$!qrEULPD z3DB;ml4Hc>@$6A0Epa_RJ~;h?>4#zg#z4plsG#s$P5zJ}c_B$LjP*>0@F}WO7qb?A zQ>0npgKaD!kmM(HAp;x%m5;Q`C>rl9fomI8iRO>$#28oPd$i4le`$3ld)8!51(AXte-yCR}$2g z_u|`p6;Rw|FCd1luITv<&5OL{38#GZw_tmQ0HXF+=JJ8#q<_EJ_ZqPpL`~Xo?weV31$;o=X3*%_5Fr+h+2Th@>YDMx?*LmwWv&SNF)V~z zbkQtDo*SYi7RvH4h!CS;AHehw&T@oBY4tHs81%_*z7#^7K4pOR2_eS1vuS%M8olSOp!1>=cM9l97ZpNl#%`ZdO@Lc9ZTTMJn0$>+B1sk2Inw!z`q4 zo_!7c+W%GZclmECa!hh`@|W3A^4G(VY(c(OTay=DL&slnbsw=RJu*}iV9Ngp(^0A{ z&nufRZA(c{pG^0Dlg~DkYQ|y894K>|w~aoNq{mdnI+-h$&sJPpW}rJ)(OltXFOWkw z5}BbgRPw%T^1H_P?hmPf@l}rBXsaR~@xR%AKUodjWtD z{|WGN6U*&L>&OksStx(5C!~X~r(wvYy-+t_DN#}U&bpeYnoqe@ZZ%7d8{LMC4~;xK zK`Gx7R+Wy5u3a35Gu~;k%KV<};3(^a&Ic$H2zvF#ZS-Mfj`fa}0U(Hh{ zzG$^5y|z(%3vSRUWHzUsTi&jH1v~XDd-x+Y`zwcy4-b~NfESvVf|$6sn0K^yqL+;q zjyHwZ-iG!u<%!O*&}QQn-lo%5%;wls-H%QeTAL;NWV`yArJAlld4_J0?$v1Z=&=O7 zM6|?*37ZMKPkto)O7u*qPN>TPt5d4;J#WhP$}TSRDRU@$T$VYa#UIH3c1EaSR~4qJ zk$Rq5#r%t>JSQ+`J$EWwy572ge1A1*Zqu1Pul6U2R;JUQS-%UCv#M9l7DqVqjp^;J^th$OrIi@F;OTKqlY` z!smFeA94|dkXb*W$92F~l#i4XhArUngH~b$qayde?BgNN=o9EY=tFYV@}BTET5qPR zR1aLFye#>n-B5h0)E<8tXHFc<(kG(y=m#}Bjksi!&}nd<=KLjm;r#0X&N4T11O^ajb!n!Tt7d_&gM@Vc~j7vs{LzeBUA#IL?l_KIGMX0GLonu(q{0EUVCXy1R^f9J@1 zjXV3!GJHKEA15X1E6yD02B%3=h1-X_w5E+ahNigN|_1_KNy;w&up%LEKoKmui1sz zbe4H=x^sr^oOthUIsRm?9TP;k~UI2(r!ye;2QkgmMKr9g3QyLB@P$eRPlQ+md};8+W*3&f zd;AuTU#v3qHer8VNonet=Mgk>kV1^f5Mnb@b;xR*9Ijal=e8HV8v}bjoirPt_MF?cp`SkwRaTb^o1$7 z#YxOp8oc(?_I~Pf3HUM3CNf`nTbY}BUFvqec^i92bwhWjym73ipz-wT#8LXHuks2-i_H>Jn#4vgm)X@%jon%;S146MrBl7Aps`Lu&GIuZ$wKos0%Ak zJ-%OMDu6fgwIjZSBEE~~C~TuvoTAX$@uJY}C+J}3qW0)b$0se0Q=>F~Aa^TylWT3m znWz}mOooU_NfFLLNj5<_r>7(*Ul8Iuv=wFy#0hJn+-TS2Q1I(pv|>}rEU(~vAU)8D zViDjRoEw%94x|d$P!(fO6ciGs`xmOJ4&y$MMdUl^8F?A0KNqucbK$aj;bv{i<>vwg zMx&re_=y1@U2MIqX#HHAT|LG8B%7#QLy#2@o<28Ik>sf-mhzA?dI(zNl$;j(SLsaIj60k!~gE& z>iO@rfD3Zpf5Xko#l!udwSlP;_fN&N9Q{^HdrzdkV?KJE4#uE~dlhF!A7TBd z*VVdAhyPyWQ(~*4f z*0YY**0bH#v%JNn%0&V>mQO<*h;p(>W9{PnQ0PrmO6{a9;Jw3{xf)4 zDJ&ZehVwzSTk6!q_Ez{P3porCRIZh%h361u7uB~}Ib=67dg^VSri!vx>i>HnYK$x< z0c5dUuE4fUtG4(rsiq^+@6pD}MmJUtc2ufS&whpItTUZFM2iTcr3Lkbqcp%&;T7MR z@vo*`e`+)*m8^~FFFYTcY|0kig|g1QzJk1y`g?Yaa5$l5Mm;vE`t)xrnz>&*KSt{F zHEHJjw{7yrPAD>jIY#si^W1?uj2uP$` zjRtF#Ag+0c#T_!(lqy0~sr&EwHGuO+fJjHO7_Fnt%KEQ=x&t`mx))~D2=fY7a z{0d~qV)9gl;rePEyB2bv)%PyHe5hnkuzKfKQ$@IJ=T!5OCRqPj9ZU+Wo+L(TF+G{9 zU>)6kNXN-h$1Z6Y($HSmCVOBv^9})AxYF}Y!2d8!CX9TUka<2pbdxW(2e&mYd&5q| z(EQ!{pxaNvG~uTU9JzY%G#3_c4V^lG{=gMvC_y~(VNlsep9c`ng}Z%(hXweZ*MIce zQS;aQ=njcI#OjTie5`CzIh$CrrxWwGD2&sVF>Olo&Ur;*~!UnTgj7=X~+8NT2aDZ?wo zNUgFq?51}!fN48HwL+e&&0Ek_-`<(kHzHj6t*UwP{~-+vKpJLb@g~oAo*%>YznJ(( zaOW%r*)CYS<2l(ibutNVd@Fh)`rsl^KEBVcPUfyo(}C0M5woboBaO zp!E9^Jb`0o+r9y17LPEF%5OgUJ3A2sLOy^zP7NBaXKw{0qjfOLz4p3Rq~SRcQs+~4 zkH1R(UkbbRCfNPs+p%YYd@?S{y&^!dOy9RxKW+<`2IYFwRE&F3b}X8 zsM%IiicLRuYz)cyN7EM{zs!ggzu#uJbAsjXHaUb((GgU(F6LmV!4Y3re7qe|GP#eL z7NF5S>AWkD3p01SY+T!w-PVd5_uY` z)mHtw!oAm--O-}7?XM7dMb)0T<9qi!D%IX7vI19%3ZvUp8e#oLW9nm zZx`c^i>)TS(w--H7x^7>09f`MKKaJ4xyIkc<4hLx%X_fzZJZW9D1k!wb->AH`jOsi z(KRx$+w+zD3mBA~L|s4*o=x&=R9uPvuZmU3V8Ec+?6J;Mui?bcmwCDstcD^4kX#UW z^4eHIf%h{(=jr61HO8rgbze;ypDbsRZ)h2ov>Er+3_o`3+c>n#o`rd)3(t8>=2o{= zCy=wOwr{yv>*Uriv7FEB(R>a)RHofLI$BAR{+>Y2k48Z$?!z&XCz|>K<|LCwfCL<< zGHQ%9Gj_IBiJG)MAbryEufwVn*twJiF0}8-sWQsJ>XX|I8Nu#6U(b`A}fz7$1C_LAw}|1m>2;km9Yst0{^F_d!U`93)fo*(^DyNhGjmLdttPNpmhI&Mx) zb9k&G(4Lngzu;CYY!v{y@ZLE*=6<;^59yhK?#=Hu?u*uU|O3>Zoe5Zv-BV+s#q za_%UW+jjkiF~v4arc&Qz>*tLiQQX%1DRCWtR)`74+mMz|J1jE+AxgUaOfyjuo3DLP zv(#4>2@FBPNn6aun~Z?EdmnyGS1*r(MnP{xA>>FqNA$rp)=c69LNMZ_C|L#@ILTuK zCmV7!M=<{OsNn9r`Es{)Pa}~+*r0#bZ%FeuijctVYNka+$j#+Mqmv;&>uG)U%`^pe zC3NF^>G4K+K@}y1TN|%S=gleIXQoOph6kbR&E-}_=-ow7=li2-8tIP5Bl&al(n^Ik z{pk}8>mL)~g^}DH_G;Yi6DCaYYNDXh!B?dJoMORmB~ie0?fKot=8tlME+d>}`nFU} zsqCM%X4NYG!&$r`pBd3=PgCvtYdw*OsN;FhvGLZuj%q2ck zAh21U;`+?klC~TUvN!A}zp@8m5bjo%hiH^2x9azN2Kinb_I;W%zrylyo~*h&o;2L5 z_R?m>Lg*1<%faQ~Obqk4H&;Kp38ZVNKcS*O00)WA5h=iV+H#~rI|lI$t)Z$8Lu1l+ z&l_Hj8H}5{erDN@O9rc|*m6YNi>s<_`d?o!&p(?yxmYjw@%w9PdN^r9 zW{dA$?O@-@Uanh*_Q~1yOzIOs$9Gjx?pa`02&i?-op9GK7b$fi?KtBhI961r?AGyF zlnN>{q9RO+(4W#Q5z-)*f!4?Z(vXj#iK4tWWK z20vv417Qt>ZvG9?Zk2xZpJ)nKt>y#UOf#j{-4>@l&U{8>agoo?>^ODcwyazG2}!4Y zr>={z!*|$hlgqPwao@6omHr=t>Fkpgz7rMNj{Z7IST+)1L?YpESd}wE^7vct@!`;3 z#k^5RLpMIwI8$bj4m-lEsfL%zThG_L??m#waRW7ld1Ik*gAilI8(A$>!uly_J0F=7R$+ zi;lce$ur0#%dt!APSemIi##kjxO?$N?GWSq+Q?MB&x+vfACpo(8uy+B$M)@SMf^^R z@Y)POSCe+zo6)&`WktE*@I0wWbN8{<8v|*9;6;aph~n0P)LP9@;LeK zlg%%7N=>gn-@;uxF6^YK4JNYqqfG)9gU?P@R`5Z{zC?SUU}ofNGT!_LxLMVFk|%nm zZ>7&Dt9xYOEvsWvPNPR%lJ;E@c5G@OLk8XK&cD*j0;!R02jW{$w3_(NC*5do*Eg`a zGIffGZWm748Nf$``OLWpw_G0%c?*~Y9rO-X=I>o=2!aQFAq56vhoUZCoR6aKrs zgl{Udy7lAv_D#aJjj~*OKp+i}jM$_~y3O+*@s3z`(F8Oax@FI`GX`mmIkQZo6$Ln6 zB}rY{Q&C;jfnB$g?eswiOjeCtp||UQ3@eU9Zd$%EBCI2_M)rOj;#0U19Rc@D=uGnpocf6H77WdMr2j&{E`&KCX z>BUzfM1ZA>Sso+Onmk!(8qqCn-2+m*zS?deFoSks-d>?p*$wkXI-h)``d^A*BB&(J ziQgj5wN}3n6~2|^FULLvV)@ORKiVZ_-yF(0_mh;9YKuF9TS3UnS9*5$R#5mcEWV#0 z1HqvDTJNUIo|tX%o(LxLG59A1L39wBxQ}>YQ03QogAY88CPWbkztzEL^7YoQ%+>xQ zr;gGZ>s_Fe&1&(h1FLYCri<{kVgJdo!`mwg-Apd5|Jp?m7O(zr31wx@tyAC0&WegM z3d+k8v{9N_#WL53ZwP8WG!GC${(KOg!q8mYFghz)N00ZU#wPx(^PpkX7R#a8(fb|P z)%;Jo8}mJ(Ccs=iTmGx*hM-n7D{(22rm!pdDOXwDat(ca?tOdfQPPawrsoAx`AZOV zdYDs*Zgzh!{k%rL0o$JblU3DJlaI|GZ=#@rw2v>@VeGJ6Hbj(f`O%oPg=lVAC{lRA zuUi$)WBom&ClZUm&f6@rM)|y#)Z$$~O(>)n2+g{5QwV7x&hMMg7KYHrI-h0kP3!#f zjEH!iwcZ;7CuVUiYuf0pc>Ht3$cch+e+c((*o4~8(n)MR?-%U9H|_O&#ksWQNE4_`ckhMl1I8=%VwZC;<1i_5J#eLz_W)D^L0;GY>Y$MqWQ zoc!8PtB-%D?xd9@TpZNiX9Hr@#Tzb2==InqG!a3gApSusLzjU z{ls*3u45(RgmoG_KaJS%KW}4*n|k|srUjogu$i_Zu)3-$2*pb}S8mQ0$97NbE^d>5sKB9Rs9P^N>vj5SRlyv_ z5t=VKr&S%!ejVjXPuTtzrU-|F&SGfZ4%JG~+6`xMR%#Y3R_M{qqa?F2n%oPbHv>c> zaC%}Xiqw3cMxGRzLqSH%AfaqERS03@OuRI)Px#zGNB5IJ#^Yh)+;@w2*Iz5E( zK98UdKGrS`_?h%HgSd;E4`fug{unX&NAJ{)5{A=HUZSjN_3X0Ff5Rk^q)&mQma?wx zV}p)(?!kTFn@&pdPhb=+X>C&Kyyo7Uy#cc6r{h44VDry)X*xJ-Z#-HR8!dki@m2|F zAsmQ-I(a?-{BT@z^mR&oxoawO=eIWh5x<+A2gf7A3)KBSW^!;L`gCs&;JBvi*uu5S z1|VTB#vi7d^Dxsn{>O;-oYt%9767RLb-BzYL2%Y(=59H|pk@^+kPm;OMPN+GEDRcd zGRtZvE(b5|HaB55-!+OiyH}&2s4-x2P;R1*Kj3lcsy@)ESGWKYfXa>X&wQ z3mBk_+3%w&7v;_cbNw5m8w@!v4f!Ep*bjN~qtW?mh~9?+@qW`sOzM_F_%a&%t1PYI zCVC(JI8q>lat;5Y`oRbctS1SWxOAFpjeId%=Z7|EEU+Pa`(i?bZB)35F#|2k^k=5< z2@AkcPap=GOsc%ySGmz;r{W+3K#Pc?)CgW6D`JOj5xeq2#G`*Ta3MTinoh!WS5saJ{%=&GAIS`1ndB*yPU+^xJ7RN3uJwJ%w1ag=31 z&Cu<4FT8-{#umoTFu%F30usK&VcHP{A>ZbnbE3*Qp3AmM%$>bx< z?@FrpYiewB25z0a4m-|$d~+Wp^Tq_OBnp2Y&gQ=hLzb-I7-~UN6^|8gJU*(&7sV#`Ry=?+*IyYGCZ-|8A`&3+FU#dIyC4xlaV_m(IF_ zXou3Fb*;N%If=qz&l>DTD$H7aKJ}8Af=|(If2V3rU)%_@xkTa#6#{-CUs+26MiiS6 z#yr1O$FjUf&V(z<2~jy-&4iOE%(*`6X)Yy7Fyj6U^9wHWt+|VImThtR2@W@M|J-pk z%Dz~HPRP!hX8CM=i2F=wq^0KCfD1^nY!BdodRx8T?2&{g4T5 zW!uWvADEB~NfL4^{Yi#eUc2+zwe6=S#3a~&Osc|9HVGyBuLkC-R-)UyIVo2up0i6YHY49hZ*$F);SW|HBP+_> z3AxByGk!^Uvc0kT;(P|qS)e26uOX3Q6p5s=@P0f#|5?z1sOtIuoUS_>ujGSG|954Y z;CHx@uI(p3Ysz_EL4iQhOz-R34#jtZ)?p3`co5*(7y9 z#T2z3LJMz7={cffUoN}YJS`=rrT z6`PdJj0_{(sLA%|+m@A-qsqTg|L2KA_j?Z(?EilP3l&Z={jCQ0J8Q!y*kT&<^pI2_R& z<2GKy-}P;?n;`b~khFMA>c@ve(+vP-0#cnkDAIi+j=k0r3%%fnhWL(xp}BwQ9fVmG zPRlDY{}b&^?}NepeJ@cGO@kpI;2P$hR~*ES^7&r$dH1sFX1#FZ;DIX(3z{_b_%`nvyKNk)KyX~X!4xxvf?z-GFh(|4;#LTFds2dB@vIy;7P?N+eXNdtv#lmyEk8L;+n4Oj7k*iHZuH>OxDti_`4;Q#4+44!m@#41-=skJ&Ax;@d7$&7 ziIny;=H=B@wR+CAA#PH=#7Arle-&I;9lN8A%#h)Bxv$}by9CnUl|7diU}YyQb())iHECu?39}FU4a37wuPTRVK)@wY&XP~tt;_`Wb)@6MrfB!f>*m;Ghrhjs1upsp9 zpIDaVD&x({>c5&%76(0`y^npIN6tQ(L_TD>H4=J3%%v1wu;s{N%`)&oHXMu{T^QiyKaI=8Ke_Dtln1xP*#sk`|ita zeH2*KXA8-}-aqFdZ+UWi7uFi3e}5w6%3$b1V!dOjqXP`5nV!fW8i@Msk?a#J(6YQTuhuiWMK?j)Qh8F(D^~dWSR}%dsZk;RC~hzWDy~&a7f^ zfz1{8sv%-zJ3Q#!BqQ5B6^rQ#u}O3F!YPl^SqhF%M-@ZBwL>TmVMB`{jGM5Jx}mxUzh&~9pOBi6a)xP~LkemXL0 zs~$Ms*3ofCvrh-4|Kd}6HNL8*Fj@|DWs`{_zTg(aoo|r4wyOgIyz*)Fws$-sV2| ze-49sc5rV7J)oNx3&q|##=f_@m$13_royW_V2$Gs>Qp_9bo|>bTO&Y0XK)J$?cIAn z5B3on4mU0TshiLa1om)B%1(Pl5bnVY@w-NMqeNVpzAZaz{!}Pl`PHLssK z03|*(=~8DTFpT7Nej4NH0oPLuI_P@7@AtsHcIAFbPh6_d>w{6}VjVjpfpHKrPHpBQ zElnE$8pzFtiShtok>`&b{9*2MqVX$F$2W>3R3wOz9=7wJY>R1i)AG_niD3s2QP&Ku z=ZGj$w)Sw;rkfQ>QAZ~2p{Xj~uX&_sR{9!<`fotUs_J}IdI!dN zw~mWwyRo~xVY~wghwV_4c3r)u_1vqhH*DMbSVwPW@WSL^dcpGaG>H7D?Oyp76X3P3SeR73VL(06znJTUHKI0fcQJ!K`(B4!%6U z-__3lZSQ*8d;epmyWg5cpKlSa@ZpJl2>D-SJDq!S|K}9zi9$kR=Jg{MaRoAPsAg#Rs z$vLZ{M&KFMnYRb6a0aU?AHj~Q>pH6N0fyQM$;_CrFdYJT1E{95YC;hnL!~rPPH?a= zq%dXvOu#v7YSq+@X?d?Bf{D#aiPHkwUusDUc)0Ly$@iqohvA^zfV#0sD$+m1yjS-F zGNwZ_n3e0{z4gC1*mGKddUZu$Zu$WQv0@-wV;2+{3<7E)pC%1VSsVY5l)cqj9lgbx z6r7lz)+G(S_X{OEF3rC(OqyNu&Az;nodGH&m1LiyuF2AEVEFPp@UQbPQq@7;-W%VU zwkYqt=5-R6d7IHH<^z|xCjF9tAWH$4*$Mj*969!|qY1RF0W6vzEh=iSZ?YH?&iCfO z!m2ykMmrlv?7#gBqehh?bZl+C)0V{ac z5KrNWB6RKk*9!Xj3eLaZCzPxWclocr6nNYiv`ek_8V*)@1GVapI8z1cLV073ko9Ce zNkDmLjtRM3tE8`y1aVnFvAIOA|4)@V-05eHRik;R!QM@ckbc-|LB>72FHa!(4&S|q z#{V~A!544p3Ex51@^8o(n%a_&A+la%4&8`vB+2KhEcx#S=YoIu|3&6cCUhr}vCjUH zO_smJvD_ffd~ErRTzX%{;9SUv(b{J^1JRThK%AE&7P&u@Ydvb=0InxP*qQIsPv!fq z^Y(;W+@vu(DV>j`t0hp#o>WWL%=p~F{uUPvIF!j)|pVMvdGe51>gSo=%R=*n&I!g zHhHf}ee*X%YUD0TtOb)53XHF1-oOIctM&GaLVl+xSDhIwvk-ft0~GrPP)tc7t24@S zLKw)!>dK!dH$gnUGC4l_33s=7!f%&Q;7r7&)G&<%J&SExFB&9}zAiLhGIGTTUnkCt zA#VHao*MQ|w$|@+v(uEu2)hl&XDEV;Mi%W4+s*!StRK3di3kC z)VRH`sL$+o<-NoyG{leSQ|yd@8Jf3*rDx$!?>`xXSh;(~vv_Wwq^9GF<) zWVkF9=uZ3GAVdW7!XKsf#W=ki4A&z%0yQrE;wZKe!@MOPT9V=Gqef4x_{DGg<1DW< z49A#+5hxHkpb-l0jmWXRjE-|_F0=*$9h{59A#THU03*2~^2rW^E2Z$1MYIq_WCjBD z2)$`Wh{96!J^+mvIGkfOZY9>xU=T+3>~`LTAov(K{VHvaK{^Wwq*nOL%{1ocCLtHA zqyg6l{Q!_|u5O1w{n{p zT4Vs=>6ynzzNiPTFVq%73(mU}Up?kiJEw>QAc|Qh?me=*3!RTGZEGTq7dbhMdiQWa zQEh1WUCRsqFzn| zukPZxHELF(rc35KZ+6^{r#f%-6TirmQTfr2xYAUarzuOynrdXj;(cVEFds0~YJ~@2 zTeO3Z(HT@S(;}eZRJXK7O^sK(tuMVtC{ZoGbPojczceonG19hJ1@hx?;ozBx)r)!-MBSL=j+ta5* zKw<~7V%*XBHG4C-KFz2@meBQPj>)d0Vhegig|t47)#lJ!%j$j~gRS~(DH;8y3lfw> zE&1pAXza`r(1||sm*9kD=WuPRTG8JoLNu?JzsXOz5bchNPYr*t7RxSA4-Ti7TaOyd zZanX>TtojcLhJnVx&beuAdS!x6XgS%a;z#`d+4PAJ(i*(5WteT0^KO;mmtz;p#X(T z0DNCq1ll0|$YyAzF-oj=yayTQpZNpLA67-Uvt2~*@Pwwnob_RNf27v$wja`CDoGHs zMsRb4H3|xXOq-K}zyLqcCK(*L1v~HR?ab>fQNu;7+Hp$wKq0)DIN>QXgvyjfh|^A> zu>a~&@QC*)*^Kq*%v)k1Y|`KD{L^DZMp`S8*nDB$p8!AJoL+JolcS(bq_lS)jhsP$ zY`4?QT{cK+Lig7CG^&#~Xv-G&uo~+!qy_)as*;Ehwy>kWS?4tg4S+RU4o5*B3vDn! zz_=4|aKdaoKIt+&goEZ1yd`MFlN3YvxQB7RUa$qI7*=#A1q2;icnuV0#ay{PCM3-k8@t;O>$(&#@&Ob&%wKEhmis>|~*s)gKOI4FC% zxQ%&|(Z)!m^EN;|hkSD(e;P3x&wGE520Uz1())X~-QVMSjfWA>6J9?($&k5wZScKH zi4=(-!+Q2IIog(?J|HR!7OxHc7GP|2Bn5OsRN+pv`yE7nImttYTdJPn8@1F3EeZ(S-tDuE^|tV+54!<^iMDJzyV6M~cv&ewPIOFps(AM5o2% z7;j68w5DRon9O3cO5?{C9*7{jSegi(hr7qsTX`pl8X4i1n1j|rPVBzcp6Zt+hcY9h zQ;N+n*Ag2efq%4Z#f>P8i&7&YGVuMN3@4jGJfvWA3*xcLcTTqa#jCV0A{|8Lz$%Ee zerM{!Q``FJ(us(@Zs~B{#W-B9u5FoXFfx?-CeVQysl>VGs%{-04pcpO2Jh1gz~8oD zGSG5xhKCbU8ij2;Bf7<&bC4r&sLf*v#eP}?$eurfA?4Z9H0n_yS+*P#-uP?b{kc?0 z%0J^dzkn7S@merKu{BFR+K~hn3Sd{$lT|heJ+ght0)$+bCF^#+05Y%;uq&n@irMF2 z6c9zgK0N6N9Q7kfZdFwq=XoI?J`o2^bO8AS{mBrq=|B>~S0smLoy3V$TTwfA&Tqfa zFq_xhtM%N@fcpXXRW9_vHIf-e*X&dbV&4h2FjN2;npJucadCvo=75tQf+m?IYD_0@ zGi0@l7OQ$0A#*z=qv+P?R^9v9wU1BQc)w#2P}fj8T4obYG->J2?Q8`gLV*0v^Y=r! z2$MM`tuAXS9>xUdhq7jZJk$mlxnU))=yXTy{WM+N6><6!dgmsL&;h`JxKlPtY=T*( z(A)YLWN5{0=vsrezi7}zVla&7tup>I^03PmQ%CHb`+ydYP4 z&WsG_IM9iiee7tNpiyLS&l{z$Sp`wj=DIu|dqJdV)w4iJbWt;U^a4Vy^s3@y~cae(Y|NO;1B>*cMxwl zL2FQV>ZYZVheK7gS^WK!@C@Ll5MF~Z`wH#q&y*@>ns+%vkun`^FbH-~jlDG0^-dqR zx*6gs3dhy{8?RXOG>oFL&9M5KTL{gan2U1uxutX=8F;CLhI)5jsz(41>AM{gB0mdqyTF- z(UYHbS=`ZL&c$J#8$%c%Cr8UlY5L>3DvsY49kBQ$Oe3(36r~G7 zhkVHA8@^P=(kE-@#@a~(mXZsA40A}Z=&@^{zbCTDrpFY`-BhHHA;Z(*fSSNKp8x=>{FdXkvp@fJ#>xAP^G`2?95oAj& zGbCW~2@&WL3aYVl$Hm%UIxTLajGsy2X<{mLy`jXcB1J|64CbELv?ukPB71M+YGSGj z6rU?f_fgSVh`GAt6Gt$fhhu*DyaoO)v+swEg5$#Ze#nyZ{pV4#p`1e5uq!2y0N(y( z)K=^T3uxXB0Ucautki?yj9gV{_t0P?H!UaqZ(|7AGHzu)qfO0Ea(&U~QySbZHd&R$ zun_BeJ#D>UI)~#e$gYt#4T~SBSZ&W)4do>SRCwH%bmOPs$6kLB5)D(08(Hj2t6ULz zaQL%Cw8!QX*5WAH%?yL?yIffOIQs-2ciVo%U9mEC+qP^I?MK=!6}+Q&KzCW2CZ(Nu z@*|~q+WFI2;`WrHZ-iV`Uqo&2u{`rlWW4JCV1BldN8c%AAVn5S=xG_DVe-YS^3_iQ zzH<8kOzJl~7dq>wNPnN2#@={&90^TS;o+iI3Agk(sMWb%`zJ%u8jy!c(HMHCR;_&j z#hRG;yRH%@j+De1d_n66l}8*HfxPlu*4=z=b5eU1YoQZ|f`Y0QWFSUa_#ff7RL^tU zd!>{ z?sS@N-p&G+DwL<{ebwP8~XXhC+A6G4rIFS*q?EFj z)Qt-9jdYf|XQ3vOu(kL8&?jbpE>XC>Uv@E`LMlr+8phnqJNzIOZ%Cr!LR0DtIKq&*_X$YOY>SiFwt7*d(*R)%gVw<{3u!3N^taV9At&o`IF9UwqN;V`ptNiMI&m#kO)3CY5tFHWjflg5-uLA4 zwOwX20d3p>_U9c79f>D1G+F^02J2{E0wpW;MZwBvk`Ec?>t@ggf#OKD1{QVkl5XLf zcSNAGJ5&^uySsyzXoDyyG)wpY9JP~=olK=j6?0d}D62vI7%clm&J~qgLPk~*t&iwA zIfmkwcu;ZatB05-3apPfRyhb5<5XhxVo8c$+2dx?l8G{g@hTBnoJ1Ub7loK1`;a#mW-Gpjwn+v^-r02R0j1pr z0A%9PyzDh@9}mhg=L6NUb7BZLey4|5z5^N3LeGBLu@pG_+}&WVUn>zmT0G%uKqnZ^ zSF+jg?#i*h=mct_I~I4BhUSbwA5OLl92tEPO!0r(`OsYuzQhzVIrqmr$W zB|->^u??e=v5&C~A!JL4L0KXpWF7l5wluONi7+E;mMo2Z-#OR#{{FB3qu+UOUgyy{ z=jr{r@9X~D*Y*DF@6R18*o!Ya#=!9h-{-wPSn}JtZTD^~8?Q2h|MW0e7s?yz+O$%usGVtn>kzBrCkm>j1%h)M$7|JBGc!-Klq{E47oZHt7iSH zbE1Un2fQWxgrC>kby=!N$(i17x5D3kd+(jcplU(jG`BZq`!@4Z;3P_G(A0aCyw>i! zcS8sBfdTXWXH4wk4+VduSX9(tS3PdPdgP-46e1nNK62i#OGmo3;m2Dx?Q8w$MB8ia zw=C8MCJnan^;R{}P>E^683)!F~+mT|;fYBHJ2bD(O>g+}xTDKH-L(H3M zXC;bRI%t#v%8EAeIttPgbJ-u6KY&;t$7>*D22KP#zcaO7w_m%vR#AsbB-LC`bl7Rp z@hM(jnoP1x)*%q{^`Z8^*a_Mz$)2#aT|y)^V$xAUY%p^nMMp`ePaw? zQMWUGRem#%O0Y^GUs-hUa>Kn3`R+dq=c2S&FzVk=&}3) zPjaq3UTzHwYa<()*N)*b{?$VOReL0k)x#UaD&UD@vuN$z)$lxX+bOPVY0a{Eo z(9YtUUcKqH-(LnTiHwkiR3CBfe`|jA_ZSKHlx3jX2ScE2!C-Ir*qw7VXXod7{1dTiD;VVsj>zR+2j&QpHeTEy zBdmU}e3Y(?En91L{BnU9Ar2tZOM^@;kM@`^Rm`BG=uSH~uFXb0JkWp;qmhQ02w<9f zTDOvc&V2XaTqzP?h)CbrG{}cIg2|CEA!$|;S(WMWa5&`lAcd0;;K}@|D00$_78#%FTv;QA46AJsO5Csx%Jr03No(icB|fMcJd3-LOU|tcQs$%b z=;j()ba1^tD;h8iFQ&uT9%E1t$#$aa@q$Fit^InIc9cF`2%`Q3)FLPQoXU5!${HrU z=cn`P$Q+@WNwHoy3*DX+x0{`(ti3Og34f7yid@$~o!nab(dL);#f*mBa(At3dhJWv zW+V$d!s~d};4F}eOEfV9wahAqvW{h|BZfA_hPF$Rn(Lvi)Hy_t^Y;zDvi*;)Cq;zpgfL`O=^1KRTq0uUbB28HGZOH)X$AV6Qyi)&9Mq~A4`re zvl1Ek=D5S|cBq8a7uNQY=#_x!XiYIE6c-bP#XaphYp(*Zo^GHQP|0^V0}*ejkkBAH z%1>lG<&V@=cp)P|RE}K92xu4`wO@VyRVj&-oI{iibq3>l#v)4W3HO`-?kvOX zJUMEnG2PJ!-ue*u$pR*V_NH#DDo%Io&4}}Bz?o2MehuVI`N=}u3Ak(#dor=-KAn24R??}pu8`xt+I z(dyf^Q*~y*`6(fa@_l0D75al?8QCC)w1VP#a6c(GN%_9UZ5LJvjI0889r>c7qXh5O zSd-|L;<(!HviROJB%C!+ajpXuFTmQWQ!O&oHEj8wro}dK4=o&*ws8F#ES~6>;4WCV zJ&2z7F2GC8tm^n~=?!HNKAXs9FP3!#!g%LVktYxgqBFO%*j6yhza{FxNL%&={V%vnn%(Tp+z$3EKZe)lEIeYa*?l=(hF%T0 zK2UmR^m0_pF9R)q?ywp@V#H;zm6ZnupIbYTKZ~3z*0UfWq^loiD)!1m$#r8Sc6HnB zM=d$zws9Ob^pg8zQ`6eFl9w@QC=a_}d$k794=P@g*YDBLNS zTj32dgOw+wCO@GuS0*)^Z2+^ZK-p=yO)w<{teWkxYB=^Vo!k{+s3`BgVtHC6(5&^c z+HRroo0!@=RhWsQrY>dl)?vM}BrBP}I&xQ8dy7y@Y_XOxJS5zcQ0Q})_SxZ+jg9@1 z=nRD`2g6lDJzBpW=(}?_t?ylzp6^*HYqOFB6a#MABUGjC69%gmYcsvM zyy>e%c=Xjor~U)LhsjVQ_eH_zSmI&H`Bi9xK;{xI5yHy{0r%DmPwrA&wok)PPw z*jlZ46n~jSZn#|bv_4$6{%Nsh;BP|y{O2rkM*btY68nqrkpxQd!&sHh<{pA7w3=UjT@>(kA<;;**lo> zgIu_2#Ghq2$`H~MrH0iLz2KjyVSDY1di;xSmZXUyHSDRA+GqIco@=;rafBMA6& zAJB&mnmuNCZ045)NfkPSJsNPCgg8Z*L?ons4YFy1^fY%}yr5un}s(NRWC=)~?98MrTW!|p zUmC=Vk<`%cbit5;r)TM3ET!h7>4TblC4V;K&6<|dWmrvX1DYHyxLq1XCC8<7H?ql> z7SEiZFPk?pu&awO*E=ty1P3Y6G2oEI4^pBNhv^hk?j9oHZZQu$F3b9)jLq#GK1#b7 zamasq>H-lLb8$r(tIYL_4HT(Tvrk~9}Yi-Ji5`>ct?mmwPWs<=6=v#YAt)- zk`#yVgu&a-B65AtC?X{qt_L|}=<`@nb!myp`TGQQe7tc9TE(>AwF;}x)Y|QeDSw$S zlGC{T!F&*I3X|<{b2FLAS6LKKr3+d!%2(d+;AP-;32O|gY#aqMJ`*JgU$5GLyp%a! z1X8MM)|aZ6NlSCvIQ$bIEh+^JbNIfyV|OZo`S~Hpg=>M}6C&AF0y*X)J)>nepuCkw zIcNeg-{pTk48Ql+zm*c9VxV63O7YfiY1nsCC7iPexBqKP~5RAGqFs(!;BZ z#S@{rSjt({+2QjU{?cr6lKNG`VHEH8`2w>d*6fl1F&kJ3EkUyRh*;GW|Mnu@*VN-y z;*T7;sg!^hHKl89G-}`AqgCZwH%`ok$2S>)1`mK_^-%Wjs@KdZ~1;VuejGQ@76An5eR{4i@o5bWZj6%1vSp;SkK@RtSjD<=AiS#g?rz@fHz9Ao%PFx;g$v z?_E46>&WpV-gm$T2896iv;oWCVzM>-gXR2C}Qt>^so@U^gxwoegsZz0$X zupp`z%(pt@20V(!zxEMk0pZ8_2Y5~VG)gRZg!jm<5?D?UEN5W-ujNR8Gv9mWcty4Q ztd-1tUdIe+z$n}pB1vU)X#JBFe7)3O;;<&c^lgsQu=DvjzK&c7v#FsoEv5w9slMtX zqp+8AuF>kf$46OgZDIwOzjB!Y>8*=ycPRPQL~$hu|7Tjw_ljL84VS{lA7AJwpe=9^ zlmasd79ghK<@-RAE7p(eF`H#e-U#&K({hWxoBwHxS`Nkrt3vK02EV7q`z!=NC1#aT zQns5Yz(mH_8$#1)WsUQJUUVy`4?}v+2m(Kz#zKq2q1ybLAf2ktAF2SI>b01HXM)MY ze|}?se1ri;S;3}? z!f6<-_+sNtm{&E3TLgp-3s9}WrXr2l-j~iy0=i7O`fH(Ho6ID17ntW<8m$=e%ryf% z1?^I09h|0J7sGB>73vgZD+wsJ##xdUVTulOIeUmPv#3uXLWbWr1pEl0pAkT4X9!te zR_--h7Uw;s3|&IyAM*k!!CT7GHxS%QVLq0aw1A?*b2CsI`Djo;ebexcUhgLSinj(p z(XlW9N=F)s9D}>gPJ>dz-i}}n`doogQN8?96duqa|GeD<`)I& zC?{<&{l$A{4C)Ru#@$GEYC89`Hd->)Yhxrhu{^RHqYs$EHTWNAj(F1yk4h*I(f(gR z7t>q+?OTUbn05~cttA7B(GxI(m^RJzY1p4(U^PVe{nzRg+=~?}`Q^7p`jc*8cBL|Z zysQV+Kk*|8d5)2ZZUqiN76bgYNz^|en+s&qE(~Jetg=`y)v`eJfUhWi6t+LpAyyz65SU|IF>LBMiQ%+4w1m3Jx zS+V+#4G-Z}`O{ZP$Xq8QLIEPoAei&+Ez8opsiV-1=ZP{IEe}}dFAH3!jGRb)Xc*MU zbENhj_}YwMpw^H7`P$bnZS@$!3+6zjZ>u{N5yj5q%k-o zq+0BLQ#JcZjgIXF(;&;D*91|s0$wF9D1`_bde5aG1Wc3;Kx{nj=tOdYhMNM#1*{koQ$;7 zWv1uHj(Drmz(YG3JDRM%fn8U|!Cww~!^f%N5}$JNEgjra9iW!o{xR`$<^7mB`5hf@ z3u{8YbVAEa&xl4=ZB)FSml{C#TN=7K0lNRt(7i{WY4-8c_TYRUFe6r)abmEV_^J@W za!?BLAPxSuPRg%JlJqj>H{ zv6;@V&X>J(7e--ElZz81h8BoD;n}PJ^DKS9gqz>ozA0!{ub2`?2Lh5(5Rhm9g^1=q z56ZlL`~WG7ir6snhb=}D0Uf|Zy9fscBrRZ=f{5;5BAvE*7!3Slq&C=$yv}2WC#)R{ zNyjl(NkHC8Hd)!(hQtxbr4^=G`8{lp(YyG5?B7@2b)c3lfo}+)Ic*mDcTYz{ga)(V z-kKhi&gw-;&Isz_Tdp5wkIFUJWwV3UQwL`TTnt~lr2D(_x67eTvd2|+%+#szjMzgF zrBu1t=?;a`=K7`qTA{p%s6{N>%oAGDdgO^#4t6q=8cU>PCn3P-5!qyAvi87_M#{l#Ql{cf_UsN6UFnkQ zVvmtU_|DFtQG78n{v+O;@wKOd>CS?2kwkD9G>ol^rA}{;?=@y-At3n^R6w#jxrm?u zTyw;rYnPT3_{T`U@BVIDW5eN6m1chA$JSP%iyVeX%x zGN!0ukKQ*rxRhcCu07Aw#`c)l!4f5G9;70;@05YjyFNgH<$#}=d1OZ%E?PowE{#ih zEdBYmV&POT5eGw}!I*!@F-_;?_JWh=%?h_zcb*7_4CLF=TfupWj;ixS|e^t(!4 zd6EA<1fCKEG8>rSl-RaFtjrg@mnz30HNg?a2I6IhdICr#qs%*LfpCW=gn1yZ^q*H3 zDKYLUdtI^r4g;Y&M_7L#GO^v3zb^eSE$FA|ys-_LUTcc8&m!pjR`8n9uXyAYZE$l1 zeT7sq`HqkJQ5r6<0%a)a%wbFDxLVqL@+Q#dqC(I-dIxUCuwfkJb3=BbkJF3;3WVC| z41};ms<}1(jPt+mG}VHDuuUUZ+ghm(Xw;v7uTh>Mhx`*`43atmeNyENzW(po$ZJ^t z>Kv>A(z2|BS4P$j98v3tD-@j3wVA^sr`-890*|@cF4b1W^u#*m?<$-ue+gF?{smb((OrJkO!@8q0vHQ}kIHsN?}~q)g($Ix^~~Xr=@TObIva t9;8C|-NoZHG4M1oIE4P2#Ted?f)}XWVkEDFAEO>pSJA$cuk_IWKLAX9Cocd1 literal 0 HcmV?d00001 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 GIT binary patch literal 41093 zcmZs?1z1$=_bxm`cQ;5Q-Q67`N)JO1sdRUjbW4hWh)4`DD4j!xgeYASA|2AH-{yV) z@%zp>b1~Psk9=_gcj3JXgoZp}_%xK=@BIRP;a~Fa&ttVPODQ5;$UUK_EOT z2W4fQC(6o!)SqPb_Ao&rujo=)>L@oB9ZJG;b#KAGHR5n;7$_Wt;0rsIHT3CgrjfYc zbp10t@A@bg-dnY|Be!)6ct%s`l2d=33im3hZNN1)FjCeuU2EK?a-ZQ$@Do`#fBV{t z?l9@0SNKw0j$aJVS~9N9Q@A8jJ>-b8G$1y49!oXddi;i{>lyKls-}y;G<>P9T>w|? zsZb7AAANwsBv{Xqr|=CegU~=k@A>a6Po#ud=qQIcUJYOEknRs=Mf<*TS;+9%S?cQ< zYU#8$45GglGchT1>j~z1y&*c$ujMbEr%@8~Zob-JMDs^wK%z0vWhjbiV78A(l!33F zO08o;6|nTcl)0&n12XHZEWuYvDCn!&am|c=OA}9pM$oTy{@R&whvY|*4ZrN{V?mXP zkiaz>Tz03FL6eh(eI9t^@ljB|>~0ROVp&2c7{0A8LcP`f3NCC#in7u*iQZd?`~9#`~QvK2;#*06TR*)*)z&GB3v1 z$HY`5eTmNAP3=zX9pSV}U`89VicW=ggcOKmWQ~6Hl~`1TR#0J-$f=LmLpeB|h?V4t zLPnZFbZT7M^^jgEnsMrD7U^`8pGH49JV_)|y;+*lFg4^)Xs)BJImivwM0J(Dg?8ft zOS)f*NTEJc_4{l+!ujCk;cqq=} zJ__r8C?_Ol+J^Vo+_F2c4!qCuyl+Q>R6KT17?j#Q?@!Q4WEqpQA?nYv5@FfRuo`t` zgpypWD8M2?ih}(++Mvf-iTK@XM67$XJI&!+xgKfe^dUwX;zz41T8w3+Tf|1hO~kZu zE`6@+EbBz^Dtat=>1I#XSSdJD4OAAC7SyE_cbU%9<1GY-e={Hhuxk_rvAo`2tP33McMe>=(qOvu< zyss{s{`P$G@Vng5*i@b<4{WV@zaF>m)E|WZ}kFI2Dbyf z-K5@hzV3Vzn!oZ<%Ro$zz~H$NpYBTSQiV);NvU-eNtKZL2c^wiO@34xDj^i=yd=@2 zsHD9lZBh|-|L3dEp_1=Hl_sJuD@hGy4O%MuEAcC54dg2Xr-P@3re-QcE0*;Kr=LxK ztek+<8pv163Frotd{MdRTaU+NUzt%5)e>bE{pGO#t+`aNwBo}~E$7#zk^oIFg~WH8 z@3LwdboZJeU1H{o&)$6O(7lbm@G8Uko|!kyW8*7;=_BHe;;kYj?IY#$$|u>|#v9j% z*86Bj_l)jb?@VmBVGn=TX)ktnVz%~smkXoK>dQ2H#QZ9}J4l(OSE6_G)w5R7NryDwla`aclB$ww^NF6(JrmMu%=6AGDf2CJC?hY+8PgFC5-yw<`+WE$`pNUm ztIW#B%K{(sgYviD%;w1>V9;nNnTd#TdjVWt(&MCc_yv1dL;dH``X!fuLCs;Vp~|n7 z{kshf6{3r+JvWv3V*?ZR>CZloahnR8*0_8)cqZLyUcT(v=G(@9L2#jT8+aRv84}AH zTTLKNm-UE+&PcM*i&-K-lGoGG=Vsgas9-Dj>oq^6R}NlI2j4^))w8wd^Rk=iRW;mS zd_FqW(thV1c8N8uZA+2<(lK)XF18SJ&?a?IUz>Eodm^Nezk{bE+Csg3B}h7m|Je1k zJNPanAw()9E7&IZOUTjf&drCL>1+I(#p{VvH(W+EG)y>dGjTcf5CI&Y4$l+IglLLb z3qJ*ik1&+VnuZzA0b5l$N=ZC=1z#9zBUUsf>KJ*9zjn!-#O%o&`bM*WMzFzpH&df( z=sNwy`)%FNB^PQPi5CeLq#^7B5^7}M>A4xCWgEpVLJG8(ZkkuFhJWL(^FJJtPQCb!Fn57r zKaD1V)=Iw2(1pF5eb=hLyO`%qOU=^x(nro3&OQCY53Oac8VA~5+T71*R|8H*0&2gZ zerx*JHGwrzK29~9^u>9I`30NZqfPGL!dH*xP3FBmwV~TbGAp{Jc#W48C|V)hwfQon zS(+rZsp)7sO^+GIU8LOMHEAq&tNXj2&-R*)`e`VCJ&&2?m9N9nw`{xoFBJSiXPJMs zeN;VFdrD}(m}k3LM%1oWZ<{%M_WUwZ+h4Gb%a+adfvw1>&dB^~es}9rkvilf{Lsv% ztIU(vgE#Ep+~;u5afZ8wANu#D^7gBN-dsNWG28ItMVMDd!2LLe zn({E%UBP?xy7ozzg*;U-;y!KT-Ayk>Y!m%E22NKMnk_K`u@9o>V(cP*4ERzDE9>7p ze~Ksms5JF4;ZCWbGxaL)44(g;PKwU{Y5~m!m0e$3&ar4Zbj#qbqUw>pw3Iexk(}Sa z(P+iz!WY^FUb3@+E7y0ExG9wRd<&M&?Y4e9Lr1<{3_JBl&h7i|EiGRvsy+m1UjC-P zEX*_tt|@4t@jqX0KsYVW+SL4L7ra+HdNw}xqDjcDqxEfA(AD^QTXb9hCD~!t%51ac zdF(I)e#b?}ApMn0^O?Zz+FSK~b$w2)ma~m|0mFo)yY-`N%DzR9gHU?!iF9`I}Q3uvN{i5+2HW} zcAKL@wDVRs5*gO>O*&s?A6$L`Vzd_oF&!uAVZQR&Wj;G)CIB( zbPjnFo)iIu3fS%%#$F&01>3_L{6vrS7>FX?Iv7B`q0h9WY}{PUV-)2R;S*q% z!(n7(l<~B+m(o*F`>*c6FInc7-rnv~{QQ1?etdqyd~TjE_yr{;CHVz}_=SXcffl@8 z0j}Ow{=BYUEdLJj-{Yv*dD(b6xO+RexiUVCYh~@`<1Nd~{4mk~y#8IMoxj8X&gAO# zU)usU$p3JMUyx6L|9{2?y2?CUmC|wWw{wQ7IJnrkdI59D35y8H{L}vby7Rv?{zp&a z|LrL%An~6)|KrYo^_1a%*uZ~m=wE65a}^Mm9F7eC|A<}=$6MolC!h^F2Ni7t;1lIR zHNfi+@aFihPvAR*?!23(4g^vFJyB6K@CWa|#cn3+Zy3ZiU@a#mCr?7*D_*Bz({*Qh z90yj6dTLw+GyxNq|rq* zwYu(-^&lF!r|D6Qg&g@B5;5Fl(K4@Fy}>2U@H4Onyrv*1p~DKZF7#a%%{2M4$dhQ70oNOY6$ zJf~4Jy7@aTR`~5lotYxgnFAkFBp(WEpyZ%H{@i7s4UjJMbgBA}?#=`+MX*u0b2uo5 z9`0XT|8O-6n-u-w_B+@LQdd|2=|y001}=x{ttDv)X;%+2mY2s7=&e;e8xGLTHKO>qDDF-Gh=5uXCfMhc4(rKTp{;*KZ?%y>B{X1 zrWg^=Q{u?euxqYC+=S(V3ja0=7UT*z&|Aql&r?d%wP0B3#}L{w1K#PkMw6hKKDQB=d3=!%R>_l zEdXoWlPhIdQ)uui#)vrQt7YWLWPcG^fy*_PFRH&++tnE$<|>I!SKG7_xC{JD3Gwar zZRA}nzp!%Y;Uk%rgS$eF4Q@3p<(=VHcDfscRkV=D2VPxWR>7a`o^1VCXmFa&V0+3V z&D=!m)?T;p@_n+>#4HEGlRuBd;rK8T>1YDi$KG%H5xN`noqRsZUia{{1@k| zs48?wWpj8>C;E)vN`QLD^{+S7TNSDKj5%D4bNqwZVskLm^Jyc?`QD^z>FgxZaqw)a zt)QnC|p;?)TjB#FgCKbrikspnt5B^~zOQmH=uo5)R~}HHV2dJjw!*>@_aBR%xQcyZ1cy_1Lsn0(!#;ter@K?W)| z7y>Q8Flb}IfR~kh_RqqFx`Z@lpMcBCTXi=tXg;h!Zf-tw%I{c8=x7qKk^u`R-9&|N zLoGwjr{`V}p}kx6FxzNu{Xcz0)=!1VaB{I1n$@~U&1bV*jFgD{!be#OoyBB^@|6X9 zS@{nK{l^=?8*qqXl3>6Q!uU0r>>n?ql6)h`y{gOfM%G=>YgAEQydi7`{|VSuyB z<+J>Xo%t_tn#LleFd+0=QMNRH;t~kqzY+*Ews4>8p1&XIJ*txUkkxGOp~lyNg~>Gm z21D-gdK9Ajs|0dp5Uqzc-V<%Nu5Zil%Wrfyb??dEx0B5B3^YGHLqy$?ZohZ8$mw?( zPFg!D^e5{~60%K>TDe2`uvq&+5H+Ix*%U5ET8KQHr!u5&gxh1xI@$ceqVj+IJFA6@1XD$|>si{Gn?163{q?1;@>(Z zwn(;O6&XkbGrvS6qQkwnE;bKu+l~I&%gxrQ2`XE)-|m0b)_vtOZFc8`|=Z*{NF#9UDFM- z7F8=@fA3d0y|m=j=>tlo)}l%EwlWFeM&>?Qfon08dUsfh4&Td?rFP5TPbp$PGVlSl zIZktl!jK^0ePF-0j#tNPR!wPs#%$r|pC{z>HwKc0ZDh~BDo-D_9Ti?SSG|h>IrVY< zAq=}x&QuIXMEtDrh!|TY42tVM?cP9$xf(<75kgJ)w_&%7KcQ zS8Ow%jT$h#h}D+ApF+!RS&hWi8$*YFM<&=h2HbDvSkgHB7~`dQas1Z$Wu714os293 zJ`5K}@IDrf^!ysj5L%9bPh-pH^DFN~WzpSF_RVJJ$y(h(Bu=f_lj&u*Rhxo76DOo* z$|>MUp&_>iO^&_z;s%uF-suZYwH-nhRN?Duu?&zeIsDok?FK}!XQ+F&k52IZ7)KVV zh_`K|NKbwH@buYlgjMfU=k1|Wl|is67p%H385@zExffuHUW^lNCg5{PJB|WR%hK7; z{v-kOSwt|Xxmq5ELYHC2fF?W_QmKX2WY4Gc%c{QccbUDTOk`#i_+)EB0!zk#e0e`> z5%esXO0b76LpyX88F69I--H4u4?{CX)oAa-Q8z<=x&?9j!V@X^$Mw+B7HvI5OC}J3 zR(8I4-CnwzM;(`it;apM)8apZZ9wF(vDlt)pZ)~$$87kOY02U%(1YfUl*erTQ&jus z8rTF_9rqWDYWH8^#=;H2x2m^(E5`3Wo-viGK&~e#Ph)SatEwCI4i=k?-b(vH_NtpK zAmVa~MoYLMiqZ%5nG4RJ$3{D=Z16SVWs!ddL(cbRCq}WFDa(ef&VCOvyx9?kP#mYB zD^s9ee{Fa<$y|kfzD60n`#43t+|#}8)mK?-Fqj+{x+aSIR`KB}J}Qit9s9^8ulu$S z9*3@2`WHBe`7Z|Xa1u##%8%?7e_b#6F1nPBSj9dnyIAtlZfc$X5p>i+j+;2GR#ZdWMLF{ro@4=v0_;1FU{c#>Z-%$? z6bT?q67OGt@e)kw**9F@ola=q7e|qUuHLwH&ZE^}7oy^U%T_vqUIY(Co?j`Jus`-v zruxkydw=$QnlSV@D(QPmxX(Zmwd38@Zw#uniTM{rZ%v|4zz~Hx|GLWVy@W>JKWZCE?<1mizrKs=@!q@MD$;%; zG)Nx=U+34DTC*#WW5l_F-Pp2a`27f(71>$oXp`DV5{?`j6TEZ>2FjD;P!LklM1T2} ziKQRKeUQ#8=_Y_NQk4$z=XxRxqF<&ZI18f*TX6Vj7aqh1>(9<4#IQFe!VNQT^%lIn zV32uJe;A|Gqg<(UqvE-#jLz6YX;0Ceib6rR?E=;q$EOzR^L1*hM-K)WvHEGPo9#3~ zkvFSRg!$C@9TFsewu-`BOApL`L1d&?l^LNY(9g4Z#Dp=#+n(5awgCr zyG8!%@dK=18tqyWO1q~n+J4;EC|SGv)Q59^qmrj{wl!9VRKz-SY z=07@{>e( z6Cd{j#xb+8l9pGxe@PsJ!&j)hmbL2J-(Qsp^IUsM%@(_G;f*}a%dTw)Nu`hO7n0Qn$ z1IBIDJYtb-G$`$tmVtUwn5Y?U2qH$=hA`;*LuSKyrBH+wW1>S zQe&MdB{Z+c^y`2OJ}2Fiw;?0xP+jHbJH>o;E;~WIM^!Fvw=WXcqY93`|o1$27 zxT}zA3Ug>V$LVPRILar_^~6SIr!+Ik^-JfJ?raj5Uj%s?j7`j+_bkY_`)B*};QCUi>)U`Xcjaw}T(iL+QT){nXWEb})IQ{#&-DdK)V0XGTrkxpg#rWjFbPI$8;e490dC-PKvn zS}Wgit@*=?z}RDwwGMrC!<66&=03O*V3cLfv;KAV5TDrVCNq@ZBks4=QgG!*5SF? zrzr@;i)y;S^W7}HFS(i#VUWt+>{Grsoyie z?4N$H)hr|=M}g1PSTEubm$Ein9|{vt)_d(JzrXp6#L=!3Ty|+%P6{9+ks$l{{tO*L zd3Z>i^x5L<(A{$e4hbj~eZd2DaD5ZQ^(hZ-PF>Y(bFt(_ zIu7<`r$s>>jwhqtmM~R7jD;R#*MJa4gT6z1;N08r)r)@hJSryUy0VsUr8IC99>hX_ z%GvSVMI@tU6+&nvy%dgbmn%u+vtCXFL)Xd0`OF5xxETn>mKBs2Q@~lK!!^FnK%XgY zAy2g9$yd~-6>pFx$BaGooy z!JULEV(4;^vV3a%BNE#MLYz*Hxxf9ktp-$@-yu)-JkT@+7HV+8iMjwYvw<{pnZxuK>H!A z#usL{U$aBI%SG|4Cr(w`9+fDuWRBh_GB{k&)lBXH9eVy6Zj)Tc4;-u;EdOS zp}9lusTnT4O(7(i07dIG&!sO!2b<(U18_lTEFwF(P9= z_TvQ(TA$!QWL=p2rr+9}eJjV@k0jmlaI*UyH@&&~lo}I$w#6rdZb-q0uwHee{F|HK zD_-iseAa8jL6u1kizw1qT9U^V$$kIZYD4*z7rK-*hZg>7YyC-gR89>q&#dZ^rmUIC zWRpqf*N@zFpN3*;q$$_95|+K_=3mgfpjQi$6H7|%fBMI)R`_aA9o2zOuA7pAVYX*?C*xk#>ep>GMPU^n zTrc==fBB$v`zcqULA~4+rT71^1f^(}e~r!t9)FQ{2HT+{j39lSMlQTLk0jOe@U2{}diG z<&4cqBg}V5{pJZA9QjScXR=L?u$W+g)IDPQ;*AsSodjcln{L_5R2uY7k00F?(l93% zUogao-}UJ2Meu7LkH0dgg)iucjUv%CzI*#WZa*F_UqJl4Fp*sQp(JVQoYGjC|bMBfw2sz#UO-Q9-(09I+(~P ze5d%S#tMj*BoFub)G#`H%H!kdwqkndI}X@pU{aojrWv6Cj>W7u;*jApdRaRf9XO4L zDa7-~9Bo_1<1KUY2+_4q2a%>mgHF{X<@$IqwxNVuMFI>qo#_1dfmlI};s zY!o^I7O`dskIUZNWGi8px|Vh`b)%7-o*P2$FHM#|ZZ_Mqk8(>3-HXlW5Q49IBR}0H zbpz+M3RE@G#_AJ|>eCBmfF-pV7hRyhQQ#g&X$7PNQ;#qojJ+&mft>`JNS@k2rwCoA zcej~{=PIr%$n-r&f{Xe;Hs78-7z?-x#C)0=KF)?DKyKBNskq~r@t8AHiCTeUqEVl$ zhXWqsckhFTKo2J`x3pzC7m+ZGra&MJ!>i6KPn~Vvr7hN z9u#~SCimCK!+bFjS&U3TD60Re#b`;HcR;e#AoBBTR4DczMIs_gX7?us(X8t@b6Dqn zF+ShDZ!avroX6p*I$MyCETq~(%X>^Z2R)OP0c{PeeYXfV3>Q6o#C1j2lCE*Fm4VOk zjFGTsr={;HEjk=cj)|U2C7;T=_HhK(>=hD~Fju0?)t2|rJY4jz_=CO!&;Q#7vVPL- z5>kA_;(tksa%093kKBFi9j&gC(bFA+2ucH0St(om1!P~N{NMV}c`}ZTxcQuwRQ?bc zGBE{R9##jQZCmM0%q6t|0j?hxM?+ZcaLo>Ck_X+%gF^a|n(g|u4MmKjzUkci??__` zW0>O<`kVLFbR~V@RyN1z5bN>Q^rLuLCoeAIk5&`1m!rG%#qlP0%wNUs`j(P&D^wMOMYCmuf~KZUDj8-42d^%Mm@PxsyG zg1z+Anj<8a+0U}#NhZznW_hwUONR;W*%Z+nLr|IMPb_9tOnG~~;AXU3l*;g`XKIw$ zCG#%&+$7xp1daMyj31!id4=AQbjWFZEDUsns03Y&rDB87R4(UVFl0Vn)a-*dDzi$; z0P(P%xe4q;c>MxEjEHeo9gK#Htk@DErPcu6Lwuentz;mTp(sn7d)k56Hv4P6>v5={OVfK#o*rZY1P@nL`8;DYFIS^F^Jqhi9Z5r#Kn_nrT5lebEFwvx+Zb z{~Jd@Rekt%z5#P^ZkhrwSnnag4rsnQ?ol>CO~Zx%Y-3jDT$OWQRS=d|*bEP#0_~;X zp9+biw{$@28lRv+6tWD~M(ZP03dQXeJ6VzWV-+eW+|CL(`GPjDCK*TwGtLIKSBB_u z(8050YIjdb5Gz33pCHdV&0X96Z4M7}K60F(fC>hm;$j&f>BZ@SF#AoK4ZKc;|Mp-ZV-z^AK>-jG|LRcX1^p^vfa+v|msi-wyof$03^tYVYI zM`#v5_pgIEu_BAgad{A*jusXzf({zgW)tl|Fvs{8y{SdyLErpy5;Cyh$0Tc!^_w!d zxbSTwmlpmmbm(Wvte3EuNAOodypd@&S0=)2W3d@1@LWUuM^YjTGr6tGukZwMV<@14 zwbtpABzHyfVVzRj#ZhGQtUM2-U#=Ipa)xfCko%Wd1=2Bq3Jqd<*aon8zYNwRvJ5@; zm`B#J1C{**AHT7iXL|yMoX`lm#9|$OI_kW&7iK9%OGJ%)1BKZM;>wwlz#0n5MCo& z?5zMmmG9RLCY6rtiuq<}%UzeY0;CsyPA1H?zvL0Pjfvs|lz3bf`gBz$!ghj;zN4NW zaD1MX?dNFFkSktfJ}J~36THOX@uogH=m)^hldLf!WlzD1#Bqdxbp(Ru!x0WmCj!(j8lgC1Lii&8+%b0DP#rG{r!*=g$nr|;@OZnxub_yUiSzL zDZ+e=q``*w@$_SZ7BwPkiK6X*1i{cc7rpWnD!gYM8;$lgDKs%pG5i);%kVzfd)zA8 zk#*=jz$PFys>SrER==oYrB-f!$)RNkTcn&poyLGq0Mouq5s|j{`+|vz85{T*nC2t* z!!-UVOsb08I2%IEa(BnQ1JW_A+f2Vz;!8|0AcxpH1qu_v?#GkrqA*T8sATMG%+#UF zbAa|++^yh+jjChA8h$xVGuPoD%IGuaiD6?u)vt7WBC@XDX*?%F4yU$}c@IAFEB$6E zEIR1Tyy2BDNrR&u>zsh7l4y$MfB_X|pCaR%FrOELyDKH7 zUA`?$K-Tb^JCGlHQ|kkUclTojy|KksNZ>(VGfgI#VY7~XrMl*~H`M7~yduE}oK=-Q z6liZ*ftB5SlVw4Mr$vy$u2hLNI#hNqVB~+;Q*i1kG-3EZQF5fYdax) zix$rh`NA(!)~ayD4nwKF$G=oDjr1}Lz23AkwEpq{uM!VR!!tMAA>iKEWjzrY?#O{{ zpQ!vTFQ$JV-Pi5{rKpXhn{?7TPy3TUbOF$_6V1{9&`Le68F`tu)eXe(*`7p*XV|~!&Ze8TY`N*m zSuxR|xSOFpvbAOx@loMl@aMIK9xjF4U}Hi~-ZeRVA8;q$0tq3qBjd1Qx@4le?$-Mg zzpQ^txaHWf4aT+%(HqD5AgS<4c`5Chr5Nuwv3u074kiZ5dF#F8hjB`PaXx|4@DK%o z6DIgQO8#9Q8v}XbDb1Co^)-HhK)UMLF%?oI0gL^~$bs*{f~sLfizj$Si6IZ^f@v0DmyW*#xs+mkZA!UCq zViBWz?)1Sd9AKl^9*&PI8BbXl0Hl&OZHZy;uoeJ`TZfr#PSw3io;I?!YuzjJxjy@@ zO>S>P_z-T%<*^PwuLP*a_Iobndky_4Z?cDHN#fz0Lfip(&(J0YgF&{!(!~M z^eoz}eRuiO9KKhBXQ!jW6S;FWZ>47o*0A6!nsy8>Dj%r!xiYJaS)zIHJHHuob<{}A4z%ZRGT)9 zw?2amMh%k{h5ZTWFQEcq#aS2jF{fq44ZlbCIa)D)>oGuGA;*i*Q@9_0D_sRJA|LHB z0Lsc}(WU8a-E)PB_-T~xkOzQdlz%XVPTL}E-G{{z&r+eCFTyECZNtK^dtnb~@srtu zJGZKR^C$moLS^ey^z|e_cax<5X%&=$Aia-QyY0_|zwkC0P+A6iaD60~v9)?R@|x3e zq258wv)p^NlNHo619!@TfomoppNZ>7+LtK0f=;vz+I!0tB@7d%r`lsCmXl)ySOMmg$ho z<&Rg&dJlyb__a~@*5RpBsrg8JwiglPHo;EvPfd3_fgDa*?c{s)kFClJs{Enjl3V4j z%dH#$8I*%sFXH); zBM5%B2V7KQR;CeLpC$)}eN(UP`g6k>FPiH;i6N!-L4nKlDW#cvgmpKrPqcIuVk@zu zKLF&#z5QDMxuDd#wdH%gdqCGC0S^!An6YiTSbW-llH8E`$y%Wf)moYtA052z8Oc<4kmJ?mCPEm+IGX|KcHK&BKjUd5cFrQ@V}K ztfoRxGoT1V4IKgGlgiUs)a`lNvwoJ??HhzQ=-A^T0#Ui)0B8Gg8c0RHY)QBmbUmO&DQ5@=lv{Tkvd)Ln}wA`M_+? zyX|p}w8j{aOSC^%c{uOSz%%WD*cNLoCWEmRQGfT1Tjc|i7PXXb z5E6`~CFqcSv$fA3G*P(iZOX&b5OpZvW79@whggWX>4OLR0ICv4)IjTc%2B>niMcFH z!C!7<0Z3`~r%0VAgFb*4iKPDCosLzDt0ln2U~GJ#;t^(g7|z^afL+T(`^*J8;6JTV z;1&R2r9ehwIiSm~139>leC@|dN9^fjuzs|7g%xM)xkxreIfG|N%~hHL;w$JXYh)~W zW5c=T!*+}xlw@~)SvN>8oWUh8u+IwAEj;l760rH~X=q;D-U;`&m*SUSN(_qRLOeS| z{smNn{{SjOT!fG^Si_@8H78*zzt&>;vasqyA;{^Swosp270nt8@r9&P$Pt zU`l2_Om)W6C`84;6R5fby~8JSZE_8w{r+0)fiV<}S#a;g*Egiv0Lqc3M)QP|pUeQM zRF`o>RK&uqokF=VmJq3W^ISF;uIiFcV#ssAq*8DYYAD>l#qut!XyYyLC-Qk4=a>^W z{PA=N5jctLM9w>P+8{&uX?j$WSv}lmrT7w=%<+1xgO)uG;k} zWo>IMI_B^{0D#LCYv=CI?U6VG9b`xsABuwt87C$M(Wf8noleL%P3xN?b5lu>j$^Zx ziq_sATP?^y3P4I(JDOO<02!L(s*(!+y!~-p&4PJ-_#z+(uljJ{ZOZdHj9# zzl)H5|1%tZVcbDIn*=7k;I$s~ua({P#hz(b!R*PS%qdcWW;zbC+;upLbCQL4c}viN z*{$3G?xV08!Jl&cNJoEz=Lqht2G1^#p0Y&RScFo<6M)j0Za3USYxA_=zGY`&>xo#R zBZ7^My}zO%2SN5#jYBNk{XMuo)I9*exz*njgJE3&s;Cs?S9qA*YL-vUShChI>9;1s z@P>j-s1aYzIxED3$X#+8VM$4VqS6Q)z`%iJurDaIz|T-EN`??faW=k=3U|>I*eb5= zlw}91F4-Pbz;Q{+QP)~%#CO}Lh}?X&&z^_c%us96Fw)G|iy5Aclx+6j_?8sQoTxo2 zl_#5Vo2=?!t#Jnw?HDoFJ*I)FOI+TZGMM+W5 zm8E-{6z~BjBLCnr?61z53=6Yp@wm)#TzYX&d3aP}@L4maixs1^e7?bnit1N})#jMh zctM(EeBK)>XMy+Uv(G9i{KL(~>P!#zKhj?@BTa_%$zat^F#I4 z2=Wk_?~5H53*6lO=RiHD6NTbY>mFZk4=mr+vJ+w>yFPM4TsO~Nx%TxEOEaXJZV*+x!iWD#Q2H%U^?>sElYV)f#62{4iOb z2{IO!R@w-vc794x1%kZ!jt^r?;VVpeKx?4ABx+%;%13&K0>aF5(wA}pCqL)9aS`^P z*@5(sS~v5F;gU#d3_e8yqbIYVXi|yiF0^-G6-X-kE!XoEh2B*3IOAEbG4lvf{&sm! z(KUonpa739YmBNBaF*Ir$o2TQdGF{oIL`~cuYu9X`5l^(n$Zp;{(q?PZWX8j3`dyq zIZ#)v0%2lt3r6TZfXFy@&POI6e3Kc4p+YGaW9~`*>YUoQHb3WO8- z1Lr2g{}1QJ?C+DNL7QxY8G?ar?$#B0Z*a8IAxuGB9ZGuaGFhTJk{5lEdz&_wPG_q% z&1)sJ^*E@0^TX0fUBG%g`zTwkRBQ;VX28AP*%&k1ME|Qv)_%fPzfuU0eF={@1W};K zppGJD;KuK-j?cFzSv;Z4b@rbh>ymIcO?**GVRyc{5(Jo`erhwtqld@&PYqZ|8j-qwm$4R9O*)xYXNT%M*taNQm;z+(X} z1BhA3orWOuR&O+#Ble;Nh4e#(Ua6sfoFm9H!e=yB@bMJu|I%;gEJLQtLCs<|V3_gGN=g7~{sedz0BX)9l0|$^{%pg?G)Gk2tJzH+R;k2%8zeXbnEL!vl+RUUhQnc?{Z-_R+|ilN6!kJiSy_OF$i ztdI%2;2?`nCmCdQ7FZS~`u!Hvt^1$8j0#@Hr;sb6R)hdmLxxav#E^u~uEBeoN0Oel zn&Pwrgp+mlUThvxkR0^?CmL-%h`a?jt|<_EQShgh5Tr-!Bi|tydInb?MIx$JHse)% zD-wnKwx;6%0PjxT9Z(DqDBEAmt>Zj9ojz4+m&4Soo_B&_3&#L#JKx_Ep~@}S8ctLR z0u;QU$~~11MJF5aPltA}LcaX^SQKiq?7xxxi)aVuEo|;=W`=O_F*;PR3dQ;41C#Qr z+!!TXxMFY6m3-KrV4tswn4F)UKxqW6qruV&Srq}2SWcL%B1<8<#z5&OuiBPric`)B z4S?i4jlx9%BxktP8du_`>b4F1KZHyQfU&Pfa~qY>{N@3wHDu0aU%`X60qKYuJU1|n z22a|l_*f`ZX1jlR^uzK|=+)Z(jL7dLC|RE#Y7d06qQE%T5FLiz8;foT%BDH}H6~mM z8eLtd_^m>ocJIh)LXJF3-FX~nsO1Qq9O%Pf_-*zW!pEHnRD^Ah%l{4b+5>1lz#SM1 z*sS4MPbT$$4rDZL=laQ+(6`H0hw&C&fT`yZg=|Txe)w%1z+G-LWB(k>MyGEhT0dcS z?Sh1pzm?A5A~VxTZ0mBAX9qktDGBTxWd@t{aXA3`M>Q(pt~Tt~KQpzr2TT86CtRWRNqu!6c5cGGlHCqpdHcwQAGRYc$b@{(Z%&lDhcrLcAJKC#8YoPvyB}GE%otqDfg5Ur;E=6uV15e6a`FR!w z#C!MuW9+S?s_go;Q94AV1e695L`u3tT0#_LvFMUcr5mJML0Uk%7P07(?ow*eB`F~# zUEf?j@BZGs&w2Ma=P$?50W5CkJ?HhiYHp-^mcBzd8;S9G`$fvF8zaVO(2r%9fd2+n z-<8d*`{4VU65n2ghGeFyJR<`1fh6S$q8d`Z*eCr^PN>MPa8+J_4Grod!9j2`mlKPk zldBp?5kKb+W4OBD>X+Z3k_$}6I}9CXQYe8Gp?je|8H`KdG_aHGKM6XPs49NL^Z7 z@T@PJqWnG<>`TW7G=O9ya9%t|i#&gV3PX66j1sfH!~Ll6PM1zJ)7iqICwf)o4F}^v z-K$S_soGOL;z==G?i*?5!2><_mp@DgN7)5UVACqRK~+j%dyOPx)*)=$IBlHv0mVdBD8{ zEto}xWYAv?tvG6A8;kFI$OO`Rf(box&(A4!_{s(VG5hBs(pQ+Fokb;xz(913L4T>o z;sUs)`~2RF!-kZnQ~BPRE3Rdeb=P-LSWmKG&7As8g7KqMX*7+OMgl>;;y0JxmnRxg zMuz_@X!e^hKj_ks4wz^|))A^7bD2%^e}0xAH$u za{QsWm>yT+@HG|*`7xkiELpAZ*^^NCNk_OEO2r)iZ}5!nM|P^G+jJn=A^oX~*(`04 zhHF5oOa3ly1wKyse@D)j&&8f&A=C(oGWQch_7gXP*g}+8e`n{!9X$3rej)Veb1J%E zG9f2wJG;m2_38Kdf?%uA07-fR*5y{kR-9PW1ob_w;^)x)PqYGdU)*Oyixm~ObBN*S zy@W~#NT@?d;2Rh-@>I9HQ;y&(=g_Wt5`XohnThP~KCCUZ`!N|-Ry!7PpVg1A&+~*W zAquszZy3TS!D>;j>JT{pctXQw*7R6hQZ5Bx05qN z;~MK_$nB}6!RAk$v-l$xQbSV1V=n^?X#6>y9>8Htk^)wY+*pLQ@jIvhqEs_87q?Kf z%0%iiI^r10Ui;@~NA(B;CM?b>_NB+kh&+T?0qoQeGpAbJb$1J1kX(VTQ?^ce6q zIAprON;M?EZ9_ZGHdx`rturdyQa8+l-CsNIiLmkhU-aAhfnePZBbtd=LXEbx&joda zdJh=uqhvxGvcN*J0-hx^c!5bzbP+(n-Rvnoz11?E+&=rc!vfAg$u0mc$X1M1rm;4b z{4WfEa^GsM**=jqCJp0Tt^OMNnMe8hFcWUlld?3?#xIk)5nyp7UB8jce**03GCy4< ziueOimSzD%`k4pUmg|%r{z$0z$F^4c+hLUxi-8FE{g(vFhN?tnwaBLJ|sh*u?Bt@ErYYUxE zQ8;)LWWb}CIGVSNj3#|XE$UW&{>fM-+J_i-oVey2n~QQP|4=~y6y-S|N`0%a+28{b zD`E#)L5Hte3eVRh^|?_H)RcWBeFZwoQW?F~p?v)xGKR)kTZNTAxrCYrH*3Mrc66A* zQ@2_SqS7I6@){~p=h;`~gUnL!EO_LAf{z9}(d(VN1LQQbW};8>AcRaA7Wx;5RuI#p3>F<; zY?o!kLIY^5_%S86$IHPvhz~Pt11HXZ;VL&rl)~>ey9gKK5Syh?3wkve@rwlERFrhl2aHm0-rg} zwb3!4wsw#_M=MSv8zqq7wGq>+_?0tl{7y2(GPIC?3k}RVKpEEl+QE&2QqF5L^~{9N z0T{Sbd53D6KC=7lPa61s8=0>#ly!k5ZjnN}=K8(+qo&EjG#DY@Pnir1yI|oOf4nSv zKhm2`ECQ}`FsVzc;h>vx;_Ccp0;FzodYOzYJ41=g-u)`bj#l%t&Xge}_Dk)mxVgS8 z?jVp5EmlHBNJn)(b%C{MN+XRcXD?-_Hh>*~$7JRn(VXd`_i5CoQ00RNbg8G>hShkF zY9RG~6Em=i&0ru>lvk`64uFV>P=q#w-QV&70=HUORDcWLQI?L3CJBD z;WwyWUh6}t=5J7w+K@o|Q|%s4e2fiD71($*{0h*1E@Z}jVNHDMM>`KBMf=~GTxxG~ z{YlM3nbX}E_RN1Ic@AANm|D=SLxP@ZF+x5;#TGNX9z#S+Z~(0IV|StNJbF)>2#zBO zHMhj)Y>;Ktkk#m2u9v88i_3K2tBmZcfRV|1%9BW~#^cy3+j{jrQRKy+UnFX*m77@+ z9BOWCw%?8s;Vh!XH_dM>!$?EWe+xy|NxVT7d`1#ER(LzyA8mf8wJ2) z^t{XLkf74iaW`6SH7%1FI_*R8x}?B&1@py=WLOO@+0+4^8{v?Y|%QzB` zjP04F1e6LsrV4B$nTPgWb49PxBRaJwiHoDCw=PqbY95kqrikxvn;)mn$r;Hc3AGQ^ z|3Nwv3*&nsgi8bA+%y^>%_JaxQzMQ504RYne)bMf0X&iV*ame!ChW~)K|FQoLoxW* zAgflj2j%ahlTa879xaj+#eTqeKmxyGSPjRJF~)yi=+!fYeY7mH*K{-}d!AI`7St8+ zAHduc6%S=NLq?(38=ZddnTV%f+lx?sh8&_j&uL!o(Ys^3Hl7EV%x2@R@Crr}wytmY zXPEh8t_Vl+?V8Wmi!_QM>qubtamxX~!Fph1;>uRLVDU8%P-O-70 zU;IxBI^T^vK5lm?9Eo7D-Tu?>gc~? zWUV_3xIjB9VBRC_@&{AP1J-+U?)2@vStgIONQEJP3NbF3S_Kmx1LGdw2F%m`d%9S5 z!^!9;IerAQOixkr(udIby_xFb*LE!+)O*MGeQ!G6@6t(H&~s{D9TWc)q3QK23G3)M^}i|5TJRk9HX#T2 z!2jFa*SaRj;y>|Me{P%{uHO5{uQut)2=dFO8$cV+WGnNF)?h?11&Q-nf($DQ_MP~% zImXXz{uc^g+W3FNRWGG__Vml%UH+v$4}|ppp*}^#TuwXe!g>2;T=qb0CER!kmpQPy z%G9VeLF?prvS+GaYgTt9`}tVjscE$m`CENfJq}M0yq8!#Z7gb-jlR%3zsGwI>P?pM zvvdUs?|SqW_jG+Y{p1317xP41P}}D7m zq?0d9w5**Gk-#qSBV8kP+dKi^hSvK}?mdptk7em0+BRX%skY78E{osVI#}JVK};=| zjQxV`w_dCxU$?(DHe@d5g-XCSH^wU>LG$&&h)&L2@fDc@`rHk9*uNI~xA||c_cVa^c&a=P zK98Lg6-E<{r_5tiNJo+fCZcg~K^4T~_1NW$D) zLgVe^zEwx#`ck+s?PbrkbeT@7(2=*Tj@aZlDaZzrfp?0k*S%)1!yleyeck za{Zytew%jM-QAAyBet5SU-cTvgp(J{@@`f5E%U@QN89ujS>R+OQ2XZdOJUZnmn<}C zzBdx%LjnSUXv(CB>1dSV95c%~R_bdb-0iZ`H>d$vV)=G0*F(;yh4w?4#^}Xk)x(kv znJx~3+#0|?W-L3g%SZ}U{Q+33_f^xhFU5}Etkej5o$bHD@&}_&>Rt3ee3?n4mF?&% zPdS-5^+S*R&qz-quNkm&JrCo+V4AQa-;VR#%BM~<6xcJ0xTlB|CoTs!c z(ctNEOlyO>bxE+zB*#p2^%r7mk3aaHEtm7+!cw=oG!H6$qYWg6^p zfEqfsT^ae=Oit09`-V55GtO23N->_M@vp1v-AZWx z?5hdi95Jz;$dc`lU;gf^pBG?gUl=WV=qVi0_7q^+y{)9uV(0YBsBw6YUH*Kkz{oph zaYTL6!YB%1`h06NS4IgdoWr15uG;dbT+oasJDKI8 zTRnylOUK@%yqS`zW~l50u7Wo`(bUJ4zli#5Q4ux>#+Nn%Md3oqV$uSMZNZ0k>B6E$(vRw_C;b6H1)k z#*B6%`lnB;IpFU(jqTRNN4!o=53HzrM%A8Rx*RAn^GvMED`I!Q%rFy@-Eh9A&9qFzY_29sGD?kzih%5|= z!uZa}9wug9yd&(*y;ELWW-e&8`6@SG2>&sq{hSQ`!z^B$oC%CnOQj;~G7XQ0@mKjY zeiI8V^XrXn6Iopz+O9KWCYg5GSFi^IbOSVMvJrD#=bva*yW2VIA~`T4KjO5Y426q z;F?jqw}z+A_;-rH&nO|=j>U#OTkm6zM;!2##yLX-&U;m0qDf3#eXh7WZW%15Y@4qt@#=VMg2~yd9T%@^;bM^@XH*zs&C~c@JhdpT@?^k=L?#na48|+m& ze2o3$XIl>Ba?=sp+%81J74nS=+X|Z}1xBo`;h$S7^?Q{z{&+0;=V}{#>qH^Zt?&}v zDKByMh;d=T%2KR5?APS>JBC$~70udtj%^Bn1ycTI9#TRx+R$1(GoBxp{(Q%*|F!(p zS&yTd^POp-lPTMYgPz)n1)mwjN*wljGSNjB-|5AM;xZg{ z&?bOC9Rg^kiJ#k)+Y~s5#W$$|x6q`=3RX29QSmFxo$`)*EE=s49oyUm&pom1J7+Qc zXXyg=>8~-V90$3y+1EF_*c=hIvZREDq2kI_ogM@1ir2?wzgS?Vsu|*ns;0ZI!SAV2 zsAG5DEWGh0O)WB6;`R~JnHnKd*#0ri_iDw&1oahOYs1ZjN>%A~(q|9!I9N&TjpGog zI*}bLkJx0i7 zO-^mtXQ;&Z^Ric{n{_S0+Eo@=*`j<&n%rYWMdpR#8 z#xD#@2(;x*dYUu!f-^_}7gSvqI0^jdcO3NJ1&yk`NRePp8<(Me7pP{^^E4OzDreIV z{d2-5#l2#n8Q3nGXCxGI-JJF4SYB&hxD9_HeV9xVsw864efY-P6&nRrfsF{Z_5trE zQ$49n;e5j+`p>jEe)Gk4$z*4eT>WI@Ui0G_?Ile76wCP5yi~id4KDLirxUNSkEgV4 zo6n_%P+GrEk!>P&wiHb8C>i4i{O8Yx)bx~jz&u-te0zThq%hVj9d9tA&!nF{ zT$!1X-|jw{X5RIoYLW+rE~*z_qmNa6)h(X9vA_ms%7PWpe1YAYMA@q;Q1e;xJbhHIh?2NAz ziyu&KuZv%a>mV83k!~ZMtN@#C8VQZP`@LABIC|t}LoqcjC%OD|0iOnfC{N?@BgDVu^74h7yCD!$e`Sj&4va63x91DL{Kx-M)H9qZL#+IYA=By63ivic3cmh;$lv zdZ)+@hYUl66z-N|m7wWD>RPuF8ZRfZ?l|dWXp#S16?XH3)0)@$F>-o=Nl3Te~D3kjjjY}pNjAAdi?I=wr$na z8s#X;Q;4C=d*}ca*qxDH`98XGkU>r9*>4zD4~%wR>yMY~2^T-k2jc6BFx|n6nO%pH zd&k{E+_+U^J_}-=dGlTY-(NF=cyRbzyNPOR&FncJ(EQ+>p5>cTTnEfJ<^=xZ%q_1u zoAy8G;|M8ZH$5PP$o^uC%xNMTYsZ;xgMl=|7I5XG1znKN#Eg|&7(KB z-~B~H8A4=xw(e1%hca^Xqgh5ALHqu-X#e0b% z8bpTpYvK13BEZ8K`68~g!TIyFae$*SN6k!ra#(!jpCR>f=u>P8azy>}=nrc5CPMKX zM8LpI;~BE58}H%C6R=%gfcQaAu`Dfn-`wy5sUr_`Mg%I_FOUkjc z%n+JUE_w1)LYrJHh??0Iz^$h=f9OM~vb@g)_DeIW!U2P$S@!KD<=&5@V2{&z*C=Pa zuToEA=Rv(ssT(s#Nl?@L2yH^S{`Ng6?gRMPOOYU9{6Ma+4@~`j0WV!Y8Vp$!7R~G# zbXtyE`h0Ha*~L!f_bSlX?6?IA8u%oMLD|s&joT8cGw6@mDi0CPg|xj-+U z0#3;&avf+|c|{J|HZQf)X=&qcZnj^bs^lKC&|u)Wqq_`_SmZ)jsR4o02N%6LsSV18 zUwNs6yI(-XN4KprC?_hsi&p7;ahLlpAwS^+qTqG5@;r-rUQ&$eeOj5Gl%a>OG2b3B z?!~?@LTkrjMw)K-E6SW(^+d1F3Q&rW-5FY)xMg}0xoi+r7-*;~u4nN(2G%`+zg8gb zLM^kwq)hc+R8}Am!E+HmT+oL$`Bvc26GsUHTA9oM0#-TAmb0ID$Nj8jIluSD zR3#>!7ufUkbXraUf?9oJXLv!F!fpH~IbByfP(lEu7Hx%qC4WkJr&5C#T0>i-7Nt7?jvS$ydsUjIjeM7mYM+VRA~w8WF$(pP;X5~ z2{yGu-SW@=OJe`He;7)JQ}~q9TxKlS9~uTDOP&4^Mpk#jI#Y%yK0zzpV_jdAD8Q~> z&B@~U@FJWdEZ=P-N-BS+v8>4PW1lrtCek)g_WJlm#tA(cezWd?HB+J|7+H`f_0Y*1*_NwvC*dMz(D(m895fN^D<51J2ZXzG!AU$k8B+ONYZhbz{;3$EOC1U{O0n26_zO~+!7_z4$} z&y`z*vr#T6JL-+35u0JtZ<^WxJzHjTb?<6=k$Vbwc}|mVF9DnaFQ#=fg(;}~)0jmB z;*uPmFW#Kg4oTdetMULjW~|Y5&&x-Lgw|`{UkC&&Y>!dVv66s2EE$C0l0YgUb+>kL zp$V`xX6J`%>!8*wgWVCdgG_r?lx!4Ndw{=K90DSsZ7tH~C4O@dlM0&38rP97c#sS& z5(0f^LyuxajqU2!$d0DJ`yisrI(G6Ut}RCf&f5rl4EKDWQ42a4j=b->*Ew#F$9Z4G zVD>P=z_;KVl~OoVy7hD1q;e;il+Cr#F0M8t>_LB&`u)5R8joIN#Z`izeap2=<%k^R zLwT5J;5GISQcYc>bZ~MVb%7boYeQtQuhzW{lN_2w$^AA|2uw(a=e8~e4P@DD=`!z83nzi1GlbKx(cF@JSh9!Vd6=qK`#y>+N6Xdw z%WnoPKI@JdmtXuX+{p0Hcn4Pg078*Z%xCP{;y~Od(FY}3- zCN|N>9FaO;msbI<;+@<%@M947TXjNsFr9#9&UpL|xW|oe5c*XQcKy z3^|R7jP?C6_EtnrBPQ8bC~rf|RgTnq=cmA1FtOv=3ss_Q*xxN85kOmOJZc9J! zXez%ov5ktfft7Hvh%hTrZnxYVb8i3GbtN9cW5;73%{b1xOQL-Ck&ML00kFe+uyuD3 zZkwI<4E@2r<&fdy?L|kc=1K z%Ft;;ax?n`K7c0~odurc*1U9XXnfXS?2%O{k2s08h4o9R&VH^0Z)D(meM92bJv~yF zTAbd*OOqZ#hCx>?1^W|(jXPj90@EzfmWAlM09UM1QR(72y7QM-$`9BXI_}-8R7%F)jIBrAp%B82{1g2)|Iip($Q^covJ0X9fZF z4E9@1eD{-S^WDHtizk3{umWBIP6;JT`d2(S$WFh=- z#X2ketQt_&=(D#g`8uv`QxCF`vse^k;DBm9v6aMUO}j?;TBWF_O~#v-IA=I-##mc} zyxGXu2lwQkP^)L$)Rt8Rboa+aPa38>0LOpeH=hifD<>Q(Gq%D3io?Biw7mq@%oAz% zwHitt@d3xKlNW1&ele1KVG~`O#{nsq8yxY`g`CEu@PvjKCtaMk(codQuZ}|5KWJHf z0>9htZsx^Mn51?Tr=@kmgD4){22TeZ;}=CH z2+{IXJp0b;9upNam3ieL@>sduCl)%x9Z%5-M1TC;q7%93GT>VUkF}qZ&|#Nku->7h zStL)D{zp2Xm(OHiDh2S1#;OZ}GBTVqizoAgIOSJKxs7aka&+2g8-!0n9uL7@y*&&| zOPJ<*Sj)zgt93D8ht7atVuqaf&X}dz?hJMO^n2B$;`wY%ftjixE+jkT7O4ee8nmv|VXbknPyimjn~uJhGFw6+w2CR=6? z`O9NRN4*dCifk;xag4-9s|gpHbqTqSEy0nCxqucWF)edCb8Z` z^jckR|EQ7;tK*Uo_x_}-~e;x1@9+()TD`x$X0f+GsHzF=xB z9Li8jYXEmnDyAt1G0_N?>B{^N1-yZpeiH5zn|Xc+X^Y5EzCgvd)>sWsIV~V?Nu;J^3_Gr1N>p}4C&H}Q1O9N-+kTX;Hm=xt;q2U-)d;|t$_dL zaM#A++Q3jaDJ!hr7;N0!JM6L!yCBq<45;#H(~+tUeI*&T6>Q&DRO~aAH)3KS%+T0J z$J_wATy0v4d`9oSq%&5*t58Pe#=Ku2LdL1DTZ84Y5+yLRG!djiY@2@l)uQs1n{6lm zbc^pTSzM?cHY&rBea*DldCBX7?}H+6M`WPWd`~a6Ztx&6K9W{qflasWN9^s8z~YxW z11QA1sM9?u=dG^p0~My{@Ne1G!cnBQ$DibBxRA(=8hx%jRA?|@@1yiRmKl6YSV`um z?iq@>3db#36JXZ5d~}%ou2ctvcv7c5wo7#MM9LzAQhynFEceJL2>HNvmg<3*ZHis* zN9eRX-mkIznCi)TIcoS}+M$@|3GdW#Qi;5<%Z`QX9aE zv~@LSbxP7w7!bTB^k7GjTaXZUUtV6vLaNNsL{!s7>nZXrC;4SzmP_RD zaOk7Gj2gn?2$?ti@6sQdRjJ~f0oJv)Mo}IX#5%6~)kOKu4$W3z-y^0bx5pb3O(};U z4L!vU7wYM&H{1rzFU9M>xa}#~;DrsC#EAwz_1DAB#wJ~Jz_tZaeeE8Y&-w(d5p32b z-~Nm$9o0;~>M;c;cKAOhcD}w1EQlP!0XN1HM8bWZk1N9>dvjSYcr^l5MeikHNX=PS zKY}P@uL?aE=|n`BqhKt(9WQu|ur>sCOqW!%M1v^Riwmh zW9Gzo)2EU$PJWPB7TMkljn^`}ib_*X=I&ZrcUtntrf`XKcn=J4 zP}fqEXrXS&(g|W-zCo0!5Ww0P@Boe6Dy?XKh7_I`q~H0}II1b6|1so*XQi5&zE0^c zL=5afEl#9@b0-IRBVKrtuo7i#QhrFF^3*H6J>KuD<6;TT$7+|M))um-9=`trP=EoH z)OZ->eBS{CnE>Urs)b ziPs*7y3Zy7$>tlzsGX_W4&Q{j5RrAt2r*bU19iSHh1Tvj8W%bIEP#?s&Fcm-+Pq^6 zP$ouPx&Fm?e8ydlsHa?6t0vekCo4JIg_9=0IbOvN$84F$#9-EQV8Pk1 zhzs5t8S^jUwxg44))_MZRQwi+?Osd^Y0W^j)_-BEWmZ~5H>KMHgIkHdX>w>=P(jju zfQbMG@Ao*zn^f3z2+llK*zd8j7=l&c61Ab$&w$rf426(f2!h$RaTh!8k#xwt>JY0a z=yWLttqunFI}#E8?6#XeR=Qn3MH5?#sOne|9EW0fDQ&)2Rn132vuFp$W|(gMs8xwf zjTD`M*=McS$R_tGzME3f1@vk~m>nwG>w26zDZ?Z=QsfXp3LeZi&(I0g+%NOE;_b!s z^jAqg7@EGu5yIc!%~0hPKe=R(Qp=R=wrBzG+~ZeD6x(1rd;7?8WDm-tu*`=TbG-_1 z-zR#DwQSUHN77_lw+V6c-uIL7Gyv@c$)Nkkxz_l7Ol_tygSw>kTZ-ZPl2@;2=~jafxb&LaQ`z zhF~pz*3Raf?K|l#b36%}INAgwggkqa!0LBK;~Qb*MBJ&pB|iF&{-Y51?W#U1nN;JL z70Q~qmsZt=YzZ)?pm!Lrv{yyl?}#U;Rx z(&SzI{r|bLSOXT`yDPGc(Pm}sTmAB1?apAw-}2LnjB8_Jv6-7kI`$E0Ev^o0xu)I&W~ z2}c=-0YKk0kpmr7Mrr8_GTU69lS~?}EPjCKn|)-brL0II{b3#}2hJ}9NBYkJJgb3A z&<>J-xz<+0M^Prw_?u|Ehr3-bTS8TZ*XO8Hq<#U^b=%D!tr@Y(yOhwrO-SHrBT#5}=jC)YYO3t8mK(y3 zB`rEW5=wodCf+Z6d8htCks)tSar`HEo1ej7 zu{CJa>r|fYA{=LIorn0!ch3FL@0DNb5h-jSySfMoN6QQ(luA#O(62}Ue3SVXd~3ae z0|t{aonSOH-i#5F^|KLs3~=_Az{fmq?D>}t0VVQNYzED9?Si18sxgO$Y)qRSBnNw@ z>l*A%LqKY5%CuOwc=q`cGRD92Oma$6SqDUFD-#_-pH5Y4!9K+r>8nr$yR||<&1q+( zU8ayr*bWXRTN9j>O>xXG=%e1OYGkLP9d@idmVwbccpUq&lhfw_PbZwcPHR{535=!* zsEaoIluX8C1+GRF;ScINCuQj%KQ}8zN4iYBH#0+aAR;#P9jPOxiF#(tb#okeUC#*N zHF9#De-Teb6+Bp&TnUCzep2gf^qqOGoA4)ZAhO@2W@?puocFsg%L za~N5R234(qL4!JWy%gDfn+{3|D0LfkYVBOwLjm*)fb_j3mT=P_V`Z`UYIhjZ$_Ogd z-HX@BIrVL-A@|A`NhsM7jmUZQ>%ZWiSNjvS9E}rX9Yla8ISt*6D4qy_f91zCzE=l% zK$c-=BAUO4_K|b;>ocMa4A%^>&m-iHnX_U^bl1;{KbL2tr9Auq*~$_--@Vi^=J5|I zsa^-tR|U|kY7QxBpAIh@zkQAAPl=P7mtGL(t_S*ION-K*ds`@ zdNZgM!fK@9c&h+-8+?(VpJ|e3!e)CT#`!9#2^K4r@sDoLbNZ^6i?yCuR3Kx!V$72d zN1O4s$2(kA^m89RwuFuTg-*))&+|aJd2^bO{A?p|HXnLwzti^egqtAmYk=sOqIFJl zc^iW?auGfsZhZ!z*m+R4buwcH zdq7iSm7y>_G#^SawS@uw#&Ut5vK>%xh=SgK0sF3QZ8xJeVpcO00nqGBMoch6)6PtT zjYrN=wRlis+TKsZYPqrO1Eh81Wll z2-bsYADGA_R}Ym5-K1h~c8s{yzLO-5de}-6XWhr;-6dWvUJ@O@ z`d^4Kq0<8}9Xdnz&E1)PNWoFctJ9qkS}uH)#gz`9%Uq4-l3*R`=wKYkLtPO5!txcE zoUsIil~LWSK;g!}0i+Zi00kCr?^qyF(~$908@t1?Q3AufcC8I~lsidDlu;tc z*egQbwLGo4fI+Xd2g$+aOG;E6%6E9<4&qn482DYupTiHPol@q2^T*UJ$Q&U7nb9#Z zJ>1QSdsz`@t*pIFtE2qoG@zxq89MFJhx`UC;2Ts&UWRCz=zqdvjt=MZpC6J{XeaC8 z?qzHvs0LmCH3|S#rwM|lWm`jQ8XSNl@5+@Ln@$7q!}78TRIIJ8U`<5|U-wiSn>_X) zeGe1|C?)FxHAe$!@j}v9r<;yWn-T(5mAqjvlv?U#POW#~dNYRd7N;grJrs5-5PO5y*{|(JhDFDwVH(?C za`(|5C_<_Bw{)X(*gbRL_Se{B09@gtCrGXk*p%s7OS&6;z8p@^K4A<@UU8lxzY-hK z4zmf8lpK6gnH6sPk`BV`MlencALaZB&x#UiV_7(dXMGb90#*#0XAe)mrAvSzPF@u( zu;btK5Q$|-93zJL^29!{-c?%v@qUj>#A-`K;)lOAh0J~ip*_sLmV&Y2NLoEN$)#l@ zrhB^#2!ymnc0VuXCSnsurnkn~I5%ygXU~G@M3oQRS{plLYumR2uo?6GfU24NkjD(_ z%IX#EPWhCH8?#C-Z&?}j>Oq}9p{~{0J56b~G)60l_F|z~kN?3A9$rMQ_vCq9NlKdq zYHvvpU^6-05g_@aLSH?;Smiweh8K)fU6bR?v6D%03Pk}CB)W8!Bq3%^iLE?6|3Q?7 z!(Ktr&WKGS*GwqFdp~HAGrNk=f)fM0TOaMEjPbv{4?Y}iO?e|Q;Q07hx+hSQ3j=2% ziacbN%l}!_6g12tD2mkc>GKxcXVK=ka7zF|CqOZa#l>F`O8JM1^CMDY0{S2syXKB$ zWK=?5?MXatdBo6Ixkt<)v8GsX{Yt-nw>#ncp)TB8 zO^AN!(JPfQ6!H_ILuaMnYsW;zo^g9cYZV3zs|Ws76d5hJrpuO7=RO zeX86ut~z_$?wD}<4L1w~JqbkjCGlW%A~+c>R=psG_1S=KX@)t;aar5jRXIcIs~C}j zk}{zE0Ma5-N#QM=@GizgE1`QW7st?gxB_A!)45=09w8PCkrZOHGz(&gQ*ClSJPFky zV-h!3D4+EUt?mmu29jg>>xeq{XR0=Uz+q(N0EqMj(tC zOTae}{_Iac4AIq{z%!SCDoNg?D@Cmj(v&W&n}+_vL-)a6B#1NYzX44aQ^?)GB&M{($K65Ovk18l*<%ccAJJb7tAOV!%F1@(_F=lnCv z?Mi$?lCG{B{}cEmn)Tsna|U`ivp(c57ZV?$x~N4(amuB1KE>!u>;|l7ldL!ewFH)r*A;ZS%@MR@lsA6MBB})Cx!}r~#-Whn zozVEb&wJCA`E8{kcn{k_lULRf5~enh;r!;YovD(c%qXEcAebxyG1l4G7lA`|gdM^7 zm1{pBBijG-SfAl`$4mw~^tp=?$(LSEP}zHx4*5dn`-(~Acgw9$aS1o-PnYM%Va~uG z?Xph&UShXuY)9q}6ZXmG2gB>;C~6Ue)82yEVNp1FKtb9`nApR{_X=b{I1@t?>~tFgrD^Uh^r)8P7oL<;Y%^lM3piDsuy0 z%)ks3T=g^BWXm5Y9T~;@=KEULcw&?WKg6}=?a4!H6ZV}~^_}4l{j{7h-oFis<+Lcm z$#A1W65BpKwF$Va`Eas}*(@RB?Jk~H6^hsI0C(x4^!s@L*0gPktqIF*W;Gm>*QX_k zCh6@FISw$1OVp4EPo*aB82h8xX&rMM-cbkL0Gb zeC1(vn#*JxZ$Vd*8AxB3=zc}s>$5%VJjm7aptoA>a9myW;gvdkFBs#eDf8W$hwb8# zec21LohdFDf1f3i9u2Z^vz5pBJqn7U3wV~_Nh-(!>i2CtAAHnOl+kU%G@}M7UhV~m zoz9Jw`%xw2)KXPZ{;a=_`$BQ`0-GuE*epoXW``09Gc1PSD~gQzc>Y z1J7P+6bpJIhYQ5(ovQFJ#um5vJU#$aH*@|k(WzxAOJ^@0Hy0nC)(i=&E^oQ%FIMzm zyr0gzy$3KvXvE*;`2BLP#U9CWD)IbfIX}aMyv`g%3Z?%r|HE;5&_kGJLnp#@rhq@; zmD3CR9p_0!B+L1U@E?|Q3OikMA*cGt$MHXKoY=;Xv&8L;lh9)!=8yFp*=bS*-(^+9 zeb!hF;PZ{3I0P0&x_6I!K3v0I`IRt|;WT7g_K#rI0QHCmJ7 zkF*LwSR+*KV)7AebB+iX z5oRB*_ZQGfJ%19=_X%_+%}4_gxK4HX5L0XEC8i5TKpW38NS9W5clna)BKL+>(vL0x z^7B`9YxhxO)J-Ivce7-Ci;`+Dezv^sWkg0@0&=K0s{@hTjDP+c-zf(R?0CdR(;i5A z9ip37o<@)qXv|#4eSE@VtRi9z92>>11P9s?Ix4YC9-YPX+LR>Pb`wO-Qk{z8T&PoD zqpzDoKfZbLk!yytGu$O~JiOZjP(IqW6qJ>mOP$83#^m?0(hmkEOfEn(Ih;V;%ZZ4F z%3{sU^*77ex3RMLyVMJkBP=Q@!@Jd|l8&vlVpE?9`bB*d# zW(+Tkq8Bg@xiZ@EISJQ7CuMKsj5h9_vm!<3hy>sC_Slr*^u-^IPT9iqFJkxzSveBO zWZSWb?AC(z+|sB^*xp_rRkP3E>kMysQYu&0bg1Z~WO92vr5eH!Zu^;>4QhzI-D!*r zMS3^nP->%8Mn7SrNXB;}n6Q8mjVn&|#YRNoBALE|u=#mU%17wCKR!r?)DUy}`Pr3* zH0Zp{yFx>q6mR>2H@Vela&aFmDqIpE6+KVzS*=CD&%=I&hvCKhnG9{>IbHCBxM3;) zVP=Lq6?w01Yo@!SAFbn}S-p`C#p&{J4?I3hdR_r_?>c8oo28kURZ7%)rr*`0m^*ppH2euQOXdOxr%jUIQ7lf!Fx;Ka{9T>q-9 z^3(_Iqx{PHWS!dorbWe6|I(sN^c9mx#SbL(6MYpbYDrQbBoOzy0R@>~PR2+pG@d=1 zlKrF7vU9^8SLbGbu(-(Mz*Mr@e&N?oQ+hO#OEJ@@6exXm|LU?OUvu;spXUR9ix{MoPv^PA7lfl)Q zNtVb~y)<^&m+T}dJYr@TTb4Eq z39quG($gafg%pKs<2~&w2x%`C?G!D zLisS8tgM{bfq?Qyhj+#Ir8T8r$5KZ`DoVmH5S^CQy^~}b;~SPvPmPdlwmh$f%zL|S z)NAfPM?3Ov%g&|c0nv@lzfo$D>BA}c7AI?X*iA0SG5_guuA*SYo8}9^%(>8Z zuOWrjsJ)VE7I`}`#g4z*6nPn28_myYcFyi~k_`9)k&g#&Jz5>94YHj z+-e#=Ht5$R$=wEFO%RIo@-x$$*+7_;7_7&UQjQ{Snv`bSfU{xLugUxSYlIG#Rkj>6 zxleQFS5>r$j%IO34{1)sH~3>?1AlEVedm)^&wWaBJoW%8x5CR}AAODH#BBVtzpFb& zOkWN&2N)-1r2mX4lh&XjHIfp3c{#5C@>hxq$xVu!)N` zKn4%JA*1#$TIUD?sh^qo3sR2|4{bb8%V7xO8c$BU1;buD`toXyt}QU}f&kC0K|P~3 zl9oi42_B>1v^`0s5ldYKym^ulre@#O>o|Kg`UF7-Xqw$4FGVFG$zIXYB}QVXaAvX% z#FLYz`is1!PBn*_+oV?pUJH1lvMO##L5RkAuIUIJ=H2m5nV88FAJA4EKkx3{4{t#F>bSBl1}_9`;x>(Saz*G}JPCmMNdy*u=*hCAJWQd( zbw(b8vvh1@)6=k$r;N69JZ}!1K0NcIhNQI$6hJBMHP;HdX|#xzbTjLuw$4SoKuE!tOape9v+BL2E-$ORLh zOZ~XRbxisoT4L6Z{7n)fi=!;q4PEcqb&bEHWqxT&8#g0ZqV7(hx{UZ--Z~y#0lI($ z!A(FvuITzgyFOHyxYHsHdo}OBZK&Qj`k?r7q*5Gyobd}iwI7KWg1RZo8X+bVO^Q3c znfaRLQscvQDZ;90vr{E8=Qwt=d0w?~RPExB;oj>Nd~q8C$qW02IwQ`#JC&0620#z$ z7fsyvo&`oc*6f4ar zAoxWhQwE#d{sslZM|<1;bULO6iNy8(;$upHkCp0%V{pB0ZMqx8Q@!opK`x>ONkqSG ztKR>5szM`DquWQoh4FliPM?s*`)r@~=rZQ!tr6KC(CLg+`0;yj#BKTtH7#3=WaWip zb*vzWzs!IDM({_T>S!yP#(7O%#tO=lI$^$RjDzx^uYF_r;@BOmmK9Wj&mZ%q!eO zf1R)G>*cq{aMqNdRH7kD`w=i2LyWkGB3SGIReqwp(Fd>!LTQ(oLRx0)v5o_%3^Oc& z7OTfGm*q1XwVo|BabGLn)PeRlOt-LV;c5N!+-mBRWkaxSMiYTQ=zET&V4&zdATSd6 z?L93HNou+Jx%!IEv`&rxT@Rgp=Oo`+0vWiTx^XJi3{OAL8sKZHE*n}G%PQ8BQJz8z zD!Z=fTCXqJ=|Ph;dC*WpQNjxuyl?pDJ0%}9*0baMMQjEaB>ZCJ^8W=q9lT zWV>$x{nCg_iy_n7)aM|@Yks=zKBK2VZ(LEM zVpH_9aoKIH*<7acCJIhZ^m9!Lb;sjb-8L)&U_Fsh82Iify-fn7eCcYRC#lL-wc}Yyv%7FY;?sfJ5dn`!ls94T`_oGSy|HG zd82IQpeMfPb?-)oo^-{af}`OEsppVtC5{|FiH@^VL`v$26iSYeW+g5yFgqGLJYKV0 z$mC|j+VMfprJM{Bn(DO)=P8pL=?(N0prit=Jlg*U_;SI_h zqe785T2uR?+1SIYe}_DOf^*01<9H!uLPsCM;r^4n?)weO8#|t6E%M=w_vC{Px>=%? zLOt9ssQ-2WRN&}8{LQLY8e$z<#q}<@)OaIi#@8;patsFb8}idGVW0cT%IG~^63nN2 ziRdgdJfonaJ44yOxn8l_gi;o6pn~OSM|&=+TuYL-mkl_NfRr1VztAvyQ(z#jWyssV zV+k9+KQmW|!Hcm8c=L&xm*YPSBA@!9bjJm2CxHm%&C4H8vV@n3Zs+38lCatU?qdAGWRUnMzA=e|f*Ly5ZhANG_z2>9u8xmM>PG342vJY}!d1+Ah6JeUxT# z;QTo8%?}`p`A#)Y-`tN>iD2jCz*nW?(#Ks zc;*)f$wSKF7E(g`<;wgF!9T#tl3evj2&wFO#M+b1pbDS8>JCaUX`O%KXuMcEKNzc*DS5WK z8!_c#!=@|1eAzcJ(hMh_Cl|D8t=ZRd4BoCaf!$7F^k6P@!#WaBROPr+qRQ}g4t%c8 z#Cci}Wpqm!gQ4qoYRt;TMqs)H;d42jvo6pL?d{NNxtND*>HoT)h>TMh*@MwTOYux@ zPdvE?2b{{%4lff>p^99(d(0;ps^6Fr+VH`{7)|oS;^p4-<+RmL?E#ZHswc;!)q`z_ zG{NFiH>(6UPfCI!9E2jJ*`R^o&~rN9y0Am^@3qN0GP%cg@9b}Iy%wq7-FjCZRe4--`bw@$~#T$jZ%>G|~`RkUUROHIXOHX4U#CV&9 Msf`KE=ycRS0CfaELjV8( literal 0 HcmV?d00001 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 GIT binary patch literal 10671 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 literal 0 HcmV?d00001 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 GIT binary patch literal 24652 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 F@K3$OYOMeO literal 0 HcmV?d00001 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 GIT binary patch literal 20698 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 axe6{9Y*Wo$uphGk literal 0 HcmV?d00001 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 GIT binary patch literal 20057 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 A8UO$Q literal 0 HcmV?d00001 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 GIT binary patch literal 19032 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 C7m%L- literal 0 HcmV?d00001 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 GIT binary patch literal 27084 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�!dc8?LC4Xo$i!u1UWAz~- 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 z zhxcX?9_{g{v38tv zU*@L(yhAt>m<5#%nz`U&x|qZEC|vxHS(N|xoW+0J@cEy5alHirXN7IG%E0BB@ONO@u>S<%>;2jQ literal 0 HcmV?d00001 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 GIT binary patch literal 19440 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

+ * 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 000000000..329b260cc --- /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 000000000..746069e23 --- /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 000000000..4603af282 --- /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 000000000..8f178a991 --- /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 000000000..6451b5d66 --- /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 000000000..6f27dc5ab --- /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 000000000..622c5aa00 --- /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 000000000..caebd95e4 --- /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 000000000..3d3086f6c --- /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 000000000..ee129282f --- /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 000000000..8c4846908 --- /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 000000000..8c419082e --- /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 000000000..eb218c7e0 --- /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 000000000..ced2e9c90 --- /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 000000000..e2830055c --- /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 000000000..cff9b8051 --- /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 000000000..9a019ef18 --- /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 000000000..935301bad --- /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 000000000..15af42f68 --- /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 000000000..dc8c00218 --- /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 000000000..76cc33e62 --- /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 000000000..eedc8fe2d --- /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 000000000..1af96edf7 --- /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 000000000..2d7a9b52f --- /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 000000000..c6ba1ba18 --- /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 000000000..d2a5a19b6 --- /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 000000000..76b915f1d --- /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 000000000..34beea03d --- /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 000000000..7944355b1 --- /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 000000000..150590a77 --- /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 000000000..152b49d22 --- /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 000000000..3bddc6d90 --- /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 000000000..5d016ca5e --- /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 000000000..fa1518e05 --- /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 000000000..33dfe0df7 --- /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 000000000..49a10e336 --- /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 000000000..04b296cca --- /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 000000000..b97031286 --- /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 000000000..8805b0bb3 --- /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 000000000..c644ab4e1 --- /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 000000000..521a542dc --- /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 000000000..0e321fdb8 --- /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 000000000..ef9650ce8 --- /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 000000000..4a1fb1951 --- /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 000000000..ce48080a3 --- /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 000000000..c7a5c0bd5 --- /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 000000000..f3abe6fca --- /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 000000000..60d52c771 --- /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 000000000..0d192d667 --- /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 000000000..d6314d855 --- /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

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 H7ykbX*5gm0 literal 0 HcmV?d00001 diff --git a/mabe-lanterna/docs/examples/gui/tables.md b/mabe-lanterna/docs/examples/gui/tables.md new file mode 100644 index 000000000..b523dbe1f --- /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 000000000..808ad5439 --- /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 000000000..6fe67909e --- /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 000000000..56cd09422 --- /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 000000000..b48805421 --- /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 000000000..77d4e3f99 --- /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 000000000..f504daf0b --- /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 000000000..8235f70d4 --- /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 000000000..6b40a19cf --- /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 000000000..59b9e2ffd --- /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 000000000..0bd8a0923 --- /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 000000000..d9585515a --- /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 000000000..f381851e7 --- /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 000000000..af92df813 --- /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 000000000..b7dc266e6 --- /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 000000000..75376c00d --- /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 000000000..f4cf36c1f --- /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 000000000..6e5c86e23 --- /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 000000000..24e9b88b3 --- /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 000000000..3018df3d1 --- /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 000000000..7431dc873 --- /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 000000000..b370a76d8 --- /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 000000000..6c4e6b87e --- /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 000000000..a8fb28dce --- /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 000000000..db8406978 --- /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 000000000..943afecb6 --- /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 000000000..f40185d8f --- /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 000000000..b97afb9c8 --- /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. + *